refactor(git): rework blame highlights and rename overlay to gutter
This commit is contained in:
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user