feat(config): replace algorithm 'auto'/'native' with 'default'/'vscode'

'default' inherits algorithm and linematch from diffopt, 'vscode' uses
the FFI library. Removes the need for diffs.nvim to duplicate settings
that users already control globally.
This commit is contained in:
Barrett Ruth 2026-02-06 21:23:40 -05:00
parent cc947167c3
commit 10af59a70d
6 changed files with 85 additions and 47 deletions

View file

@ -17,6 +17,7 @@ syntax highlighting.
- Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`) - Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`)
- Vim syntax fallback for languages without a treesitter parser - Vim syntax fallback for languages without a treesitter parser
- Hunk header context highlighting (`@@ ... @@ function foo()`) - Hunk header context highlighting (`@@ ... @@ function foo()`)
- Character-level (intra-line) diff highlighting for changed characters
- Configurable debouncing, max lines, and diff prefix concealment - Configurable debouncing, max lines, and diff prefix concealment
## Requirements ## Requirements

View file

@ -66,7 +66,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
}, },
intra = { intra = {
enabled = true, enabled = true,
algorithm = 'auto', algorithm = 'default',
max_lines = 500, max_lines = 500,
}, },
}, },
@ -158,12 +158,12 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
an intense background overlay while the rest of the an intense background overlay while the rest of the
line keeps the softer line-level background. line keeps the softer line-level background.
{algorithm} (string, default: 'auto') {algorithm} (string, default: 'default')
Diff algorithm for character-level analysis. Diff algorithm for character-level analysis.
`'auto'`: use libvscodediff if available, else `'default'`: use |vim.diff()| with settings
native `vim.diff()`. `'native'`: always use inherited from |'diffopt'| (`algorithm` and
`vim.diff()`. `'vscode'`: require libvscodediff `linematch`). `'vscode'`: use libvscodediff FFI
(falls back to native if not available). (falls back to default if not available).
{max_lines} (integer, default: 500) {max_lines} (integer, default: 500)
Skip character-level highlighting for hunks larger Skip character-level highlighting for hunks larger
@ -285,8 +285,8 @@ Summary / commit detail views: ~
- Code is parsed with |vim.treesitter.get_string_parser()| - Code is parsed with |vim.treesitter.get_string_parser()|
- If no treesitter parser and `vim.enabled`: vim syntax fallback via - If no treesitter parser and `vim.enabled`: vim syntax fallback via
scratch buffer and |synID()| scratch buffer and |synID()|
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 198 - `Normal` extmarks at priority 198 clear underlying diff foreground
- `Normal` extmarks at priority 199 clear underlying diff foreground - Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 199
- Syntax highlights are applied as extmarks at priority 200 - Syntax highlights are applied as extmarks at priority 200
- Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at - Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at
priority 201 overlay changed characters with an intense background priority 201 overlay changed characters with an intense background
@ -383,15 +383,14 @@ Fugitive unified diff highlights: ~
*DiffsAddText* *DiffsAddText*
DiffsAddText Character-level background for changed characters DiffsAddText Character-level background for changed characters
within `+` lines. Derived by blending `diffAdded` within `+` lines. Derived by blending `diffAdded`
foreground with `Normal` background at 40% alpha. foreground with `Normal` background at 70% alpha.
Uses the same base color as `DiffsAddNr` foreground, Only sets `bg`, so treesitter foreground colors show
making changed characters clearly visible. Only sets through.
`bg`, so treesitter foreground colors show through.
*DiffsDeleteText* *DiffsDeleteText*
DiffsDeleteText Character-level background for changed characters DiffsDeleteText Character-level background for changed characters
within `-` lines. Derived by blending `diffRemoved` within `-` lines. Derived by blending `diffRemoved`
foreground with `Normal` background at 40% alpha. foreground with `Normal` background at 70% alpha.
Diff mode window highlights: ~ Diff mode window highlights: ~
These are used for |winhighlight| remapping in `&diff` windows. These are used for |winhighlight| remapping in `&diff` windows.

