fix: carry forward highlighted hunks on reparse to reduce flicker
Problem: toggling large diffs via fugitive's `=` caused the top of the
buffer to re-render and glitch. ensure_cache always created a new cache
entry with pending_clear=true and highlighted={}, forcing on_win to
clear and re-highlight every visible hunk — including stable ones above
the toggle point that never changed.
Solution: on reparse, compare old and new hunk lists using a prefix +
suffix matching strategy. Hunks that match (same filename, line count,
and sampled content) carry forward their highlighted state so on_win
skips them. Comparison is O(1) per hunk. Only runs when old entry had
pending_clear=false; invalidate_cache/ColorScheme paths still force
full re-highlight.
Closes #131
This commit is contained in:
parent
d45ffd279b
commit
2ab17a05d8
2 changed files with 201 additions and 2 deletions
|
|
@ -222,6 +222,75 @@ local function invalidate_cache(bufnr)
|
|||
end
|
||||
end
|
||||
|
||||
---@param a diffs.Hunk
|
||||
---@param b diffs.Hunk
|
||||
---@return boolean
|
||||
local function hunks_eq(a, b)
|
||||
local n = #a.lines
|
||||
if n ~= #b.lines or a.filename ~= b.filename then
|
||||
return false
|
||||
end
|
||||
if a.lines[1] ~= b.lines[1] then
|
||||
return false
|
||||
end
|
||||
if n > 1 and a.lines[n] ~= b.lines[n] then
|
||||
return false
|
||||
end
|
||||
if n > 2 then
|
||||
local mid = math.floor(n / 2) + 1
|
||||
if a.lines[mid] ~= b.lines[mid] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param old_entry diffs.HunkCacheEntry
|
||||
---@param new_hunks diffs.Hunk[]
|
||||
---@return table<integer, true>
|
||||
local function carry_forward_highlighted(old_entry, new_hunks)
|
||||
local old_hunks = old_entry.hunks
|
||||
local old_hl = old_entry.highlighted
|
||||
local old_n = #old_hunks
|
||||
local new_n = #new_hunks
|
||||
local highlighted = {}
|
||||
|
||||
local prefix_len = 0
|
||||
local limit = math.min(old_n, new_n)
|
||||
for i = 1, limit do
|
||||
if not hunks_eq(old_hunks[i], new_hunks[i]) then
|
||||
break
|
||||
end
|
||||
if old_hl[i] then
|
||||
highlighted[i] = true
|
||||
end
|
||||
prefix_len = i
|
||||
end
|
||||
|
||||
local suffix_len = 0
|
||||
local max_suffix = limit - prefix_len
|
||||
for j = 0, max_suffix - 1 do
|
||||
local old_idx = old_n - j
|
||||
local new_idx = new_n - j
|
||||
if not hunks_eq(old_hunks[old_idx], new_hunks[new_idx]) then
|
||||
break
|
||||
end
|
||||
if old_hl[old_idx] then
|
||||
highlighted[new_idx] = true
|
||||
end
|
||||
suffix_len = j + 1
|
||||
end
|
||||
|
||||
dbg(
|
||||
'carry_forward: %d prefix + %d suffix of %d old -> %d new hunks',
|
||||
prefix_len,
|
||||
suffix_len,
|
||||
old_n,
|
||||
new_n
|
||||
)
|
||||
return highlighted
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
local function ensure_cache(bufnr)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
|
|
@ -246,11 +315,12 @@ local function ensure_cache(bufnr)
|
|||
local lc = vim.api.nvim_buf_line_count(bufnr)
|
||||
local bc = vim.api.nvim_buf_get_offset(bufnr, lc)
|
||||
dbg('parsed %d hunks in buffer %d (tick %d)', #hunks, bufnr, tick)
|
||||
local carried = entry and not entry.pending_clear and carry_forward_highlighted(entry, hunks)
|
||||
hunk_cache[bufnr] = {
|
||||
hunks = hunks,
|
||||
tick = tick,
|
||||
highlighted = {},
|
||||
pending_clear = true,
|
||||
highlighted = carried or {},
|
||||
pending_clear = not carried,
|
||||
line_count = lc,
|
||||
byte_count = bc,
|
||||
}
|
||||
|
|
@ -862,6 +932,7 @@ M._test = {
|
|||
hunk_cache = hunk_cache,
|
||||
ensure_cache = ensure_cache,
|
||||
invalidate_cache = invalidate_cache,
|
||||
hunks_eq = hunks_eq,
|
||||
}
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -139,6 +139,134 @@ describe('decoration_provider', function()
|
|||
end)
|
||||
end)
|
||||
|
||||
describe('hunk stability', function()
|
||||
it('carries forward highlighted for stable hunks on section expansion', function()
|
||||
local bufnr = create_buffer({
|
||||
'M test.lua',
|
||||
'@@ -1,2 +1,2 @@',
|
||||
' local x = 1',
|
||||
'-local y = 2',
|
||||
'+local y = 3',
|
||||
'@@ -10,2 +10,3 @@',
|
||||
' function M.foo()',
|
||||
'+ return true',
|
||||
' end',
|
||||
})
|
||||
diffs.attach(bufnr)
|
||||
local entry = diffs._test.hunk_cache[bufnr]
|
||||
assert.are.equal(2, #entry.hunks)
|
||||
|
||||
entry.pending_clear = false
|
||||
entry.highlighted = { [1] = true, [2] = true }
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 5, 5, false, {
|
||||
'@@ -5,1 +5,2 @@',
|
||||
' local z = 4',
|
||||
'+local w = 5',
|
||||
})
|
||||
diffs._test.ensure_cache(bufnr)
|
||||
|
||||
local updated = diffs._test.hunk_cache[bufnr]
|
||||
assert.are.equal(3, #updated.hunks)
|
||||
assert.is_true(updated.highlighted[1] == true)
|
||||
assert.is_nil(updated.highlighted[2])
|
||||
assert.is_true(updated.highlighted[3] == true)
|
||||
assert.is_false(updated.pending_clear)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('carries forward highlighted for stable hunks on section collapse', function()
|
||||
local bufnr = create_buffer({
|
||||
'M test.lua',
|
||||
'@@ -1,2 +1,2 @@',
|
||||
' local x = 1',
|
||||
'-local y = 2',
|
||||
'+local y = 3',
|
||||
'@@ -5,1 +5,2 @@',
|
||||
' local z = 4',
|
||||
'+local w = 5',
|
||||
'@@ -10,2 +10,3 @@',
|
||||
' function M.foo()',
|
||||
'+ return true',
|
||||
' end',
|
||||
})
|
||||
diffs.attach(bufnr)
|
||||
local entry = diffs._test.hunk_cache[bufnr]
|
||||
assert.are.equal(3, #entry.hunks)
|
||||
|
||||
entry.pending_clear = false
|
||||
entry.highlighted = { [1] = true, [2] = true, [3] = true }
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 5, 8, false, {})
|
||||
diffs._test.ensure_cache(bufnr)
|
||||
|
||||
local updated = diffs._test.hunk_cache[bufnr]
|
||||
assert.are.equal(2, #updated.hunks)
|
||||
assert.is_true(updated.highlighted[1] == true)
|
||||
assert.is_true(updated.highlighted[2] == true)
|
||||
assert.is_false(updated.pending_clear)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('bypasses carry-forward when pending_clear was true', function()
|
||||
local bufnr = create_buffer({
|
||||
'M test.lua',
|
||||
'@@ -1,2 +1,2 @@',
|
||||
' local x = 1',
|
||||
'-local y = 2',
|
||||
'+local y = 3',
|
||||
'@@ -10,2 +10,3 @@',
|
||||
' function M.foo()',
|
||||
'+ return true',
|
||||
' end',
|
||||
})
|
||||
diffs.attach(bufnr)
|
||||
local entry = diffs._test.hunk_cache[bufnr]
|
||||
entry.highlighted = { [1] = true, [2] = true }
|
||||
entry.pending_clear = true
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 5, 5, false, {
|
||||
'@@ -5,1 +5,2 @@',
|
||||
' local z = 4',
|
||||
'+local w = 5',
|
||||
})
|
||||
diffs._test.ensure_cache(bufnr)
|
||||
|
||||
local updated = diffs._test.hunk_cache[bufnr]
|
||||
assert.are.same({}, updated.highlighted)
|
||||
assert.is_true(updated.pending_clear)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does not carry forward when all hunks changed', function()
|
||||
local bufnr = create_buffer({
|
||||
'M test.lua',
|
||||
'@@ -1,2 +1,2 @@',
|
||||
' local x = 1',
|
||||
'-local y = 2',
|
||||
'+local y = 3',
|
||||
})
|
||||
diffs.attach(bufnr)
|
||||
local entry = diffs._test.hunk_cache[bufnr]
|
||||
|
||||
entry.pending_clear = false
|
||||
entry.highlighted = { [1] = true }
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||
'M other.lua',
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local a = 1',
|
||||
'+local b = 2',
|
||||
})
|
||||
diffs._test.ensure_cache(bufnr)
|
||||
|
||||
local updated = diffs._test.hunk_cache[bufnr]
|
||||
assert.is_nil(updated.highlighted[1])
|
||||
assert.is_false(updated.pending_clear)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('multiple hunks in cache', function()
|
||||
it('stores all parsed hunks for a multi-hunk buffer', function()
|
||||
local bufnr = create_buffer({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue