fix(highlight,parser,init): line_hl_group, clear_end, did_filetype, email-quoted diffs

Problem: line backgrounds used `hl_group + end_row` multirow extmarks
vulnerable to adjacent `clear_namespace`. `clear_end` was off by one.
`vim.filetype.match` returned nil for function-handled extensions
(`.sh`, `.bash`, etc.) when `did_filetype() != 0`. Parser didn't
handle email-quoted diff prefixes (`> `).

Solution: use `line_hl_group` single-point extmarks. Fix `clear_end`
to `hunk.start_line + #hunk.lines`. Override `vim.fn.did_filetype`
via `rawset` during retry. Strip `> ` quote prefixes in parser and
store `quote_width` per hunk.
This commit is contained in:
Barrett Ruth 2026-03-04 17:29:19 -05:00
parent 0f0acacf96
commit 13c0ea1e92
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
9 changed files with 552 additions and 52 deletions

View file

@ -496,17 +496,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
if opts.highlights.background and is_diff_line then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
end_row = buf_line + 1,
hl_group = line_hl,
hl_eol = true,
line_hl_group = line_hl,
number_hl_group = opts.highlights.gutter and number_hl or nil,
priority = p.line_bg,
})
if opts.highlights.gutter then
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
number_hl_group = number_hl,
priority = p.line_bg,
})
end
end
if is_marker and line_len > pw then

View file

@ -765,7 +765,7 @@ local function init()
if not entry.highlighted[i] then
local hunk = entry.hunks[i]
local clear_start = hunk.start_line - 1
local clear_end = clear_start + #hunk.lines
local clear_end = hunk.start_line + #hunk.lines
if hunk.header_start_line then
clear_start = hunk.header_start_line - 1
end
@ -799,7 +799,7 @@ local function init()
}
for _, hunk in ipairs(deferred_syntax) do
local start_row = hunk.start_line - 1
local end_row = start_row + #hunk.lines
local end_row = hunk.start_line + #hunk.lines
if hunk.header_start_line then
start_row = hunk.header_start_line - 1
end
@ -953,6 +953,7 @@ M._test = {
invalidate_cache = invalidate_cache,
hunks_eq = hunks_eq,
process_pending_clear = process_pending_clear,
ft_retry_pending = ft_retry_pending,
}
return M

View file

