fix(highlight): split old/new treesitter parsing

Problem: highlight_treesitter concatenated all hunk lines (context, -,
+) into a single string. Mixed old/new code produced invalid syntax
(e.g. two return statements), causing treesitter error recovery to drop
captures on lines after the syntax error.

Solution: split hunk lines into two versions — new (context + added)
and old (context + deleted) — each parsed independently. Use a line_map
to resolve treesitter row indices to buffer lines, with the old version
only mapping deleted lines to avoid duplicate extmarks on context.

Also fixes three related issues exposed by the improved TS coverage:

- Replace Normal extmark with DiffsClear (explicit fg from Normal.fg).
  Normal in extmarks doesn't reliably override vim :syntax foreground.

- Reorder priority stack to DiffsClear(198) < syntax(199) < line
  bg(200) < char bg(201). TS capture groups can carry colorscheme
  backgrounds that would override diff line backgrounds at higher
  priority.

- Gate DiffsClear on per-line coverage tracking. Only clear fugitive
  syntax fg on lines where TS/vim actually produced captures, preventing
  force-clearing on lines where error recovery drops captures.
This commit is contained in:
Barrett Ruth 2026-02-07 00:50:21 -05:00
parent 93f6627dd2
commit bbb87b660e
3 changed files with 175 additions and 97 deletions

View file

@ -7,8 +7,10 @@ describe('highlight', function()
before_each(function()
ns = vim.api.nvim_create_namespace('diffs_test')
local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg })
vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg })
end)
@ -82,7 +84,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('applies Normal extmarks to clear diff colors', function()
it('applies DiffsClear extmarks to clear diff colors', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
@ -99,14 +101,46 @@ describe('highlight', function()
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local has_normal = false
local has_clear = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group == 'Normal' then
has_normal = true
if mark[4] and mark[4].hl_group == 'DiffsClear' then
has_clear = true
break
end
end
assert.is_true(has_normal)
assert.is_true(has_clear)
delete_buffer(bufnr)
end)
it('produces treesitter captures on all lines with split parsing', function()
local bufnr = create_buffer({
'@@ -1,3 +1,3 @@',
' local x = 1',
'-local y = 2',
'+local y = 3',
' return x',
})
local hunk = {
filename = 'test.lua',
lang = 'lua',
start_line = 1,
lines = { ' local x = 1', '-local y = 2', '+local y = 3', ' return x' },
}
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local lines_with_ts = {}
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
lines_with_ts[mark[2]] = true
end
end
assert.is_true(lines_with_ts[1] ~= nil)
assert.is_true(lines_with_ts[2] ~= nil)
assert.is_true(lines_with_ts[3] ~= nil)
assert.is_true(lines_with_ts[4] ~= nil)
delete_buffer(bufnr)
end)
@ -576,7 +610,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_syntax_hl = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'DiffsClear' then
has_syntax_hl = true
break
end
@ -610,7 +644,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_syntax_hl = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then
if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'DiffsClear' then
has_syntax_hl = true
break
end
@ -682,7 +716,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('applies Normal blanking for vim fallback hunks', function()
it('applies DiffsClear blanking for vim fallback hunks', function()
local orig_synID = vim.fn.synID
local orig_synIDtrans = vim.fn.synIDtrans
local orig_synIDattr = vim.fn.synIDattr
@ -722,14 +756,14 @@ describe('highlight', function()
vim.fn.synIDattr = orig_synIDattr
local extmarks = get_extmarks(bufnr)
local has_normal = false
local has_clear = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group == 'Normal' then
has_normal = true
if mark[4] and mark[4].hl_group == 'DiffsClear' then
has_clear = true
break
end
end
assert.is_true(has_normal)
assert.is_true(has_clear)
delete_buffer(bufnr)
end)
@ -765,7 +799,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('line bg priority > Normal priority', function()
it('line bg priority > DiffsClear priority', function()
local bufnr = create_buffer({
'@@ -1,2 +1,1 @@',
'-local x = 1',
@ -787,20 +821,20 @@ describe('highlight', function()
)
local extmarks = get_extmarks(bufnr)
local normal_priority = nil
local clear_priority = nil
local line_bg_priority = nil
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d and d.hl_group == 'Normal' then
normal_priority = d.priority
if d and d.hl_group == 'DiffsClear' then
clear_priority = d.priority
end
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
line_bg_priority = d.priority
end
end
assert.is_not_nil(normal_priority)
assert.is_not_nil(clear_priority)
assert.is_not_nil(line_bg_priority)
assert.is_true(line_bg_priority > normal_priority)
assert.is_true(line_bg_priority > clear_priority)
delete_buffer(bufnr)
end)
@ -960,7 +994,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
it('enforces priority order: Normal < line bg < syntax < char bg', function()
it('enforces priority order: DiffsClear < syntax < line bg < char bg', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
@ -990,12 +1024,12 @@ describe('highlight', function()
)
local extmarks = get_extmarks(bufnr)
local priorities = { normal = {}, line_bg = {}, syntax = {}, char_bg = {} }
local priorities = { clear = {}, line_bg = {}, syntax = {}, char_bg = {} }
for _, mark in ipairs(extmarks) do
local d = mark[4]
if d then
if d.hl_group == 'Normal' then
table.insert(priorities.normal, d.priority)
if d.hl_group == 'DiffsClear' then
table.insert(priorities.clear, d.priority)
elseif d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete' then
table.insert(priorities.line_bg, d.priority)
elseif d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText' then
@ -1006,19 +1040,19 @@ describe('highlight', function()
end
end
assert.is_true(#priorities.normal > 0)
assert.is_true(#priorities.clear > 0)
assert.is_true(#priorities.line_bg > 0)
assert.is_true(#priorities.syntax > 0)
assert.is_true(#priorities.char_bg > 0)
local max_normal = math.max(unpack(priorities.normal))
local max_clear = math.max(unpack(priorities.clear))
local min_line_bg = math.min(unpack(priorities.line_bg))
local min_syntax = math.min(unpack(priorities.syntax))
local min_char_bg = math.min(unpack(priorities.char_bg))
assert.is_true(max_normal < min_line_bg)
assert.is_true(min_line_bg < min_syntax)
assert.is_true(min_syntax < min_char_bg)
assert.is_true(max_clear < min_syntax)
assert.is_true(min_syntax < min_line_bg)
assert.is_true(min_line_bg < min_char_bg)
delete_buffer(bufnr)
end)
end)
@ -1214,7 +1248,7 @@ describe('highlight', function()
}
end
it('uses priority 200 for code languages', function()
it('uses priority 199 for code languages', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
@ -1231,16 +1265,16 @@ describe('highlight', function()
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
local extmarks = get_extmarks(bufnr)
local has_priority_200 = false
local has_priority_199 = false
for _, mark in ipairs(extmarks) do
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
if mark[4].priority == 200 then
has_priority_200 = true
if mark[4].priority == 199 then
has_priority_199 = true
break
end
end
end
assert.is_true(has_priority_200)
assert.is_true(has_priority_199)
delete_buffer(bufnr)
end)
@ -1278,7 +1312,7 @@ describe('highlight', function()
end
assert.is_true(#diff_extmark_priorities > 0)
for _, priority in ipairs(diff_extmark_priorities) do
assert.is_true(priority < 200)
assert.is_true(priority < 199)
end
delete_buffer(bufnr)
end)