refactor(git): rework blame highlights and rename overlay to gutter

This commit is contained in:
2026-05-26 14:52:59 +02:00
parent a0a8d723d6
commit c560f62fb2
5 changed files with 157 additions and 109 deletions
+73 -47
View File
@@ -76,19 +76,45 @@ local function wait_buf_name(pat)
end, "current buffer name to match " .. pat)
end
t.test("relative_time buckets", function()
local now = os.time()
t.eq(blame.relative_time(now), "just now")
t.eq(blame.relative_time(now - 10), "just now")
t.eq(blame.relative_time(now - 60), "a minute ago")
t.eq(blame.relative_time(now - 5 * 60), "5 minutes ago")
t.eq(blame.relative_time(now - 60 * 60), "an hour ago")
t.eq(blame.relative_time(now - 3 * 3600), "3 hours ago")
t.eq(blame.relative_time(now - 26 * 3600), "a day ago")
t.eq(blame.relative_time(now - 3 * 86400), "3 days ago")
t.eq(blame.relative_time(now - 14 * 86400), "2 weeks ago")
t.eq(blame.relative_time(now - 60 * 86400), "2 months ago")
t.eq(blame.relative_time(now - 400 * 86400), "1 year ago")
t.test("inline annotation includes the relative time", function()
local _, buf = setup("alpha\nbeta\ngamma\n")
vim.api.nvim_set_current_buf(buf)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
enable_blame(buf)
t.wait_for(function()
return #inline_marks(buf) == 1
end, "an inline annotation on the current line")
local mark = assert(inline_marks(buf)[1])
local details = assert(mark[4])
local virt_text = assert(details.virt_text)
local chunk = assert(virt_text[1])
t.truthy(
chunk[1]:find("just now", 1, true),
"a fresh commit reads as 'just now' in the annotation"
)
end)
t.test("line popup formats the datetime in the author timezone", function()
local _, buf = setup("alpha\nbeta\n")
vim.api.nvim_set_current_buf(buf)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
blame.line_popup(buf)
local float = wait_float()
t.defer(function()
pcall(vim.api.nvim_win_close, float, true)
end)
local lines = vim.api.nvim_buf_get_lines(
vim.api.nvim_win_get_buf(float),
0,
-1,
false
)
t.truthy(
(lines[1] or ""):match(
"%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d [+%-]%d%d%d%d$"
),
"the head line ends with an ISO datetime and numeric timezone"
)
end)
t.test("blame layout squeezes the author before date and sha", function()
@@ -225,7 +251,7 @@ t.test("blame actions are no-ops off a worktree", function()
t.quietly(function()
blame.line_popup(buf)
blame.toggle_inline(buf)
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
end)
t.eq(blame.state(buf), nil, "no state created for a non-worktree buffer")
end)
@@ -329,20 +355,20 @@ t.test("inline annotation follows the cursor", function()
t.eq(assert(inline_marks(buf)[1])[2], 2, "annotation moved to line 3")
end)
t.test("overlay toggle sets and clears the statuscolumn", function()
t.test("gutter toggle sets and clears the statuscolumn", function()
local _, buf = setup("a\nb\nc\nd\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.truthy(
vim.wo[win].statuscolumn ~= "",
"the overlay sets the window statuscolumn"
"the gutter sets the window statuscolumn"
)
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.eq(vim.wo[win].statuscolumn, "", "toggling off clears it")
end)
t.test("overlay saves and restores the statuscolumn", function()
t.test("gutter saves and restores the statuscolumn", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
@@ -354,21 +380,21 @@ t.test("overlay saves and restores the statuscolumn", function()
end)
vim.wo[win].statuscolumn = "%l custom"
vim.wo[win].signcolumn = "yes:2"
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.truthy(
vim.wo[win].statuscolumn ~= "%l custom",
"the overlay overrides a custom statuscolumn"
"the gutter overrides a custom statuscolumn"
)
t.eq(
vim.wo[win].signcolumn,
"yes:2",
"the overlay leaves signcolumn untouched"
"the gutter leaves signcolumn untouched"
)
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.eq(vim.wo[win].statuscolumn, "%l custom", "statuscolumn restored")
end)
t.test("overlay gutter uses the full preferred width when it can", function()
t.test("gutter uses the full preferred width when it can", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
@@ -381,11 +407,11 @@ t.test("overlay gutter uses the full preferred width when it can", function()
vim.wo[win].relativenumber = false
vim.wo[win].signcolumn = "no"
vim.wo[win].foldcolumn = "0"
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil
end, "the overlay blame to render")
end, "the gutter blame to render")
t.eq(
assert(blame.state(buf)).blame_width,
40,
@@ -393,7 +419,7 @@ t.test("overlay gutter uses the full preferred width when it can", function()
)
end)
t.test("overlay gutter is budgeted under the 47-cell cap", function()
t.test("gutter is budgeted under the 47-cell cap", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
@@ -407,11 +433,11 @@ t.test("overlay gutter is budgeted under the 47-cell cap", function()
vim.wo[win].relativenumber = false
vim.wo[win].foldcolumn = "0"
vim.wo[win].signcolumn = "yes:9"
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil
end, "the overlay blame to render")
end, "the gutter blame to render")
local native = blame._native_width(win)
local width = assert(assert(blame.state(buf)).blame_width)
t.eq(native, 18, "signcolumn=yes:9 reserves an 18-cell sign column")
@@ -419,7 +445,7 @@ t.test("overlay gutter is budgeted under the 47-cell cap", function()
t.truthy(width + native <= 47, "blame plus native columns fits the cap")
end)
t.test("overlay re-budgets when a gutter option changes", function()
t.test("gutter re-budgets when a gutter option changes", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
@@ -433,11 +459,11 @@ t.test("overlay re-budgets when a gutter option changes", function()
vim.wo[win].relativenumber = false
vim.wo[win].foldcolumn = "0"
vim.wo[win].signcolumn = "no"
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil
end, "the overlay blame to render")
end, "the gutter blame to render")
t.eq(
assert(blame.state(buf)).blame_width,
40,
@@ -450,17 +476,17 @@ t.test("overlay re-budgets when a gutter option changes", function()
end, "the blame to re-budget for the widened signcolumn")
end)
t.test("the overlay statuscolumn does not leak into other windows", function()
t.test("the gutter statuscolumn does not leak into other windows", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil
end, "the overlay to render")
end, "the gutter to render")
t.falsy(
vim.go.statuscolumn:find("git.blame", 1, true),
"the overlay leaves the global statuscolumn untouched"
"the gutter leaves the global statuscolumn untouched"
)
vim.cmd("new")
@@ -476,15 +502,15 @@ t.test("the overlay statuscolumn does not leak into other windows", function()
)
end)
t.test("overlay gutter shows sha, author and an absolute date", function()
t.test("gutter shows sha, author and an absolute date", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.tick ~= nil
end, "the overlay blame to populate")
end, "the gutter blame to populate")
local g = blame._gutter(win, 1, 0)
t.truthy(g:match("%x%x%x%x%x%x%x%x"), "the gutter shows a short sha")
t.truthy(g:find("t", 1, true), "the gutter shows the author")
@@ -494,15 +520,15 @@ t.test("overlay gutter shows sha, author and an absolute date", function()
)
end)
t.test("overlay gutter is blank on virtual lines", function()
t.test("gutter is blank on virtual lines", function()
local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.tick ~= nil
end, "the overlay blame to populate")
end, "the gutter blame to populate")
local g = blame._gutter(win, 1, -1)
t.falsy(g:match("%x%x%x%x%x%x%x%x"), "no sha on a virtual line")
end)
@@ -512,11 +538,11 @@ t.test("the statuscolumn expression renders the blame gutter", function()
local sha = h.git(dir, "rev-parse", "HEAD").stdout
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf)
blame.toggle_gutter(buf)
t.wait_for(function()
local s = blame.state(buf)
return s ~= nil and s.tick ~= nil
end, "the overlay blame to populate")
end, "the gutter blame to populate")
local rendered = vim.api.nvim_eval_statusline(
"%{%v:lua.require('git.blame').statuscolumn()%}",
{ winid = win, use_statuscol_lnum = 1 }
@@ -589,10 +615,10 @@ t.test("detach clears blame state and annotations", function()
vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win()
enable_blame(buf)
blame.toggle_overlay(buf)
t.truthy(vim.wo[win].statuscolumn ~= "", "the overlay statuscolumn set")
blame.toggle_gutter(buf)
t.truthy(vim.wo[win].statuscolumn ~= "", "the gutter statuscolumn set")
blame.detach(buf)
t.eq(blame.state(buf), nil, "state dropped on detach")
t.eq(#inline_marks(buf), 0, "inline annotation cleared")
t.eq(vim.wo[win].statuscolumn, "", "overlay statuscolumn cleared")
t.eq(vim.wo[win].statuscolumn, "", "gutter statuscolumn cleared")
end)