fix(highlight): support combined diff format for unmerged files (#106)
## Problem Fugitive shows combined diffs (`@@@` headers, 2-character prefixes like `++`, ` +`, `+ `) for unmerged (`UU`) files. The parser and highlight pipeline assumed unified diff format (`@@`, 1-char prefix), causing: - Prefix concealment only hiding 1 of 2 prefix chars - Missing background colors on ` +` and `+ ` lines (first char is space → misclassified as context) - No treesitter highlights (extra prefix char poisoned code arrays) - `U` file header not recognized by parser (missing from filename pattern) ## Solution Detect prefix width from leading `@` count in hunk headers (`@@` → 1, `@@@` → 2). Propagate `prefix_width` through the pipeline: - **Parser**: new `prefix_width` field on `diffs.Hunk`, `U` added to filename pattern, combined diff range extraction - **Highlight**: prefix stripping, `col_offset`, concealment width, and line classification all use `prefix_width` - **Intra-line**: skipped for combined diffs (`prefix_width > 1`) since 2-char prefix semantics don't produce meaningful change groups
This commit is contained in:
parent
59fcf14817
commit
cc5a368838
4 changed files with 484 additions and 28 deletions
|
|
@ -239,13 +239,14 @@ local function highlight_vim_syntax(
|
|||
pcall(vim.api.nvim_buf_delete, scratch, { force = true })
|
||||
|
||||
local hunk_line_count = #hunk.lines
|
||||
local col_off = (hunk.prefix_width or 1) - 1
|
||||
local extmark_count = 0
|
||||
for _, span in ipairs(spans) do
|
||||
local adj = span.line - leading_offset
|
||||
if adj >= 1 and adj <= hunk_line_count then
|
||||
local buf_line = hunk.start_line + adj - 1
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||
end_col = span.col_end,
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start + col_off, {
|
||||
end_col = span.col_end + col_off,
|
||||
hl_group = span.hl_name,
|
||||
priority = priorities.syntax,
|
||||
})
|
||||
|
|
@ -265,6 +266,7 @@ end
|
|||
---@param opts diffs.HunkOpts
|
||||
function M.highlight_hunk(bufnr, ns, hunk, opts)
|
||||
local p = opts.highlights.priorities
|
||||
local pw = hunk.prefix_width or 1
|
||||
local use_ts = hunk.lang and opts.highlights.treesitter.enabled
|
||||
local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled
|
||||
|
||||
|
|
@ -296,14 +298,16 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
local old_map = {}
|
||||
|
||||
for i, line in ipairs(hunk.lines) do
|
||||
local prefix = line:sub(1, 1)
|
||||
local stripped = line:sub(2)
|
||||
local prefix = line:sub(1, pw)
|
||||
local stripped = line:sub(pw + 1)
|
||||
local buf_line = hunk.start_line + i - 1
|
||||
local has_add = prefix:find('+', 1, true) ~= nil
|
||||
local has_del = prefix:find('-', 1, true) ~= nil
|
||||
|
||||
if prefix == '+' then
|
||||
if has_add and not has_del then
|
||||
new_map[#new_code] = buf_line
|
||||
table.insert(new_code, stripped)
|
||||
elseif prefix == '-' then
|
||||
elseif has_del and not has_add then
|
||||
old_map[#old_code] = buf_line
|
||||
table.insert(old_code, stripped)
|
||||
else
|
||||
|
|
@ -314,9 +318,9 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
end
|
||||
|
||||
extmark_count =
|
||||
highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines, p)
|
||||
highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, pw, covered_lines, p)
|
||||
extmark_count = extmark_count
|
||||
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines, p)
|
||||
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, pw, covered_lines, p)
|
||||
|
||||
if hunk.header_context and hunk.header_context_col then
|
||||
local header_line = hunk.start_line - 1
|
||||
|
|
@ -344,7 +348,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
---@type string[]
|
||||
local code_lines = {}
|
||||
for _, line in ipairs(hunk.lines) do
|
||||
table.insert(code_lines, line:sub(2))
|
||||
table.insert(code_lines, line:sub(pw + 1))
|
||||
end
|
||||
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, 0, p)
|
||||
end
|
||||
|
|
@ -367,7 +371,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
---@type diffs.IntraChanges?
|
||||
local intra = nil
|
||||
local intra_cfg = opts.highlights.intra
|
||||
if intra_cfg and intra_cfg.enabled and #hunk.lines <= intra_cfg.max_lines then
|
||||
if intra_cfg and intra_cfg.enabled and pw == 1 and #hunk.lines <= intra_cfg.max_lines then
|
||||
dbg('computing intra for hunk %s:%d (%d lines)', hunk.filename, hunk.start_line, #hunk.lines)
|
||||
intra = diff.compute_intra_hunks(hunk.lines, intra_cfg.algorithm)
|
||||
if intra then
|
||||
|
|
@ -401,22 +405,32 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
for i, line in ipairs(hunk.lines) do
|
||||
local buf_line = hunk.start_line + i - 1
|
||||
local line_len = #line
|
||||
local prefix = line:sub(1, 1)
|
||||
local prefix = line:sub(1, pw)
|
||||
local has_add = prefix:find('+', 1, true) ~= nil
|
||||
local has_del = prefix:find('-', 1, true) ~= nil
|
||||
local is_diff_line = has_add or has_del
|
||||
local line_hl = is_diff_line and (has_add and 'DiffsAdd' or 'DiffsDelete') or nil
|
||||
local number_hl = is_diff_line and (has_add and 'DiffsAddNr' or 'DiffsDeleteNr') or nil
|
||||
|
||||
local is_diff_line = prefix == '+' or prefix == '-'
|
||||
local line_hl = is_diff_line and (prefix == '+' and 'DiffsAdd' or 'DiffsDelete') or nil
|
||||
local number_hl = is_diff_line and (prefix == '+' and 'DiffsAddNr' or 'DiffsDeleteNr') or nil
|
||||
local is_marker = false
|
||||
if pw > 1 and line_hl and not prefix:find('[^+]') then
|
||||
local content = line:sub(pw + 1)
|
||||
is_marker = content:match('^<<<<<<<')
|
||||
or content:match('^=======')
|
||||
or content:match('^>>>>>>>')
|
||||
or content:match('^|||||||')
|
||||
end
|
||||
|
||||
if opts.hide_prefix then
|
||||
local virt_hl = (opts.highlights.background and line_hl) or nil
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
||||
virt_text = { { ' ', virt_hl } },
|
||||
virt_text = { { string.rep(' ', pw), virt_hl } },
|
||||
virt_text_pos = 'overlay',
|
||||
})
|
||||
end
|
||||
|
||||
if line_len > 1 and covered_lines[buf_line] then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
|
||||
if line_len > pw and covered_lines[buf_line] then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw, {
|
||||
end_col = line_len,
|
||||
hl_group = 'DiffsClear',
|
||||
priority = p.clear,
|
||||
|
|
@ -438,8 +452,16 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
end
|
||||
end
|
||||
|
||||
if is_marker and line_len > pw then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw, {
|
||||
end_col = line_len,
|
||||
hl_group = 'DiffsConflictMarker',
|
||||
priority = p.char_bg,
|
||||
})
|
||||
end
|
||||
|
||||
if char_spans_by_line[i] then
|
||||
local char_hl = prefix == '+' and 'DiffsAddText' or 'DiffsDeleteText'
|
||||
local char_hl = has_add and 'DiffsAddText' or 'DiffsDeleteText'
|
||||
for _, span in ipairs(char_spans_by_line[i]) do
|
||||
dbg(
|
||||
'char extmark: line=%d buf_line=%d col=%d..%d hl=%s text="%s"',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
---@field file_old_count integer?
|
||||
---@field file_new_start integer?
|
||||
---@field file_new_count integer?
|
||||
---@field prefix_width integer
|
||||
---@field repo_root string?
|
||||
|
||||
local M = {}
|
||||
|
|
@ -133,6 +134,8 @@ function M.parse_buffer(bufnr)
|
|||
local hunk_lines = {}
|
||||
---@type integer?
|
||||
local hunk_count = nil
|
||||
---@type integer
|
||||
local hunk_prefix_width = 1
|
||||
---@type integer?
|
||||
local header_start = nil
|
||||
---@type string[]
|
||||
|
|
@ -156,6 +159,7 @@ function M.parse_buffer(bufnr)
|
|||
header_context = hunk_header_context,
|
||||
header_context_col = hunk_header_context_col,
|
||||
lines = hunk_lines,
|
||||
prefix_width = hunk_prefix_width,
|
||||
file_old_start = file_old_start,
|
||||
file_old_count = file_old_count,
|
||||
file_new_start = file_new_start,
|
||||
|
|
@ -179,7 +183,7 @@ function M.parse_buffer(bufnr)
|
|||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local filename = line:match('^[MADRC%?!]%s+(.+)$') or line:match('^diff %-%-git a/.+ b/(.+)$')
|
||||
local filename = line:match('^[MADRCU%?!]%s+(.+)$') or line:match('^diff %-%-git a/.+ b/(.+)$')
|
||||
if filename then
|
||||
flush_hunk()
|
||||
current_filename = filename
|
||||
|
|
@ -191,22 +195,33 @@ function M.parse_buffer(bufnr)
|
|||
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
|
||||
end
|
||||
hunk_count = 0
|
||||
hunk_prefix_width = 1
|
||||
header_start = i
|
||||
header_lines = {}
|
||||
elseif line:match('^@@.-@@') then
|
||||
elseif line:match('^@@+') then
|
||||
flush_hunk()
|
||||
hunk_start = i
|
||||
local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
||||
if hs then
|
||||
file_old_start = tonumber(hs)
|
||||
file_old_count = tonumber(hc) or 1
|
||||
file_new_start = tonumber(hs2)
|
||||
file_new_count = tonumber(hc2) or 1
|
||||
local at_prefix = line:match('^(@@+)')
|
||||
hunk_prefix_width = #at_prefix - 1
|
||||
if #at_prefix == 2 then
|
||||
local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
||||
if hs then
|
||||
file_old_start = tonumber(hs)
|
||||
file_old_count = tonumber(hc) or 1
|
||||
file_new_start = tonumber(hs2)
|
||||
file_new_count = tonumber(hc2) or 1
|
||||
end
|
||||
else
|
||||
local hs2, hc2 = line:match('%+(%d+),?(%d*) @@')
|
||||
if hs2 then
|
||||
file_new_start = tonumber(hs2)
|
||||
file_new_count = tonumber(hc2) or 1
|
||||
end
|
||||
end
|
||||
local prefix, context = line:match('^(@@.-@@%s*)(.*)')
|
||||
local at_end, context = line:match('^(@@+.-@@+%s*)(.*)')
|
||||
if context and context ~= '' then
|
||||
hunk_header_context = context
|
||||
hunk_header_context_col = #prefix
|
||||
hunk_header_context_col = #at_end
|
||||
end
|
||||
if hunk_count then
|
||||
hunk_count = hunk_count + 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue