From 3bf41c2af3489cd6a15085a0c35e9800c09db5fd Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 15 Mar 2026 12:35:30 -0400 Subject: [PATCH] fix(highlight): use multiline extmark for `hl_eol` with start-position clearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: single-row extmarks (`end_row = buf_line, end_col = len`) do not trigger `hl_eol` — Neovim requires `end_row > start_row`. Line backgrounds stopped at the last character instead of extending to the window edge. Solution: use `end_row = buf_line + 1, end_col = 0` for line bg extmarks. Replace per-hunk `nvim_buf_clear_namespace` with `clear_ns_by_start` that queries extmarks by start position only, so adjacent hunks' trailing `end_row` is never killed. --- lua/diffs/highlight.lua | 5 ++--- lua/diffs/init.lua | 20 ++++++++++++++++-- spec/highlight_spec.lua | 46 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 445354a..806f782 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -636,10 +636,9 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end if opts.highlights.background and is_diff_line then - local bg_end_col = raw_len or (line_len + qw) pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { - end_row = buf_line, - end_col = bg_end_col, + end_row = buf_line + 1, + end_col = 0, hl_group = line_hl, hl_eol = true, priority = p.line_bg, diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index 81d4f97..327318e 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -437,7 +437,10 @@ local function ensure_cache(bufnr) if config.highlights.context.enabled then compute_hunk_context(hunks, config.highlights.context.lines) end - local carried = entry and not entry.pending_clear and carry_forward_highlighted(entry, hunks) + local carried = entry + and not entry.pending_clear + and #entry.hunks == #hunks + and carry_forward_highlighted(entry, hunks) hunk_cache[bufnr] = { hunks = hunks, tick = tick, @@ -514,6 +517,18 @@ local function find_visible_hunks(hunks, toprow, botrow) return first, last end +---@param bufnr integer +---@param ns_id integer +---@param start_row integer +---@param end_row integer +local function clear_ns_by_start(bufnr, ns_id, start_row, end_row) + local marks = + vim.api.nvim_buf_get_extmarks(bufnr, ns_id, { start_row, 0 }, { end_row - 1, 2147483647 }, {}) + for _, m in ipairs(marks) do + vim.api.nvim_buf_del_extmark(bufnr, ns_id, m[1]) + end +end + local function compute_highlight_groups(is_default) local normal = vim.api.nvim_get_hl(0, { name = 'Normal' }) local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) @@ -945,7 +960,7 @@ local function init() if hunk.header_start_line then clear_start = hunk.header_start_line - 1 end - vim.api.nvim_buf_clear_namespace(bufnr, ns, clear_start, clear_end) + clear_ns_by_start(bufnr, ns, clear_start, clear_end) highlight.highlight_hunk(bufnr, ns, hunk, fast_hl_opts) entry.highlighted[i] = true count = count + 1 @@ -1185,6 +1200,7 @@ M._test = { invalidate_cache = invalidate_cache, hunks_eq = hunks_eq, process_pending_clear = process_pending_clear, + clear_ns_by_start = clear_ns_by_start, ft_retry_pending = ft_retry_pending, compute_hunk_context = compute_hunk_context, compute_highlight_groups = compute_highlight_groups, diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 37ae9d5..29856dc 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -397,7 +397,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('line bg extmark survives adjacent clear_namespace starting at next row', function() + it('nvim_buf_clear_namespace kills line bg extmark whose end_row bleeds into cleared range', function() local bufnr = create_buffer({ 'diff --git a/foo.py b/foo.py', '@@ -1,2 +1,2 @@', @@ -423,6 +423,50 @@ describe('highlight', function() local last_body_row = hunk.start_line + #hunk.lines - 1 vim.api.nvim_buf_clear_namespace(bufnr, ns, last_body_row + 1, last_body_row + 10) + local marks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns, + { last_body_row, 0 }, + { last_body_row, -1 }, + { details = true } + ) + local has_line_bg = false + for _, mark in ipairs(marks) do + if mark[4] and mark[4].hl_group == 'DiffsAdd' then + has_line_bg = true + end + end + assert.is_false(has_line_bg) + delete_buffer(bufnr) + end) + + it('clear_ns_by_start preserves line bg extmark whose end_row bleeds past cleared range', function() + local bufnr = create_buffer({ + 'diff --git a/foo.py b/foo.py', + '@@ -1,2 +1,2 @@', + '-old', + '+new', + }) + + local hunk = { + filename = 'foo.py', + header_start_line = 1, + start_line = 2, + lines = { '-old', '+new' }, + prefix_width = 1, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true, treesitter = { enabled = false } } }) + ) + + local last_body_row = hunk.start_line + #hunk.lines - 1 + local clear_ns_by_start = require('diffs')._test.clear_ns_by_start + clear_ns_by_start(bufnr, ns, last_body_row + 1, last_body_row + 10) + local marks = vim.api.nvim_buf_get_extmarks( bufnr, ns,