Merge pull request #74 from barrettruth/feat/vscode-diff

feat(config): replace algorithm 'auto'/'native' with 'default'/'vscode'
This commit is contained in:
Barrett Ruth 2026-02-06 21:35:23 -05:00 committed by GitHub
commit b6f1c5b749
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 90 additions and 47 deletions

View file

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

View file

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

View file

@ -11,6 +11,10 @@
---@field del_lines {idx: integer, text: string}[]
---@field add_lines {idx: integer, text: string}[]
---@class diffs.DiffOpts
---@field algorithm? string
---@field linematch? integer
local M = {}
local dbg = require('diffs.log').dbg
@ -60,11 +64,35 @@ function M.extract_change_groups(hunk_lines)
return groups
end
---@return diffs.DiffOpts
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 new_text string
---@param diff_opts? diffs.DiffOpts
---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
local function byte_diff(old_text, new_text)
local ok, result = pcall(vim.diff, old_text, new_text, { result_type = 'indices' })
local function byte_diff(old_text, new_text, diff_opts)
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
return {}
end
@ -95,8 +123,9 @@ end
---@param new_line string
---@param del_idx integer
---@param add_idx integer
---@param diff_opts? diffs.DiffOpts
---@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[]
local del_spans = {}
---@type diffs.CharSpan[]
@ -108,7 +137,7 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx)
local old_text = table.concat(old_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
if ch.old_count > 0 then
@ -132,8 +161,9 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx)
end
---@param group diffs.ChangeGroup
---@param diff_opts? diffs.DiffOpts
---@return diffs.CharSpan[], diffs.CharSpan[]
local function diff_group_native(group)
local function diff_group_native(group, diff_opts)
---@type diffs.CharSpan[]
local all_del = {}
---@type diffs.CharSpan[]
@ -147,7 +177,8 @@ local function diff_group_native(group)
group.del_lines[1].text,
group.add_lines[1].text,
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_add, as)
@ -166,7 +197,7 @@ local function diff_group_native(group)
local old_block = table.concat(old_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>
local old_to_new = {}
@ -184,7 +215,8 @@ local function diff_group_native(group)
group.del_lines[old_i].text,
group.add_lines[new_i].text,
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_add, as)
@ -202,7 +234,8 @@ local function diff_group_native(group)
group.del_lines[oi].text,
group.add_lines[ni].text,
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_add, as)
@ -295,16 +328,26 @@ function M.compute_intra_hunks(hunk_lines, algorithm)
return nil
end
algorithm = algorithm or 'auto'
algorithm = algorithm or 'default'
local lib = require('diffs.lib')
local vscode_handle = nil
if algorithm ~= 'native' then
vscode_handle = lib.load()
if algorithm == 'vscode' then
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
if algorithm == 'vscode' and not vscode_handle then
dbg('vscode algorithm requested but library not available, falling back to native')
---@type diffs.DiffOpts?
local diff_opts = nil
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
---@type diffs.CharSpan[]
@ -325,7 +368,7 @@ function M.compute_intra_hunks(hunk_lines, algorithm)
if vscode_handle then
ds, as = diff_group_vscode(group, vscode_handle)
else
ds, as = diff_group_native(group)
ds, as = diff_group_native(group, diff_opts)
end
dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as)
for _, s in ipairs(ds) do

View file

@ -90,7 +90,7 @@ local default_config = {
},
intra = {
enabled = true,
algorithm = 'auto',
algorithm = 'default',
max_lines = 500,
},
},
@ -259,9 +259,9 @@ local function init()
['highlights.intra.algorithm'] = {
opts.highlights.intra.algorithm,
function(v)
return v == nil or v == 'auto' or v == 'native' or v == 'vscode'
return v == nil or v == 'default' or v == 'vscode'
end,
"'auto', 'native', or 'vscode'",
"'default' or 'vscode'",
},
['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()
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)
end)
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)
end)
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)
end)
@ -91,7 +91,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({
'-local x = 1',
'+local x = 2',
}, 'native')
}, 'default')
assert.is_not_nil(result)
assert.is_true(#result.del_spans > 0)
assert.is_true(#result.add_spans > 0)
@ -101,7 +101,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({
'-local x = 1',
'+local x = 2',
}, 'native')
}, 'default')
assert.is_not_nil(result)
assert.are.equal(1, #result.del_spans)
@ -121,7 +121,7 @@ describe('diff', function()
' local b = 3',
'-local c = 4',
'+local c = 5',
}, 'native')
}, 'default')
assert.is_not_nil(result)
assert.is_true(#result.del_spans >= 2)
assert.is_true(#result.add_spans >= 2)
@ -132,7 +132,7 @@ describe('diff', function()
'-line one',
'-line two',
'+line combined',
}, 'native')
}, 'default')
assert.is_not_nil(result)
end)
@ -140,7 +140,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({
'-local x = "héllo"',
'+local x = "wörld"',
}, 'native')
}, 'default')
assert.is_not_nil(result)
assert.is_true(#result.del_spans > 0)
assert.is_true(#result.add_spans > 0)
@ -150,7 +150,7 @@ describe('diff', function()
local result = diff.compute_intra_hunks({
'-local x = 1',
'+local x = 1',
}, 'native')
}, 'default')
assert.is_nil(result)
end)
end)

View file

@ -45,7 +45,7 @@ describe('highlight', function()
},
intra = {
enabled = false,
algorithm = 'native',
algorithm = 'default',
max_lines = 500,
},
},
@ -828,7 +828,7 @@ describe('highlight', function()
default_opts({
highlights = {
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,
hunk,
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,
hunk,
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,
hunk,
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({
highlights = {
background = true,
intra = { enabled = true, algorithm = 'native', max_lines = 500 },
intra = { enabled = true, algorithm = 'default', max_lines = 500 },
},
})
)