@ -13,6 +13,7 @@
---@field file_new_start integer?
---@field file_new_count integer?
---@field prefix_width integer
---@field quote_width integer
---@field repo_root string?
local M = {}
@ -60,6 +61,13 @@ local function get_ft_from_filename(filename, repo_root)
end
local ft = vim.filetype.match({ filename = filename })
if not ft and vim.fn.did_filetype() ~= 0 then
dbg('retrying filetype match for %s (clearing did_filetype)', filename)
local saved = rawget(vim.fn, 'did_filetype')
rawset(vim.fn, 'did_filetype', function() return 0 end)
ft = vim.filetype.match({ filename = filename })
rawset(vim.fn, 'did_filetype', saved)
end
if ft then
dbg('filetype from filename: %s', ft)
return ft
@ -125,6 +133,18 @@ end
function M.parse_buffer(bufnr)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local repo_root = get_repo_root(bufnr)
local quote_prefix = nil
local quote_width = 0
for _, l in ipairs(lines) do
local qp = l:match('^(>+ )diff ') or l:match('^(>+ )@@')
if qp then
quote_prefix = qp
quote_width = #qp
break
end
end
---@type diffs.Hunk[]
local hunks = {}
@ -163,6 +183,7 @@ function M.parse_buffer(bufnr)
---@type integer?
local new_remaining = nil
local is_unified_diff = false
local current_quote_width = 0
local function flush_hunk()
if hunk_start and #hunk_lines > 0 then
@ -175,6 +196,7 @@ function M.parse_buffer(bufnr)
header_context_col = hunk_header_context_col,
lines = hunk_lines,
prefix_width = hunk_prefix_width,
quote_width = current_quote_width,
file_old_start = file_old_start,
file_old_count = file_old_count,
file_new_start = file_new_start,
@ -200,18 +222,31 @@ function M.parse_buffer(bufnr)
end
for i, line in ipairs(lines) do
local diff_git_file = line:match('^diff %-%-git a/.+ b/(.+)$')
local neogit_file = line:match('^modified%s+(.+)$')
or (not line:match('^new file mode') and line:match('^new file%s+(.+)$'))
or (not line:match('^deleted file mode') and line:match('^deleted%s+(.+)$'))
or line:match('^renamed%s+(.+)$')
or line:match('^copied%s+(.+)$')
local bare_file = not hunk_start and line:match('^([^%s]+%.[^%s]+)$')
local filename = line:match('^[MADRCU%?!]%s+(.+)$') or diff_git_file or neogit_file or bare_file
local logical = line
if quote_prefix then
if line:sub(1, quote_width) == quote_prefix then
logical = line:sub(quote_width + 1)
elseif line:match('^>+$') then
logical = ''
end
end
local diff_git_file = logical:match('^diff %-%-git a/.+ b/(.+)$')
local neogit_file = logical:match('^modified%s+(.+)$')
or (not logical:match('^new file mode') and logical:match('^new file%s+(.+)$'))
or (not logical:match('^deleted file mode') and logical:match('^deleted%s+(.+)$'))
or logical:match('^renamed%s+(.+)$')
or logical:match('^copied%s+(.+)$')
local bare_file = not hunk_start and logical:match('^([^%s]+%.[^%s]+)$')
local filename = logical:match('^[MADRCU%?!]%s+(.+)$')
or diff_git_file
or neogit_file
or bare_file
if filename then
is_unified_diff = diff_git_file ~= nil
flush_hunk()
current_filename = filename
current_quote_width = (logical ~= line) and quote_width or 0
local cache_key = (repo_root or '') .. '\0' .. filename
local cached = ft_lang_cache[cache_key]
if cached then
@ -233,13 +268,13 @@ function M.parse_buffer(bufnr)
hunk_prefix_width = 1
header_start = i
header_lines = {}
elseif line:match('^@@+') then
elseif logical:match('^@@+') then
flush_hunk()
hunk_start = i
local at_prefix = line:match('^(@@+)')
local at_prefix = logical:match('^(@@+)')
hunk_prefix_width = #at_prefix - 1
if #at_prefix == 2 then
local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
local hs, hc, hs2, hc2 = logical:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
if hs then
file_old_start = tonumber(hs)
file_old_count = tonumber(hc) or 1
@ -249,24 +284,24 @@ function M.parse_buffer(bufnr)
new_remaining = file_new_count
end
else
local hs2, hc2 = line:match('%+(%d+),?(%d*) @@')
local hs2, hc2 = logical:match('%+(%d+),?(%d*) @@')
if hs2 then
file_new_start = tonumber(hs2)
file_new_count = tonumber(hc2) or 1
end
end
local at_end, context = line:match('^(@@+.-@@+%s*)(.*)')
local at_end, context = logical:match('^(@@+.-@@+%s*)(.*)')
if context and context ~= '' then
hunk_header_context = context
hunk_header_context_col = #at_end
hunk_header_context_col = #at_end + current_quote_width
end
if hunk_count then
hunk_count = hunk_count + 1
end
elseif hunk_start then
local prefix = line:sub(1, 1)
local prefix = logical:sub(1, 1)
if prefix == ' ' or prefix == '+' or prefix == '-' then
table.insert(hunk_lines, line)
table.insert(hunk_lines, logical)
if old_remaining and (prefix == ' ' or prefix == '-') then
old_remaining = old_remaining - 1
end
@ -274,7 +309,7 @@ function M.parse_buffer(bufnr)
new_remaining = new_remaining - 1
end
elseif
line == ''
logical == ''
and is_unified_diff
and old_remaining
and old_remaining > 0
@ -285,11 +320,11 @@ function M.parse_buffer(bufnr)
old_remaining = old_remaining - 1
new_remaining = new_remaining - 1
elseif
line == ''
or line:match('^[MADRC%?!]%s+')
or line:match('^diff ')
or line:match('^index ')
or line:match('^Binary ')
logical == ''
or logical:match('^[MADRC%?!]%s+')
or logical:match('^diff ')
or logical:match('^index ')
or logical:match('^Binary ')
then
flush_hunk()
current_filename = nil
@ -299,7 +334,7 @@ function M.parse_buffer(bufnr)
end
end
if header_start and not hunk_start then
table.insert(header_lines, line)
table.insert(header_lines, logical)
end
end