View file

@ -60,11 +60,35 @@ function M.extract_change_groups(hunk_lines)
return groups return groups
end end
---@return {algorithm?: string, linematch?: integer}
local function parse_diffopt()
local opts = {}
for _, item in ipairs(vim.split(vim.o.diffopt, ',')) do
local key, val = item:match('^(%w+):(.+)$')
if key == 'algorithm' then
opts.algorithm = val
elseif key == 'linematch' then
opts.linematch = tonumber(val)
end
end
return opts
end
---@param old_text string ---@param old_text string
---@param new_text string ---@param new_text string
---@param diff_opts? {algorithm?: string, linematch?: integer}
---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[] ---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
local function byte_diff(old_text, new_text) local function byte_diff(old_text, new_text, diff_opts)
local ok, result = pcall(vim.diff, old_text, new_text, { result_type = 'indices' }) local vim_opts = { result_type = 'indices' }
if diff_opts then
if diff_opts.algorithm then
vim_opts.algorithm = diff_opts.algorithm
end
if diff_opts.linematch then
vim_opts.linematch = diff_opts.linematch
end
end
local ok, result = pcall(vim.diff, old_text, new_text, vim_opts)
if not ok or not result then if not ok or not result then
return {} return {}
end end
@ -95,8 +119,9 @@ end
---@param new_line string ---@param new_line string
---@param del_idx integer ---@param del_idx integer
---@param add_idx integer ---@param add_idx integer
---@param diff_opts? {algorithm?: string, linematch?: integer}
---@return diffs.CharSpan[], diffs.CharSpan[] ---@return diffs.CharSpan[], diffs.CharSpan[]
local function char_diff_pair(old_line, new_line, del_idx, add_idx) local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts)
---@type diffs.CharSpan[] ---@type diffs.CharSpan[]
local del_spans = {} local del_spans = {}
---@type diffs.CharSpan[] ---@type diffs.CharSpan[]
@ -108,7 +133,7 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx)
local old_text = table.concat(old_bytes, '\n') .. '\n' local old_text = table.concat(old_bytes, '\n') .. '\n'
local new_text = table.concat(new_bytes, '\n') .. '\n' local new_text = table.concat(new_bytes, '\n') .. '\n'
local char_hunks = byte_diff(old_text, new_text) local char_hunks = byte_diff(old_text, new_text, diff_opts)
for _, ch in ipairs(char_hunks) do for _, ch in ipairs(char_hunks) do
if ch.old_count > 0 then if ch.old_count > 0 then
@ -132,8 +157,9 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx)
end end
---@param group diffs.ChangeGroup ---@param group diffs.ChangeGroup
---@param diff_opts? {algorithm?: string, linematch?: integer}
---@return diffs.CharSpan[], diffs.CharSpan[] ---@return diffs.CharSpan[], diffs.CharSpan[]
local function diff_group_native(group) local function diff_group_native(group, diff_opts)
---@type diffs.CharSpan[] ---@type diffs.CharSpan[]
local all_del = {} local all_del = {}
---@type diffs.CharSpan[] ---@type diffs.CharSpan[]
@ -147,7 +173,8 @@ local function diff_group_native(group)
group.del_lines[1].text, group.del_lines[1].text,
group.add_lines[1].text, group.add_lines[1].text,
group.del_lines[1].idx, group.del_lines[1].idx,
group.add_lines[1].idx group.add_lines[1].idx,
diff_opts
) )
vim.list_extend(all_del, ds) vim.list_extend(all_del, ds)
vim.list_extend(all_add, as) vim.list_extend(all_add, as)
@ -166,7 +193,7 @@ local function diff_group_native(group)
local old_block = table.concat(old_texts, '\n') .. '\n' local old_block = table.concat(old_texts, '\n') .. '\n'
local new_block = table.concat(new_texts, '\n') .. '\n' local new_block = table.concat(new_texts, '\n') .. '\n'
local line_hunks = byte_diff(old_block, new_block) local line_hunks = byte_diff(old_block, new_block, diff_opts)
---@type table<integer, integer> ---@type table<integer, integer>
local old_to_new = {} local old_to_new = {}
@ -184,7 +211,8 @@ local function diff_group_native(group)
group.del_lines[old_i].text, group.del_lines[old_i].text,
group.add_lines[new_i].text, group.add_lines[new_i].text,
group.del_lines[old_i].idx, group.del_lines[old_i].idx,
group.add_lines[new_i].idx group.add_lines[new_i].idx,
diff_opts
) )
vim.list_extend(all_del, ds) vim.list_extend(all_del, ds)
vim.list_extend(all_add, as) vim.list_extend(all_add, as)
@ -202,7 +230,8 @@ local function diff_group_native(group)
group.del_lines[oi].text, group.del_lines[oi].text,
group.add_lines[ni].text, group.add_lines[ni].text,
group.del_lines[oi].idx, group.del_lines[oi].idx,
group.add_lines[ni].idx group.add_lines[ni].idx,
diff_opts
) )
vim.list_extend(all_del, ds) vim.list_extend(all_del, ds)
vim.list_extend(all_add, as) vim.list_extend(all_add, as)
@ -295,16 +324,25 @@ function M.compute_intra_hunks(hunk_lines, algorithm)
return nil return nil
end end
algorithm = algorithm or 'auto' algorithm = algorithm or 'default'
local lib = require('diffs.lib')
local vscode_handle = nil local vscode_handle = nil
if algorithm ~= 'native' then if algorithm == 'vscode' then
vscode_handle = lib.load() vscode_handle = require('diffs.lib').load()
if not vscode_handle then
dbg('vscode algorithm requested but library not available, falling back to default')
end
end end
if algorithm == 'vscode' and not vscode_handle then local diff_opts = nil
dbg('vscode algorithm requested but library not available, falling back to native') if not vscode_handle then
diff_opts = parse_diffopt()
if diff_opts.algorithm then
dbg('diffopt algorithm: %s', diff_opts.algorithm)
end
if diff_opts.linematch then
dbg('diffopt linematch: %d', diff_opts.linematch)
end
end end
---@type diffs.CharSpan[] ---@type diffs.CharSpan[]
@ -325,7 +363,7 @@ function M.compute_intra_hunks(hunk_lines, algorithm)
if vscode_handle then if vscode_handle then
ds, as = diff_group_vscode(group, vscode_handle) ds, as = diff_group_vscode(group, vscode_handle)
else else
ds, as = diff_group_native(group) ds, as = diff_group_native(group, diff_opts)
end end
dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as) dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as)
for _, s in ipairs(ds) do for _, s in ipairs(ds) do

