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
+2 -2
View File
@@ -235,11 +235,11 @@ vim.keymap.set("n", "<leader>gc", "<Plug>(git-commit)")
vim.keymap.set("n", "<leader>ga", "<Plug>(git-commit-amend)") vim.keymap.set("n", "<leader>ga", "<Plug>(git-commit-amend)")
vim.keymap.set("n", "<leader>gl", "<Plug>(git-log)") vim.keymap.set("n", "<leader>gl", "<Plug>(git-log)")
vim.keymap.set("n", "<leader>gb", "<Plug>(git-blame-popup)") vim.keymap.set("n", "<leader>gb", "<Plug>(git-blame-popup)")
vim.keymap.set("n", "<leader>gB", "<Plug>(git-blame)") vim.keymap.set("n", "<leader>gB", "<Plug>(git-blame-gutter)")
vim.keymap.set("n", "<leader>gv", "<Plug>(git-hunk-select)") vim.keymap.set("n", "<leader>gv", "<Plug>(git-hunk-select)")
vim.keymap.set("n", "<leader>gs", "<Plug>(git-hunk-stage-toggle)") vim.keymap.set("n", "<leader>gs", "<Plug>(git-hunk-stage-toggle)")
vim.keymap.set("n", "<leader>gr", "<Plug>(git-hunk-reset)") vim.keymap.set("n", "<leader>gr", "<Plug>(git-hunk-reset)")
vim.keymap.set("n", "<C-w>g", "<Plug>(git-hunk-preview)") vim.keymap.set("n", "<C-w>g", "<Plug>(git-hunk-preview)")
vim.keymap.set("n", "<leader>go", "<Plug>(git-diff-overlay)") vim.keymap.set("n", "<leader>go", "<Plug>(git-hunk-overlay-toggle)")
vim.keymap.set({ "n", "x" }, "]g", "<Plug>(git-hunk-next)") vim.keymap.set({ "n", "x" }, "]g", "<Plug>(git-hunk-next)")
vim.keymap.set({ "n", "x" }, "[g", "<Plug>(git-hunk-prev)") vim.keymap.set({ "n", "x" }, "[g", "<Plug>(git-hunk-prev)")
+70 -52
View File
@@ -41,14 +41,14 @@ local PREFERRED_WIDTH = SHA_WIDTH + AUTHOR_MAX + DATE_WIDTH + 3 * #GAP
---@field revision string? nil = working tree, else the blamed revision ---@field revision string? nil = working tree, else the blamed revision
---@field commits table<string, ow.Git.Blame.Commit> ---@field commits table<string, ow.Git.Blame.Commit>
---@field line_sha table<integer, string> ---@field line_sha table<integer, string>
---@field blame_text table<integer, string>? cached overlay gutter text ---@field blame_text table<integer, string>?
---@field blame_width integer? display width of each cached segment ---@field blame_width integer?
---@field blame_blank string? a blank segment of that width ---@field blame_blank string?
---@field tick integer? ---@field tick integer?
---@field epoch integer ---@field epoch integer
---@field pending fun()[] ---@field pending fun()[]
---@field inline boolean ---@field inline boolean
---@field overlay boolean ---@field gutter boolean
---@field autocmds integer[] ---@field autocmds integer[]
---@type table<integer, ow.Git.Blame.BufState> ---@type table<integer, ow.Git.Blame.BufState>
@@ -66,13 +66,6 @@ function M.state(buf)
return states[buf] return states[buf]
end end
---@param n integer
---@param unit string
---@return string
local function plural(n, unit)
return string.format("%d %s%s ago", n, unit, n == 1 and "" or "s")
end
---@param unix_ts integer ---@param unix_ts integer
---@return string ---@return string
local function relative_time(unix_ts) local function relative_time(unix_ts)
@@ -80,34 +73,47 @@ local function relative_time(unix_ts)
if diff < 0 then if diff < 0 then
diff = 0 diff = 0
end end
local function fmt(n, unit)
return string.format("%d %s%s ago", n, unit, n == 1 and "" or "s")
end
if diff < 45 then if diff < 45 then
return "just now" return "just now"
elseif diff < 90 then elseif diff < 90 then
return "a minute ago" return "a minute ago"
elseif diff < 45 * 60 then elseif diff < 45 * 60 then
return plural(math.floor(diff / 60 + 0.5), "minute") return fmt(math.floor(diff / 60 + 0.5), "minute")
elseif diff < 90 * 60 then elseif diff < 90 * 60 then
return "an hour ago" return "an hour ago"
elseif diff < 22 * 3600 then elseif diff < 22 * 3600 then
return plural(math.floor(diff / 3600 + 0.5), "hour") return fmt(math.floor(diff / 3600 + 0.5), "hour")
elseif diff < 36 * 3600 then elseif diff < 36 * 3600 then
return "a day ago" return "a day ago"
elseif diff < 7 * 86400 then elseif diff < 7 * 86400 then
return plural(math.floor(diff / 86400 + 0.5), "day") return fmt(math.floor(diff / 86400 + 0.5), "day")
elseif diff < 30 * 86400 then elseif diff < 30 * 86400 then
return plural(math.floor(diff / (7 * 86400) + 0.5), "week") return fmt(math.floor(diff / (7 * 86400) + 0.5), "week")
elseif diff < 365 * 86400 then elseif diff < 365 * 86400 then
return plural(math.floor(diff / (30 * 86400) + 0.5), "month") return fmt(math.floor(diff / (30 * 86400) + 0.5), "month")
end end
return plural(math.floor(diff / (365 * 86400) + 0.5), "year")
return fmt(math.floor(diff / (365 * 86400) + 0.5), "year")
end end
M.relative_time = relative_time
---@param ts integer ---@param ts integer
---@param tz string
---@return string ---@return string
local function format_date(ts) local function format_author_time(ts, tz)
return os.date("%Y-%m-%d", ts) --[[@as string]] local sign, hh, mm = tz:match("^([+-])(%d%d)(%d%d)$")
---@type number
local offset = 0
if sign then
local h = tonumber(hh) or 0
local m = tonumber(mm) or 0
offset = (h * 3600 + m * 60) * (sign == "-" and -1 or 1)
end
return os.date("!%Y-%m-%d %T ", ts + offset) .. tz
end end
---@param s string ---@param s string
@@ -285,7 +291,7 @@ local function ensure_state(buf)
epoch = 0, epoch = 0,
pending = {}, pending = {},
inline = false, inline = false,
overlay = false, gutter = false,
autocmds = {}, autocmds = {},
} }
states[buf] = state states[buf] = state
@@ -378,7 +384,7 @@ local function render_inline(buf)
) )
end end
pcall(vim.api.nvim_buf_set_extmark, buf, NS_INLINE, lnum - 1, 0, { pcall(vim.api.nvim_buf_set_extmark, buf, NS_INLINE, lnum - 1, 0, {
virt_text = { { text, "GitBlame" } }, virt_text = { { text, "GitBlameInline" } },
virt_text_pos = "eol", virt_text_pos = "eol",
hl_mode = "combine", hl_mode = "combine",
}) })
@@ -406,7 +412,7 @@ end
---The maximum width the native fold / sign / number columns can occupy. ---The maximum width the native fold / sign / number columns can occupy.
---Computed from the window options, not evaluated: evaluating a ---Computed from the window options, not evaluated: evaluating a
---statuscolumn reports the window's current gutter width, which the ---statuscolumn reports the window's current gutter width, which the
---overlay itself has already inflated. ---gutter itself has already inflated.
---@param win integer ---@param win integer
---@return integer ---@return integer
local function native_width(win) local function native_width(win)
@@ -472,13 +478,14 @@ local function build_blame_text(state, win)
author, date = "Uncommitted", "" author, date = "Uncommitted", ""
else else
author = commit.author author = commit.author
date = format_date(commit.author_time) date = os.date("%Y-%m-%d", commit.author_time)
end end
text[lnum] = "%#GitBlameSha#" text[lnum] = "%#GitBlameSha#"
.. sha:sub(1, sha_w) .. sha:sub(1, sha_w)
.. "%#GitBlame#" .. "%#GitBlameAuthor#"
.. GAP .. GAP
.. (pad(author, author_w):gsub("%%", "%%%%")) .. (pad(author, author_w):gsub("%%", "%%%%"))
.. "%#GitBlameDate#"
.. GAP .. GAP
.. pad(date, date_w) .. pad(date, date_w)
.. GAP .. GAP
@@ -491,7 +498,7 @@ local function build_blame_text(state, win)
end end
---Render the blame segment for one screen line. Wired into the window's ---Render the blame segment for one screen line. Wired into the window's
---`'statuscolumn'` while the overlay is on, so the cursor never enters ---`'statuscolumn'` while the gutter is on, so the cursor never enters
---it - it lives outside the text area, unlike inline virtual text. ---it - it lives outside the text area, unlike inline virtual text.
---@param win integer? ---@param win integer?
---@param lnum integer ---@param lnum integer
@@ -502,7 +509,7 @@ local function gutter(win, lnum, virtnum)
return "" return ""
end end
local state = states[vim.api.nvim_win_get_buf(win)] local state = states[vim.api.nvim_win_get_buf(win)]
if not state or not state.overlay or not state.blame_text then if not state or not state.gutter or not state.blame_text then
return "" return ""
end end
return (virtnum == 0 and state.blame_text[lnum]) or state.blame_blank or "" return (virtnum == 0 and state.blame_text[lnum]) or state.blame_blank or ""
@@ -533,10 +540,10 @@ local function set_statuscolumn(win, value)
) )
end end
---Reconcile every window's `'statuscolumn'` with the overlay state. ---Reconcile every window's `'statuscolumn'` with the gutter state.
---Overlay windows get the blame statuscolumn, and a window that has it ---Gutter windows get the blame statuscolumn, and a window that has it
---but should not (a split inherits window options) is restored. ---but should not (a split inherits window options) is restored.
local function refresh_overlay_columns() local function refresh_gutter_columns()
for win in pairs(saved_statuscolumn) do for win in pairs(saved_statuscolumn) do
if not vim.api.nvim_win_is_valid(win) then if not vim.api.nvim_win_is_valid(win) then
saved_statuscolumn[win] = nil saved_statuscolumn[win] = nil
@@ -544,7 +551,7 @@ local function refresh_overlay_columns()
end end
for _, win in ipairs(vim.api.nvim_list_wins()) do for _, win in ipairs(vim.api.nvim_list_wins()) do
local state = states[vim.api.nvim_win_get_buf(win)] local state = states[vim.api.nvim_win_get_buf(win)]
local on = state ~= nil and state.overlay local on = state ~= nil and state.gutter
local has_blame = vim.startswith(vim.wo[win].statuscolumn, BLAME_EXPR) local has_blame = vim.startswith(vim.wo[win].statuscolumn, BLAME_EXPR)
if on and not has_blame then if on and not has_blame then
saved_statuscolumn[win] = saved_statuscolumn[win] saved_statuscolumn[win] = saved_statuscolumn[win]
@@ -564,7 +571,7 @@ end
local function render(buf) local function render(buf)
render_inline(buf) render_inline(buf)
local state = states[buf] local state = states[buf]
if not state or not state.overlay then if not state or not state.gutter then
return return
end end
-- Rebuild against the current native column widths, then re-set -- Rebuild against the current native column widths, then re-set
@@ -585,7 +592,7 @@ end
---@param buf integer ---@param buf integer
local function reblame(buf) local function reblame(buf)
local state = states[buf] local state = states[buf]
if not state or (not state.inline and not state.overlay) then if not state or (not state.inline and not state.gutter) then
return return
end end
run_blame(state, buf, function() run_blame(state, buf, function()
@@ -658,23 +665,23 @@ function M.toggle_inline(buf)
if vim.api.nvim_buf_is_valid(buf) then if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_clear_namespace(buf, NS_INLINE, 0, -1) vim.api.nvim_buf_clear_namespace(buf, NS_INLINE, 0, -1)
end end
if not state.overlay then if not state.gutter then
detach_autocmds(buf, state) detach_autocmds(buf, state)
end end
end end
end end
---@param buf integer? ---@param buf integer?
function M.toggle_overlay(buf) function M.toggle_gutter(buf)
buf = resolve_buf(buf) buf = resolve_buf(buf)
local state = ensure_state(buf) local state = ensure_state(buf)
if not state then if not state then
util.warning("git blame: nothing to blame in this buffer") util.warning("git blame: nothing to blame in this buffer")
return return
end end
state.overlay = not state.overlay state.gutter = not state.gutter
refresh_overlay_columns() refresh_gutter_columns()
if state.overlay then if state.gutter then
attach_autocmds(buf, state) attach_autocmds(buf, state)
run_blame(state, buf, function() run_blame(state, buf, function()
render(buf) render(buf)
@@ -714,7 +721,8 @@ end
---@param head string[] ---@param head string[]
---@param body string[]? ---@param body string[]?
---@param sha_len integer? ---@param sha_len integer?
local function apply_popup(pbuf, win, head, body, sha_len) ---@param date_col integer?
local function apply_popup(pbuf, win, head, body, sha_len, date_col)
local lines = {} local lines = {}
vim.list_extend(lines, head) vim.list_extend(lines, head)
if body then if body then
@@ -729,9 +737,17 @@ local function apply_popup(pbuf, win, head, body, sha_len)
end_col = sha_len, end_col = sha_len,
hl_group = "GitBlameSha", hl_group = "GitBlameSha",
}) })
pcall(vim.api.nvim_buf_set_extmark, pbuf, NS_POPUP, 1, 0, { end
end_col = #(head[2] or ""), if sha_len and date_col then
hl_group = "GitBlame", pcall(vim.api.nvim_buf_set_extmark, pbuf, NS_POPUP, 0, sha_len + 2, {
end_col = date_col - 2,
hl_group = "GitBlameAuthor",
})
end
if date_col then
pcall(vim.api.nvim_buf_set_extmark, pbuf, NS_POPUP, 0, date_col, {
end_col = #(head[1] or ""),
hl_group = "GitBlameDate",
}) })
end end
local width, height = size_for(lines) local width, height = size_for(lines)
@@ -780,14 +796,16 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
end end
local head ---@type string[] local head ---@type string[]
local sha_len ---@type integer? local sha_len ---@type integer?
local date_col ---@type integer?
if util.is_zero_sha(sha) then if util.is_zero_sha(sha) then
head = { "Not Committed Yet" } head = { "Not Committed Yet" }
else else
local short = sha:sub(1, 8) local short = sha:sub(1, 8)
local date = format_author_time(commit.author_time, commit.author_tz)
sha_len = #short sha_len = #short
date_col = sha_len + 2 + #commit.author + 2
head = { head = {
short .. " " .. commit.author, short .. " " .. commit.author .. " " .. date,
commit.author_mail .. " " .. relative_time(commit.author_time),
"", "",
} }
end end
@@ -809,7 +827,7 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
style = "minimal", style = "minimal",
}) })
popup_win = win popup_win = win
apply_popup(pbuf, win, head, body, sha_len) apply_popup(pbuf, win, head, body, sha_len, date_col)
setup_popup_autocmds(watch_buf, pbuf, win) setup_popup_autocmds(watch_buf, pbuf, win)
if not sha_len then if not sha_len then
return return
@@ -828,7 +846,7 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
end end
local msg = util.split_lines(res.stdout or "") local msg = util.split_lines(res.stdout or "")
if #msg > 0 then if #msg > 0 then
apply_popup(pbuf, win, head, msg, sha_len) apply_popup(pbuf, win, head, msg, sha_len, date_col)
end end
end, end,
}) })
@@ -919,17 +937,17 @@ function M.detach(buf)
detach_autocmds(buf, state) detach_autocmds(buf, state)
state.epoch = state.epoch + 1 state.epoch = state.epoch + 1
states[buf] = nil states[buf] = nil
refresh_overlay_columns() refresh_gutter_columns()
end end
local augroup = vim.api.nvim_create_augroup("ow.git.blame", { clear = true }) local augroup = vim.api.nvim_create_augroup("ow.git.blame", { clear = true })
vim.api.nvim_create_autocmd("BufWinEnter", { vim.api.nvim_create_autocmd("BufWinEnter", {
group = augroup, group = augroup,
callback = refresh_overlay_columns, callback = refresh_gutter_columns,
}) })
-- The blame budget depends on the gutter option widths, so re-render an -- The blame budget depends on the gutter option widths, so re-render an
-- overlay buffer when one of them changes. -- gutter buffer when one of them changes.
vim.api.nvim_create_autocmd("OptionSet", { vim.api.nvim_create_autocmd("OptionSet", {
group = augroup, group = augroup,
pattern = { pattern = {
@@ -942,7 +960,7 @@ vim.api.nvim_create_autocmd("OptionSet", {
callback = function() callback = function()
local buf = vim.api.nvim_get_current_buf() local buf = vim.api.nvim_get_current_buf()
local state = states[buf] local state = states[buf]
if state and state.overlay then if state and state.gutter then
render(buf) render(buf)
end end
end, end,
@@ -960,7 +978,7 @@ repo.on("change", function(r, change)
and (change.paths[state.rel] or change.branch_changed) and (change.paths[state.rel] or change.branch_changed)
then then
state.tick = nil state.tick = nil
if state.inline or state.overlay then if state.inline or state.gutter then
schedule(buf) schedule(buf)
end end
end end
+10 -6
View File
@@ -4,6 +4,8 @@ end
vim.g.loaded_git = 1 vim.g.loaded_git = 1
local DEFAULT_HIGHLIGHTS = { local DEFAULT_HIGHLIGHTS = {
GitAuthor = "String",
GitDate = "Number",
GitIgnored = "Comment", GitIgnored = "Comment",
GitSha = "Identifier", GitSha = "Identifier",
GitStaged = "Constant", GitStaged = "Constant",
@@ -41,7 +43,9 @@ local DEFAULT_HIGHLIGHTS = {
GitHunkAddLine = "DiffAdd", GitHunkAddLine = "DiffAdd",
GitHunkDeleteLine = "DiffDelete", GitHunkDeleteLine = "DiffDelete",
GitBlame = "Comment", GitBlameAuthor = "GitAuthor",
GitBlameDate = "GitDate",
GitBlameInline = "Comment",
GitBlameSha = "GitSha", GitBlameSha = "GitSha",
} }
local STAGED_HUNK_HL = { local STAGED_HUNK_HL = {
@@ -318,9 +322,9 @@ vim.api.nvim_create_user_command("GitDiffOverlay", function()
require("git.hunks").toggle_overlay() require("git.hunks").toggle_overlay()
end, { desc = "Toggle the git diff overlay in the current buffer" }) end, { desc = "Toggle the git diff overlay in the current buffer" })
vim.keymap.set("n", "<Plug>(git-blame)", function() vim.keymap.set("n", "<Plug>(git-blame-gutter)", function()
require("git.blame").toggle_overlay() require("git.blame").toggle_gutter()
end, { silent = true, desc = "Toggle the full-file git blame overlay" }) end, { silent = true, desc = "Toggle the full-file git blame gutter" })
vim.keymap.set("n", "<Plug>(git-blame-line)", function() vim.keymap.set("n", "<Plug>(git-blame-line)", function()
require("git.blame").toggle_inline() require("git.blame").toggle_inline()
end, { silent = true, desc = "Toggle inline git blame" }) end, { silent = true, desc = "Toggle inline git blame" })
@@ -341,8 +345,8 @@ end, {
}) })
vim.api.nvim_create_user_command("GitBlame", function() vim.api.nvim_create_user_command("GitBlame", function()
require("git.blame").toggle_overlay() require("git.blame").toggle_gutter()
end, { desc = "Toggle the full-file git blame overlay in the current buffer" }) end, { desc = "Toggle the full-file git blame gutter in the current buffer" })
vim.api.nvim_create_user_command("GitBlameLine", function() vim.api.nvim_create_user_command("GitBlameLine", function()
require("git.blame").toggle_inline() require("git.blame").toggle_inline()
end, { desc = "Toggle inline git blame in the current buffer" }) end, { desc = "Toggle inline git blame in the current buffer" })
+2 -2
View File
@@ -19,8 +19,8 @@ syntax match gitlogGraphLine /^[*|\\\/_ ]\+$/
highlight default link gitlogGraph Comment highlight default link gitlogGraph Comment
highlight default link gitlogHash GitSha highlight default link gitlogHash GitSha
highlight default link gitlogDate Number highlight default link gitlogDate GitDate
highlight default link gitlogAuthor String highlight default link gitlogAuthor GitAuthor
highlight default link gitlogRef Constant highlight default link gitlogRef Constant
let b:current_syntax = "gitlog" let b:current_syntax = "gitlog"
+73 -47
View File
@@ -76,19 +76,45 @@ local function wait_buf_name(pat)
end, "current buffer name to match " .. pat) end, "current buffer name to match " .. pat)
end end
t.test("relative_time buckets", function() t.test("inline annotation includes the relative time", function()
local now = os.time() local _, buf = setup("alpha\nbeta\ngamma\n")
t.eq(blame.relative_time(now), "just now") vim.api.nvim_set_current_buf(buf)
t.eq(blame.relative_time(now - 10), "just now") vim.api.nvim_win_set_cursor(0, { 1, 0 })
t.eq(blame.relative_time(now - 60), "a minute ago") enable_blame(buf)
t.eq(blame.relative_time(now - 5 * 60), "5 minutes ago") t.wait_for(function()
t.eq(blame.relative_time(now - 60 * 60), "an hour ago") return #inline_marks(buf) == 1
t.eq(blame.relative_time(now - 3 * 3600), "3 hours ago") end, "an inline annotation on the current line")
t.eq(blame.relative_time(now - 26 * 3600), "a day ago") local mark = assert(inline_marks(buf)[1])
t.eq(blame.relative_time(now - 3 * 86400), "3 days ago") local details = assert(mark[4])
t.eq(blame.relative_time(now - 14 * 86400), "2 weeks ago") local virt_text = assert(details.virt_text)
t.eq(blame.relative_time(now - 60 * 86400), "2 months ago") local chunk = assert(virt_text[1])
t.eq(blame.relative_time(now - 400 * 86400), "1 year ago") 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) end)
t.test("blame layout squeezes the author before date and sha", function() 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() t.quietly(function()
blame.line_popup(buf) blame.line_popup(buf)
blame.toggle_inline(buf) blame.toggle_inline(buf)
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
end) end)
t.eq(blame.state(buf), nil, "no state created for a non-worktree buffer") t.eq(blame.state(buf), nil, "no state created for a non-worktree buffer")
end) 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") t.eq(assert(inline_marks(buf)[1])[2], 2, "annotation moved to line 3")
end) 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") local _, buf = setup("a\nb\nc\nd\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.truthy( t.truthy(
vim.wo[win].statuscolumn ~= "", 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") t.eq(vim.wo[win].statuscolumn, "", "toggling off clears it")
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
@@ -354,21 +380,21 @@ t.test("overlay saves and restores the statuscolumn", function()
end) end)
vim.wo[win].statuscolumn = "%l custom" vim.wo[win].statuscolumn = "%l custom"
vim.wo[win].signcolumn = "yes:2" vim.wo[win].signcolumn = "yes:2"
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.truthy( t.truthy(
vim.wo[win].statuscolumn ~= "%l custom", vim.wo[win].statuscolumn ~= "%l custom",
"the overlay overrides a custom statuscolumn" "the gutter overrides a custom statuscolumn"
) )
t.eq( t.eq(
vim.wo[win].signcolumn, vim.wo[win].signcolumn,
"yes:2", "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") t.eq(vim.wo[win].statuscolumn, "%l custom", "statuscolumn restored")
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() 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].relativenumber = false
vim.wo[win].signcolumn = "no" vim.wo[win].signcolumn = "no"
vim.wo[win].foldcolumn = "0" vim.wo[win].foldcolumn = "0"
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil return s ~= nil and s.blame_width ~= nil
end, "the overlay blame to render") end, "the gutter blame to render")
t.eq( t.eq(
assert(blame.state(buf)).blame_width, assert(blame.state(buf)).blame_width,
40, 40,
@@ -393,7 +419,7 @@ t.test("overlay gutter uses the full preferred width when it can", function()
) )
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() 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].relativenumber = false
vim.wo[win].foldcolumn = "0" vim.wo[win].foldcolumn = "0"
vim.wo[win].signcolumn = "yes:9" vim.wo[win].signcolumn = "yes:9"
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil 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 native = blame._native_width(win)
local width = assert(assert(blame.state(buf)).blame_width) local width = assert(assert(blame.state(buf)).blame_width)
t.eq(native, 18, "signcolumn=yes:9 reserves an 18-cell sign column") 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") t.truthy(width + native <= 47, "blame plus native columns fits the cap")
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() 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].relativenumber = false
vim.wo[win].foldcolumn = "0" vim.wo[win].foldcolumn = "0"
vim.wo[win].signcolumn = "no" vim.wo[win].signcolumn = "no"
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil return s ~= nil and s.blame_width ~= nil
end, "the overlay blame to render") end, "the gutter blame to render")
t.eq( t.eq(
assert(blame.state(buf)).blame_width, assert(blame.state(buf)).blame_width,
40, 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, "the blame to re-budget for the widened signcolumn")
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.blame_width ~= nil return s ~= nil and s.blame_width ~= nil
end, "the overlay to render") end, "the gutter to render")
t.falsy( t.falsy(
vim.go.statuscolumn:find("git.blame", 1, true), 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") vim.cmd("new")
@@ -476,15 +502,15 @@ t.test("the overlay statuscolumn does not leak into other windows", function()
) )
end) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.tick ~= nil 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) 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: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") 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) 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") local _, buf = setup("a\nb\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.tick ~= nil 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) 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") t.falsy(g:match("%x%x%x%x%x%x%x%x"), "no sha on a virtual line")
end) end)
@@ -512,11 +538,11 @@ t.test("the statuscolumn expression renders the blame gutter", function()
local sha = h.git(dir, "rev-parse", "HEAD").stdout local sha = h.git(dir, "rev-parse", "HEAD").stdout
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.wait_for(function() t.wait_for(function()
local s = blame.state(buf) local s = blame.state(buf)
return s ~= nil and s.tick ~= nil 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( local rendered = vim.api.nvim_eval_statusline(
"%{%v:lua.require('git.blame').statuscolumn()%}", "%{%v:lua.require('git.blame').statuscolumn()%}",
{ winid = win, use_statuscol_lnum = 1 } { 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) vim.api.nvim_set_current_buf(buf)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
enable_blame(buf) enable_blame(buf)
blame.toggle_overlay(buf) blame.toggle_gutter(buf)
t.truthy(vim.wo[win].statuscolumn ~= "", "the overlay statuscolumn set") t.truthy(vim.wo[win].statuscolumn ~= "", "the gutter statuscolumn set")
blame.detach(buf) blame.detach(buf)
t.eq(blame.state(buf), nil, "state dropped on detach") t.eq(blame.state(buf), nil, "state dropped on detach")
t.eq(#inline_marks(buf), 0, "inline annotation cleared") 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) end)