diff --git a/lua/diffs/debug.lua b/lua/diffs/debug.lua new file mode 100644 index 0000000..5be95bc --- /dev/null +++ b/lua/diffs/debug.lua @@ -0,0 +1,68 @@ +local M = {} + +local ns = vim.api.nvim_create_namespace('diffs') + +function M.dump() + local bufnr = vim.api.nvim_get_current_buf() + local marks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true }) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + local by_line = {} + for _, mark in ipairs(marks) do + local id, row, col, details = mark[1], mark[2], mark[3], mark[4] + local entry = { + id = id, + row = row, + col = col, + end_row = details.end_row, + end_col = details.end_col, + hl_group = details.hl_group, + priority = details.priority, + line_hl_group = details.line_hl_group, + number_hl_group = details.number_hl_group, + virt_text = details.virt_text, + } + if not by_line[row] then + by_line[row] = { text = lines[row + 1] or '', marks = {} } + end + table.insert(by_line[row].marks, entry) + end + + local all_ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true }) + local non_diffs = {} + for _, mark in ipairs(all_ns_marks) do + local details = mark[4] + if details.ns_id ~= ns then + table.insert(non_diffs, { + ns_id = details.ns_id, + row = mark[2], + col = mark[3], + end_row = details.end_row, + end_col = details.end_col, + hl_group = details.hl_group, + priority = details.priority, + }) + end + end + + local result = { + bufnr = bufnr, + buf_name = vim.api.nvim_buf_get_name(bufnr), + ns_id = ns, + total_diffs_marks = #marks, + total_all_marks = #all_ns_marks, + non_diffs_marks = non_diffs, + lines = by_line, + } + + local state_dir = vim.fn.stdpath('state') + local path = state_dir .. '/diffs_debug.json' + local f = io.open(path, 'w') + if f then + f:write(vim.json.encode(result)) + f:close() + vim.notify('[diffs.nvim] debug dump: ' .. path, vim.log.levels.INFO) + end +end + +return M diff --git a/lua/diffs/diff.lua b/lua/diffs/diff.lua index 8dc836b..65d3ac8 100644 --- a/lua/diffs/diff.lua +++ b/lua/diffs/diff.lua @@ -312,13 +312,28 @@ function M.compute_intra_hunks(hunk_lines, algorithm) ---@type diffs.CharSpan[] local all_del = {} - for _, group in ipairs(groups) do + dbg( + 'intra: %d change groups, algorithm=%s, vscode=%s', + #groups, + algorithm, + vscode_handle and 'yes' or 'no' + ) + + for gi, group in ipairs(groups) do + dbg('group %d: %d del lines, %d add lines', gi, #group.del_lines, #group.add_lines) local ds, as if vscode_handle then ds, as = diff_group_vscode(group, vscode_handle) else ds, as = diff_group_native(group) end + dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as) + for _, s in ipairs(ds) do + dbg(' del span: line=%d col=%d..%d', s.line, s.col_start, s.col_end) + end + for _, s in ipairs(as) do + dbg(' add span: line=%d col=%d..%d', s.line, s.col_start, s.col_end) + end vim.list_extend(all_del, ds) vim.list_extend(all_add, as) end diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 1eb16d7..311a6f3 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -287,7 +287,17 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local intra = nil local intra_cfg = opts.highlights.intra if intra_cfg and intra_cfg.enabled 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 + dbg('intra result: %d add spans, %d del spans', #intra.add_spans, #intra.del_spans) + else + dbg('intra result: nil (no change groups)') + end + elseif intra_cfg and not intra_cfg.enabled then + dbg('intra disabled by config') + elseif intra_cfg and #hunk.lines > intra_cfg.max_lines then + dbg('intra skipped: %d lines > %d max', #hunk.lines, intra_cfg.max_lines) end ---@type table @@ -324,21 +334,20 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) }) end - if opts.highlights.background and is_diff_line then - local extmark_opts = { - line_hl_group = line_hl, - priority = 198, - } - if opts.highlights.gutter then - extmark_opts.number_hl_group = number_hl - end - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, extmark_opts) - end - if line_len > 1 and syntax_applied then pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, { end_col = line_len, hl_group = 'Normal', + priority = 198, + }) + end + + if opts.highlights.background and is_diff_line then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { + end_col = line_len, + hl_group = line_hl, + hl_eol = true, + number_hl_group = opts.highlights.gutter and number_hl or nil, priority = 199, }) end @@ -346,11 +355,23 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) if char_spans_by_line[i] then local char_hl = prefix == '+' and 'DiffsAddText' or 'DiffsDeleteText' for _, span in ipairs(char_spans_by_line[i]) do - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, { + dbg( + 'char extmark: line=%d buf_line=%d col=%d..%d hl=%s text="%s"', + i, + buf_line, + span.col_start, + span.col_end, + char_hl, + line:sub(span.col_start + 1, span.col_end) + ) + local ok, err = pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, { end_col = span.col_end, hl_group = char_hl, priority = 201, }) + if not ok then + dbg('char extmark FAILED: %s', err) + end extmark_count = extmark_count + 1 end end diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index d2097b1..d80cd2e 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -183,8 +183,8 @@ local function compute_highlight_groups() local blended_add = blend_color(add_bg, bg, 0.4) local blended_del = blend_color(del_bg, bg, 0.4) - local blended_add_text = blend_color(add_fg, bg, 0.4) - local blended_del_text = blend_color(del_fg, bg, 0.4) + local blended_add_text = blend_color(add_fg, bg, 0.7) + local blended_del_text = blend_color(del_fg, bg, 0.7) vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add }) vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del }) @@ -193,6 +193,15 @@ local function compute_highlight_groups() vim.api.nvim_set_hl(0, 'DiffsAddText', { default = true, bg = blended_add_text }) vim.api.nvim_set_hl(0, 'DiffsDeleteText', { default = true, bg = blended_del_text }) + dbg('highlight groups: Normal.bg=#%06x DiffAdd.bg=#%06x diffAdded.fg=#%06x', bg, add_bg, add_fg) + dbg( + 'DiffsAdd.bg=#%06x DiffsAddText.bg=#%06x DiffsAddNr.fg=#%06x', + blended_add, + blended_add_text, + add_fg + ) + dbg('DiffsDelete.bg=#%06x DiffsDeleteText.bg=#%06x', blended_del, blended_del_text) + local diff_change = resolve_hl('DiffChange') local diff_text = resolve_hl('DiffText') diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index e870177..9b7e685 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -43,6 +43,11 @@ describe('highlight', function() enabled = false, max_lines = 200, }, + intra = { + enabled = false, + algorithm = 'native', + max_lines = 500, + }, }, } if overrides then @@ -322,7 +327,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then + if mark[4] and mark[4].hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -355,7 +360,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_delete = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then + if mark[4] and mark[4].hl_group == 'DiffsDelete' then has_diff_delete = true break end @@ -388,7 +393,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_line_hl = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group then + if mark[4] and (mark[4].hl_group == 'DiffsAdd' or mark[4].hl_group == 'DiffsDelete') then has_line_hl = true break end @@ -520,7 +525,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then + if mark[4] and mark[4].hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -668,7 +673,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then + if mark[4] and mark[4].hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -727,6 +732,295 @@ describe('highlight', function() assert.is_true(has_normal) delete_buffer(bufnr) end) + + it('uses hl_group not line_hl_group for line backgrounds', function() + local bufnr = create_buffer({ + '@@ -1,2 +1,1 @@', + '-local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then + assert.is_true(d.hl_eol == true) + assert.is_nil(d.line_hl_group) + end + end + delete_buffer(bufnr) + end) + + it('line bg priority > Normal priority', function() + local bufnr = create_buffer({ + '@@ -1,2 +1,1 @@', + '-local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + local normal_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 + 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(line_bg_priority) + assert.is_true(line_bg_priority > normal_priority) + delete_buffer(bufnr) + end) + + it('char-level extmarks have higher priority than line bg', function() + vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) + vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) + + local bufnr = create_buffer({ + '@@ -1,2 +1,2 @@', + '-local x = 1', + '+local x = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local x = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ + highlights = { + background = true, + intra = { enabled = true, algorithm = 'native', max_lines = 500 }, + }, + }) + ) + + local extmarks = get_extmarks(bufnr) + local line_bg_priority = nil + local char_bg_priority = nil + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then + line_bg_priority = d.priority + end + if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then + char_bg_priority = d.priority + end + end + assert.is_not_nil(line_bg_priority) + assert.is_not_nil(char_bg_priority) + assert.is_true(char_bg_priority > line_bg_priority) + delete_buffer(bufnr) + end) + + it('creates char-level extmarks for changed characters', function() + vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) + vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) + + local bufnr = create_buffer({ + '@@ -1,2 +1,2 @@', + '-local x = 1', + '+local x = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local x = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ + highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, + }) + ) + + local extmarks = get_extmarks(bufnr) + local add_text_marks = {} + local del_text_marks = {} + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and d.hl_group == 'DiffsAddText' then + table.insert(add_text_marks, mark) + end + if d and d.hl_group == 'DiffsDeleteText' then + table.insert(del_text_marks, mark) + end + end + assert.is_true(#add_text_marks > 0) + assert.is_true(#del_text_marks > 0) + delete_buffer(bufnr) + end) + + it('does not create char-level extmarks when intra disabled', function() + local bufnr = create_buffer({ + '@@ -1,2 +1,2 @@', + '-local x = 1', + '+local x = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local x = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ + highlights = { intra = { enabled = false, algorithm = 'native', max_lines = 500 } }, + }) + ) + + local extmarks = get_extmarks(bufnr) + for _, mark in ipairs(extmarks) do + local d = mark[4] + assert.is_not_equal('DiffsAddText', d and d.hl_group) + assert.is_not_equal('DiffsDeleteText', d and d.hl_group) + end + delete_buffer(bufnr) + end) + + it('does not create char-level extmarks for pure additions', function() + vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) + + local bufnr = create_buffer({ + '@@ -1,0 +1,2 @@', + '+local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '+local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ + highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, + }) + ) + + local extmarks = get_extmarks(bufnr) + for _, mark in ipairs(extmarks) do + local d = mark[4] + assert.is_not_equal('DiffsAddText', d and d.hl_group) + assert.is_not_equal('DiffsDeleteText', d and d.hl_group) + end + delete_buffer(bufnr) + end) + + it('enforces priority order: Normal < line bg < syntax < char bg', function() + vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) + vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) + + local bufnr = create_buffer({ + '@@ -1,2 +1,2 @@', + '-local x = 1', + '+local x = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { '-local x = 1', '+local x = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ + highlights = { + background = true, + intra = { enabled = true, algorithm = 'native', max_lines = 500 }, + }, + }) + ) + + local extmarks = get_extmarks(bufnr) + local priorities = { normal = {}, 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) + 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 + table.insert(priorities.char_bg, d.priority) + elseif d.hl_group and d.hl_group:match('^@.*%.lua$') then + table.insert(priorities.syntax, d.priority) + end + end + end + + assert.is_true(#priorities.normal > 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 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) + delete_buffer(bufnr) + end) end) describe('diff header highlighting', function()