View file

@ -90,7 +90,7 @@ local default_config = {
}, },
intra = { intra = {
enabled = true, enabled = true,
algorithm = 'auto', algorithm = 'default',
max_lines = 500, max_lines = 500,
}, },
}, },
@ -259,9 +259,9 @@ local function init()
['highlights.intra.algorithm'] = { ['highlights.intra.algorithm'] = {
opts.highlights.intra.algorithm, opts.highlights.intra.algorithm,
function(v) function(v)
return v == nil or v == 'auto' or v == 'native' or v == 'vscode' return v == nil or v == 'default' or v == 'vscode'
end, end,
"'auto', 'native', or 'vscode'", "'default' or 'vscode'",
}, },
['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true }, ['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true },
}) })

View file

@ -73,17 +73,17 @@ describe('diff', function()
describe('compute_intra_hunks', function() describe('compute_intra_hunks', function()
it('returns nil for all-addition hunks', function() it('returns nil for all-addition hunks', function()
local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'native') local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'default')
assert.is_nil(result) assert.is_nil(result)
end) end)
it('returns nil for all-deletion hunks', function() it('returns nil for all-deletion hunks', function()
local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'native') local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'default')
assert.is_nil(result) assert.is_nil(result)
end) end)
it('returns nil for context-only hunks', function() it('returns nil for context-only hunks', function()
local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'native') local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'default')
assert.is_nil(result) assert.is_nil(result)
end) end)
@ -91,7 +91,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({ local result = diff.compute_intra_hunks({
'-local x = 1', '-local x = 1',
'+local x = 2', '+local x = 2',
}, 'native') }, 'default')
assert.is_not_nil(result) assert.is_not_nil(result)
assert.is_true(#result.del_spans > 0) assert.is_true(#result.del_spans > 0)
assert.is_true(#result.add_spans > 0) assert.is_true(#result.add_spans > 0)
@ -101,7 +101,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({ local result = diff.compute_intra_hunks({
'-local x = 1', '-local x = 1',
'+local x = 2', '+local x = 2',
}, 'native') }, 'default')
assert.is_not_nil(result) assert.is_not_nil(result)
assert.are.equal(1, #result.del_spans) assert.are.equal(1, #result.del_spans)
@ -121,7 +121,7 @@ describe('diff', function()
' local b = 3', ' local b = 3',
'-local c = 4', '-local c = 4',
'+local c = 5', '+local c = 5',
}, 'native') }, 'default')
assert.is_not_nil(result) assert.is_not_nil(result)
assert.is_true(#result.del_spans >= 2) assert.is_true(#result.del_spans >= 2)
assert.is_true(#result.add_spans >= 2) assert.is_true(#result.add_spans >= 2)
@ -132,7 +132,7 @@ describe('diff', function()
'-line one', '-line one',
'-line two', '-line two',
'+line combined', '+line combined',
}, 'native') }, 'default')
assert.is_not_nil(result) assert.is_not_nil(result)
end) end)
@ -140,7 +140,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({ local result = diff.compute_intra_hunks({
'-local x = "héllo"', '-local x = "héllo"',
'+local x = "wörld"', '+local x = "wörld"',
}, 'native') }, 'default')
assert.is_not_nil(result) assert.is_not_nil(result)
assert.is_true(#result.del_spans > 0) assert.is_true(#result.del_spans > 0)
assert.is_true(#result.add_spans > 0) assert.is_true(#result.add_spans > 0)
@ -150,7 +150,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({ local result = diff.compute_intra_hunks({
'-local x = 1', '-local x = 1',
'+local x = 1', '+local x = 1',
}, 'native') }, 'default')
assert.is_nil(result) assert.is_nil(result)
end) end)
end) end)

View file

@ -45,7 +45,7 @@ describe('highlight', function()
}, },
intra = { intra = {
enabled = false, enabled = false,
algorithm = 'native', algorithm = 'default',
max_lines = 500, max_lines = 500,
}, },
}, },
@ -828,7 +828,7 @@ describe('highlight', function()
default_opts({ default_opts({
highlights = { highlights = {
background = true, background = true,
intra = { enabled = true, algorithm = 'native', max_lines = 500 }, intra = { enabled = true, algorithm = 'default', max_lines = 500 },
}, },
}) })
) )
@ -873,7 +873,7 @@ describe('highlight', function()
ns, ns,
hunk, hunk,
default_opts({ default_opts({
highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } },
}) })
) )
@ -913,7 +913,7 @@ describe('highlight', function()
ns, ns,
hunk, hunk,
default_opts({ default_opts({
highlights = { intra = { enabled = false, algorithm = 'native', max_lines = 500 } }, highlights = { intra = { enabled = false, algorithm = 'default', max_lines = 500 } },
}) })
) )
@ -947,7 +947,7 @@ describe('highlight', function()
ns, ns,
hunk, hunk,
default_opts({ default_opts({
highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } },
}) })
) )
@ -984,7 +984,7 @@ describe('highlight', function()
default_opts({ default_opts({
highlights = { highlights = {
background = true, background = true,
intra = { enabled = true, algorithm = 'native', max_lines = 500 }, intra = { enabled = true, algorithm = 'default', max_lines = 500 },
}, },
}) })
) )