refactor(git): remove inline and side-window blame, keep popup + drill actions
This commit is contained in:
+4
-379
@@ -4,16 +4,9 @@ local util = require("git.core.util")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local NS_INLINE = vim.api.nvim_create_namespace("ow.git.blame.inline")
|
|
||||||
local NS_POPUP = vim.api.nvim_create_namespace("ow.git.blame.popup")
|
local NS_POPUP = vim.api.nvim_create_namespace("ow.git.blame.popup")
|
||||||
local NS_VIEW = vim.api.nvim_create_namespace("ow.git.blame.view")
|
|
||||||
|
|
||||||
local ZERO_SHA = string.rep("0", 40)
|
local ZERO_SHA = string.rep("0", 40)
|
||||||
local SHA_W = 8
|
|
||||||
local AUTHOR_W = 20
|
|
||||||
local DATE_W = 10
|
|
||||||
local GAP = " "
|
|
||||||
local VIEW_WIDTH = SHA_W + #GAP + AUTHOR_W + #GAP + DATE_W
|
|
||||||
|
|
||||||
---@class ow.Git.Blame.Commit
|
---@class ow.Git.Blame.Commit
|
||||||
---@field sha string
|
---@field sha string
|
||||||
@@ -41,8 +34,6 @@ local VIEW_WIDTH = SHA_W + #GAP + AUTHOR_W + #GAP + DATE_W
|
|||||||
---@field tick integer?
|
---@field tick integer?
|
||||||
---@field epoch integer
|
---@field epoch integer
|
||||||
---@field pending fun()[]
|
---@field pending fun()[]
|
||||||
---@field inline boolean
|
|
||||||
---@field autocmds integer[]
|
|
||||||
|
|
||||||
---@type table<integer, ow.Git.Blame.BufState>
|
---@type table<integer, ow.Git.Blame.BufState>
|
||||||
local states = {}
|
local states = {}
|
||||||
@@ -59,41 +50,6 @@ function M.state(buf)
|
|||||||
return states[buf]
|
return states[buf]
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param unix_ts integer
|
|
||||||
---@return string
|
|
||||||
local function relative_time(unix_ts)
|
|
||||||
local diff = os.time() - unix_ts
|
|
||||||
if diff < 0 then
|
|
||||||
diff = 0
|
|
||||||
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
|
|
||||||
return "just now"
|
|
||||||
elseif diff < 90 then
|
|
||||||
return "a minute ago"
|
|
||||||
elseif diff < 45 * 60 then
|
|
||||||
return fmt(math.floor(diff / 60 + 0.5), "minute")
|
|
||||||
elseif diff < 90 * 60 then
|
|
||||||
return "an hour ago"
|
|
||||||
elseif diff < 22 * 3600 then
|
|
||||||
return fmt(math.floor(diff / 3600 + 0.5), "hour")
|
|
||||||
elseif diff < 36 * 3600 then
|
|
||||||
return "a day ago"
|
|
||||||
elseif diff < 7 * 86400 then
|
|
||||||
return fmt(math.floor(diff / 86400 + 0.5), "day")
|
|
||||||
elseif diff < 30 * 86400 then
|
|
||||||
return fmt(math.floor(diff / (7 * 86400) + 0.5), "week")
|
|
||||||
elseif diff < 365 * 86400 then
|
|
||||||
return fmt(math.floor(diff / (30 * 86400) + 0.5), "month")
|
|
||||||
end
|
|
||||||
|
|
||||||
return fmt(math.floor(diff / (365 * 86400) + 0.5), "year")
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param ts integer
|
---@param ts integer
|
||||||
---@param tz string
|
---@param tz string
|
||||||
---@return string
|
---@return string
|
||||||
@@ -269,8 +225,6 @@ local function ensure_state(buf)
|
|||||||
tick = nil,
|
tick = nil,
|
||||||
epoch = 0,
|
epoch = 0,
|
||||||
pending = {},
|
pending = {},
|
||||||
inline = false,
|
|
||||||
autocmds = {},
|
|
||||||
}
|
}
|
||||||
states[buf] = state
|
states[buf] = state
|
||||||
return state
|
return state
|
||||||
@@ -327,327 +281,6 @@ local function run_blame(state, buf, done)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
local function render_inline(buf)
|
|
||||||
if not vim.api.nvim_buf_is_valid(buf) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
vim.api.nvim_buf_clear_namespace(buf, NS_INLINE, 0, -1)
|
|
||||||
local state = states[buf]
|
|
||||||
if not state or not state.inline then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local win = vim.api.nvim_get_current_buf() == buf
|
|
||||||
and vim.api.nvim_get_current_win()
|
|
||||||
or vim.fn.bufwinid(buf)
|
|
||||||
if win == -1 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local lnum = vim.api.nvim_win_get_cursor(win)[1]
|
|
||||||
local sha = state.line_sha[lnum]
|
|
||||||
local commit = sha and state.commits[sha]
|
|
||||||
if not commit then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local text
|
|
||||||
if util.is_zero_sha(sha) then
|
|
||||||
text = " You - Not Committed Yet"
|
|
||||||
else
|
|
||||||
text = string.format(
|
|
||||||
" %s, %s - %s",
|
|
||||||
commit.author,
|
|
||||||
relative_time(commit.author_time),
|
|
||||||
commit.summary
|
|
||||||
)
|
|
||||||
end
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, buf, NS_INLINE, lnum - 1, 0, {
|
|
||||||
virt_text = { { text, "GitBlameInline" } },
|
|
||||||
virt_text_pos = "eol",
|
|
||||||
hl_mode = "combine",
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
local function reblame(buf)
|
|
||||||
local state = states[buf]
|
|
||||||
if not state or not state.inline then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
run_blame(state, buf, function()
|
|
||||||
render_inline(buf)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local schedule, sched_handle = util.keyed_debounce(reblame, 150)
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
function M._flush(buf)
|
|
||||||
sched_handle.flush(buf)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@param state ow.Git.Blame.BufState
|
|
||||||
local function attach_autocmds(buf, state)
|
|
||||||
if #state.autocmds > 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local group =
|
|
||||||
vim.api.nvim_create_augroup("ow.git.blame." .. buf, { clear = true })
|
|
||||||
state.autocmds = {
|
|
||||||
vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
|
|
||||||
group = group,
|
|
||||||
buffer = buf,
|
|
||||||
callback = function()
|
|
||||||
render_inline(buf)
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
vim.api.nvim_create_autocmd(
|
|
||||||
{ "TextChanged", "TextChangedI", "BufWritePost" },
|
|
||||||
{
|
|
||||||
group = group,
|
|
||||||
buffer = buf,
|
|
||||||
callback = function()
|
|
||||||
schedule(buf)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@param state ow.Git.Blame.BufState
|
|
||||||
local function detach_autocmds(buf, state)
|
|
||||||
for _, id in ipairs(state.autocmds) do
|
|
||||||
pcall(vim.api.nvim_del_autocmd, id)
|
|
||||||
end
|
|
||||||
state.autocmds = {}
|
|
||||||
pcall(vim.api.nvim_del_augroup_by_name, "ow.git.blame." .. buf)
|
|
||||||
sched_handle.cancel(buf)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer?
|
|
||||||
function M.toggle_inline(buf)
|
|
||||||
buf = resolve_buf(buf)
|
|
||||||
local state = ensure_state(buf)
|
|
||||||
if not state then
|
|
||||||
util.warning("git blame: nothing to blame in this buffer")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
state.inline = not state.inline
|
|
||||||
if state.inline then
|
|
||||||
attach_autocmds(buf, state)
|
|
||||||
run_blame(state, buf, function()
|
|
||||||
render_inline(buf)
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
if vim.api.nvim_buf_is_valid(buf) then
|
|
||||||
vim.api.nvim_buf_clear_namespace(buf, NS_INLINE, 0, -1)
|
|
||||||
end
|
|
||||||
detach_autocmds(buf, state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class ow.Git.Blame.View
|
|
||||||
---@field source_buf integer
|
|
||||||
---@field source_win integer
|
|
||||||
---@field view_buf integer
|
|
||||||
---@field view_win integer
|
|
||||||
---@field group integer
|
|
||||||
|
|
||||||
---@type table<integer, ow.Git.Blame.View>
|
|
||||||
local views = {}
|
|
||||||
|
|
||||||
---@param s string
|
|
||||||
---@param w integer
|
|
||||||
---@return string
|
|
||||||
local function pad_w(s, w)
|
|
||||||
local sw = vim.api.nvim_strwidth(s)
|
|
||||||
if sw > w then
|
|
||||||
return vim.fn.strcharpart(s, 0, w)
|
|
||||||
end
|
|
||||||
return s .. string.rep(" ", w - sw)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param state ow.Git.Blame.BufState
|
|
||||||
---@param line_count integer
|
|
||||||
---@return string[]
|
|
||||||
local function build_view_lines(state, line_count)
|
|
||||||
local lines = {}
|
|
||||||
for lnum = 1, line_count do
|
|
||||||
local sha = state.line_sha[lnum]
|
|
||||||
local commit = sha and state.commits[sha]
|
|
||||||
if commit and not util.is_zero_sha(sha) then
|
|
||||||
local author = pad_w(commit.author, AUTHOR_W)
|
|
||||||
local date = os.date("%Y-%m-%d", commit.author_time)
|
|
||||||
lines[lnum] = sha:sub(1, SHA_W) .. GAP .. author .. GAP .. date
|
|
||||||
else
|
|
||||||
lines[lnum] = pad_w("Uncommitted", VIEW_WIDTH)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return lines
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param view ow.Git.Blame.View
|
|
||||||
---@param state ow.Git.Blame.BufState
|
|
||||||
local function render_view(view, state)
|
|
||||||
if not vim.api.nvim_buf_is_valid(view.view_buf) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local line_count = vim.api.nvim_buf_line_count(view.source_buf)
|
|
||||||
local lines = build_view_lines(state, line_count)
|
|
||||||
util.set_buf_lines(view.view_buf, 0, -1, lines)
|
|
||||||
vim.api.nvim_buf_clear_namespace(view.view_buf, NS_VIEW, 0, -1)
|
|
||||||
local author_col = SHA_W + #GAP
|
|
||||||
local date_col = author_col + AUTHOR_W + #GAP
|
|
||||||
for lnum = 1, #lines do
|
|
||||||
local sha = state.line_sha[lnum]
|
|
||||||
if sha and not util.is_zero_sha(sha) then
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, view.view_buf, NS_VIEW, lnum - 1, 0, {
|
|
||||||
end_col = SHA_W,
|
|
||||||
hl_group = "GitBlameSha",
|
|
||||||
})
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, view.view_buf, NS_VIEW, lnum - 1, author_col, {
|
|
||||||
end_col = author_col + AUTHOR_W,
|
|
||||||
hl_group = "GitBlameAuthor",
|
|
||||||
})
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, view.view_buf, NS_VIEW, lnum - 1, date_col, {
|
|
||||||
end_col = date_col + DATE_W,
|
|
||||||
hl_group = "GitBlameDate",
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param source_buf integer
|
|
||||||
local function close_view(source_buf)
|
|
||||||
local view = views[source_buf]
|
|
||||||
if not view then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
views[source_buf] = nil
|
|
||||||
pcall(vim.api.nvim_del_augroup_by_id, view.group)
|
|
||||||
if vim.api.nvim_win_is_valid(view.source_win) then
|
|
||||||
vim.wo[view.source_win].scrollbind = false
|
|
||||||
vim.wo[view.source_win].cursorbind = false
|
|
||||||
end
|
|
||||||
if vim.api.nvim_win_is_valid(view.view_win) then
|
|
||||||
pcall(vim.api.nvim_win_close, view.view_win, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param source_buf integer
|
|
||||||
---@param state ow.Git.Blame.BufState
|
|
||||||
---@param source_win integer
|
|
||||||
local function open_view(source_buf, state, source_win)
|
|
||||||
local view_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
util.setup_scratch(view_buf, { buftype = "nofile", bufhidden = "wipe" })
|
|
||||||
vim.bo[view_buf].filetype = "gitblame"
|
|
||||||
local view_win = vim.api.nvim_open_win(view_buf, false, {
|
|
||||||
split = "left",
|
|
||||||
width = VIEW_WIDTH,
|
|
||||||
win = source_win,
|
|
||||||
})
|
|
||||||
vim.api.nvim_set_option_value("number", false, { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("relativenumber", false, { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("signcolumn", "no", { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("foldcolumn", "0", { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("wrap", false, { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("cursorline", true, { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("winfixwidth", true, { win = view_win, scope = "local" })
|
|
||||||
vim.api.nvim_set_option_value("statuscolumn", "", { win = view_win, scope = "local" })
|
|
||||||
|
|
||||||
if not vim.tbl_contains(vim.opt.scrollopt:get(), "ver") then
|
|
||||||
vim.opt.scrollopt:append("ver")
|
|
||||||
end
|
|
||||||
vim.wo[source_win].scrollbind = true
|
|
||||||
vim.wo[source_win].cursorbind = true
|
|
||||||
vim.wo[view_win].scrollbind = true
|
|
||||||
vim.wo[view_win].cursorbind = true
|
|
||||||
|
|
||||||
local group = vim.api.nvim_create_augroup(
|
|
||||||
"ow.git.blame.view." .. source_buf,
|
|
||||||
{ clear = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
---@type ow.Git.Blame.View
|
|
||||||
local view = {
|
|
||||||
source_buf = source_buf,
|
|
||||||
source_win = source_win,
|
|
||||||
view_buf = view_buf,
|
|
||||||
view_win = view_win,
|
|
||||||
group = group,
|
|
||||||
}
|
|
||||||
views[source_buf] = view
|
|
||||||
render_view(view, state)
|
|
||||||
vim.api.nvim_win_call(view_win, function()
|
|
||||||
vim.cmd("syncbind")
|
|
||||||
end)
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd("WinClosed", {
|
|
||||||
group = group,
|
|
||||||
callback = function(args)
|
|
||||||
local closed = tonumber(args.match)
|
|
||||||
if closed == view_win or closed == source_win then
|
|
||||||
vim.schedule(function()
|
|
||||||
close_view(source_buf)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
|
||||||
group = group,
|
|
||||||
buffer = view_buf,
|
|
||||||
callback = function()
|
|
||||||
vim.schedule(function()
|
|
||||||
close_view(source_buf)
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.keymap.set("n", "<CR>", function()
|
|
||||||
local lnum = vim.api.nvim_win_get_cursor(view_win)[1]
|
|
||||||
local sha = state.line_sha[lnum]
|
|
||||||
if not sha or util.is_zero_sha(sha) then
|
|
||||||
util.warning("git blame: line is not committed yet")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
object.open(state.repo, sha, { split = false })
|
|
||||||
end, { buffer = view_buf, silent = true, desc = "Open the commit" })
|
|
||||||
vim.keymap.set("n", "q", function()
|
|
||||||
close_view(source_buf)
|
|
||||||
end, { buffer = view_buf, silent = true, desc = "Close the blame view" })
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer?
|
|
||||||
function M.toggle_view(buf)
|
|
||||||
buf = resolve_buf(buf)
|
|
||||||
if views[buf] then
|
|
||||||
close_view(buf)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local state = ensure_state(buf)
|
|
||||||
if not state then
|
|
||||||
util.warning("git blame: nothing to blame in this buffer")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local source_win = vim.api.nvim_get_current_win()
|
|
||||||
if vim.api.nvim_win_get_buf(source_win) ~= buf then
|
|
||||||
source_win = vim.fn.bufwinid(buf)
|
|
||||||
end
|
|
||||||
if source_win == -1 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
run_blame(state, buf, function()
|
|
||||||
if
|
|
||||||
not vim.api.nvim_buf_is_valid(buf)
|
|
||||||
or not vim.api.nvim_win_is_valid(source_win)
|
|
||||||
then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
open_view(buf, state, source_win)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param lines string[]
|
---@param lines string[]
|
||||||
---@return integer width
|
---@return integer width
|
||||||
---@return integer height
|
---@return integer height
|
||||||
@@ -884,34 +517,26 @@ end
|
|||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
function M.detach(buf)
|
function M.detach(buf)
|
||||||
close_view(buf)
|
|
||||||
local state = states[buf]
|
local state = states[buf]
|
||||||
if not state then
|
if not state then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if vim.api.nvim_buf_is_valid(buf) then
|
|
||||||
vim.api.nvim_buf_clear_namespace(buf, NS_INLINE, 0, -1)
|
|
||||||
end
|
|
||||||
detach_autocmds(buf, state)
|
|
||||||
state.epoch = state.epoch + 1
|
state.epoch = state.epoch + 1
|
||||||
states[buf] = nil
|
states[buf] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The blame cache is keyed by `changedtick`, which a commit / checkout /
|
-- The blame cache is keyed by `changedtick`, which a commit / checkout /
|
||||||
-- rebase does not bump. Drop the cache for affected worktree buffers on
|
-- rebase does not bump. Drop the cache for affected worktree buffers on a
|
||||||
-- a repo change so the next blame re-fetches; re-blame eagerly if a mode
|
-- repo change so the next popup re-fetches. `git://` buffers blame a
|
||||||
-- is showing. `git://` buffers blame a fixed revision and are skipped.
|
-- fixed revision and are skipped.
|
||||||
repo.on("change", function(r, change)
|
repo.on("change", function(r, change)
|
||||||
for buf, state in pairs(states) do
|
for _, state in pairs(states) do
|
||||||
if
|
if
|
||||||
state.repo == r
|
state.repo == r
|
||||||
and not state.revision
|
and not state.revision
|
||||||
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 then
|
|
||||||
schedule(buf)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ local DEFAULT_HIGHLIGHTS = {
|
|||||||
|
|
||||||
GitBlameAuthor = "GitAuthor",
|
GitBlameAuthor = "GitAuthor",
|
||||||
GitBlameDate = "GitDate",
|
GitBlameDate = "GitDate",
|
||||||
GitBlameInline = "Comment",
|
|
||||||
GitBlameSha = "GitSha",
|
GitBlameSha = "GitSha",
|
||||||
}
|
}
|
||||||
local STAGED_HUNK_HL = {
|
local STAGED_HUNK_HL = {
|
||||||
@@ -322,12 +321,6 @@ 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-view)", function()
|
|
||||||
require("git.blame").toggle_view()
|
|
||||||
end, { silent = true, desc = "Toggle the git blame side window" })
|
|
||||||
vim.keymap.set("n", "<Plug>(git-blame-line)", function()
|
|
||||||
require("git.blame").toggle_inline()
|
|
||||||
end, { silent = true, desc = "Toggle inline git blame" })
|
|
||||||
vim.keymap.set("n", "<Plug>(git-blame-popup)", function()
|
vim.keymap.set("n", "<Plug>(git-blame-popup)", function()
|
||||||
require("git.blame").line_popup()
|
require("git.blame").line_popup()
|
||||||
end, { silent = true, desc = "Show git blame for the current line" })
|
end, { silent = true, desc = "Show git blame for the current line" })
|
||||||
@@ -344,9 +337,3 @@ end, {
|
|||||||
desc = "Open this file at the parent of the line's commit",
|
desc = "Open this file at the parent of the line's commit",
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("GitBlame", function()
|
|
||||||
require("git.blame").toggle_view()
|
|
||||||
end, { desc = "Toggle the git blame side window" })
|
|
||||||
vim.api.nvim_create_user_command("GitBlameLine", function()
|
|
||||||
require("git.blame").toggle_inline()
|
|
||||||
end, { desc = "Toggle inline git blame in the current buffer" })
|
|
||||||
|
|||||||
+33
-182
@@ -23,32 +23,6 @@ local function setup(committed, worktree, file)
|
|||||||
return dir, vim.api.nvim_get_current_buf()
|
return dir, vim.api.nvim_get_current_buf()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@return ow.Git.Blame.BufState
|
|
||||||
local function enable_blame(buf)
|
|
||||||
blame.toggle_inline(buf)
|
|
||||||
t.wait_for(function()
|
|
||||||
local s = blame.state(buf)
|
|
||||||
return s ~= nil and s.tick ~= nil
|
|
||||||
end, "blame to populate the buffer state")
|
|
||||||
local s = assert(blame.state(buf))
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@param ns_name string
|
|
||||||
---@return vim.api.keyset.get_extmark_item[]
|
|
||||||
local function marks(buf, ns_name)
|
|
||||||
local ns = vim.api.nvim_get_namespaces()[ns_name]
|
|
||||||
return vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@return vim.api.keyset.get_extmark_item[]
|
|
||||||
local function inline_marks(buf)
|
|
||||||
return marks(buf, "ow.git.blame.inline")
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return integer?
|
---@return integer?
|
||||||
local function find_float()
|
local function find_float()
|
||||||
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
@@ -76,23 +50,24 @@ local function wait_buf_name(pat)
|
|||||||
end, "current buffer name to match " .. pat)
|
end, "current buffer name to match " .. pat)
|
||||||
end
|
end
|
||||||
|
|
||||||
t.test("inline annotation includes the relative time", function()
|
---@param buf integer
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
---@return ow.Git.Blame.BufState
|
||||||
|
local function populate_blame(buf)
|
||||||
vim.api.nvim_set_current_buf(buf)
|
vim.api.nvim_set_current_buf(buf)
|
||||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
enable_blame(buf)
|
local tick = vim.api.nvim_buf_get_changedtick(buf)
|
||||||
|
blame.line_popup(buf)
|
||||||
t.wait_for(function()
|
t.wait_for(function()
|
||||||
return #inline_marks(buf) == 1
|
local s = blame.state(buf)
|
||||||
end, "an inline annotation on the current line")
|
return s ~= nil and s.tick == tick
|
||||||
local mark = assert(inline_marks(buf)[1])
|
end, "blame to populate the buffer state")
|
||||||
local details = assert(mark[4])
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
local virt_text = assert(details.virt_text)
|
if vim.api.nvim_win_get_config(w).relative ~= "" then
|
||||||
local chunk = assert(virt_text[1])
|
pcall(vim.api.nvim_win_close, w, true)
|
||||||
t.truthy(
|
end
|
||||||
chunk[1]:find("just now", 1, true),
|
end
|
||||||
"a fresh commit reads as 'just now' in the annotation"
|
return (assert(blame.state(buf)))
|
||||||
)
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("line popup formats the datetime in the author timezone", function()
|
t.test("line popup formats the datetime in the author timezone", function()
|
||||||
local _, buf = setup("alpha\nbeta\n")
|
local _, buf = setup("alpha\nbeta\n")
|
||||||
@@ -119,7 +94,7 @@ end)
|
|||||||
|
|
||||||
t.test("porcelain parse of a committed file", function()
|
t.test("porcelain parse of a committed file", function()
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
local _, buf = setup("alpha\nbeta\ngamma\n")
|
||||||
local state = enable_blame(buf)
|
local state = populate_blame(buf)
|
||||||
t.eq(vim.tbl_count(state.commits), 1, "one commit")
|
t.eq(vim.tbl_count(state.commits), 1, "one commit")
|
||||||
local sha = state.line_sha[1]
|
local sha = state.line_sha[1]
|
||||||
t.eq(state.line_sha[2], sha, "line 2 shares the commit")
|
t.eq(state.line_sha[2], sha, "line 2 shares the commit")
|
||||||
@@ -138,7 +113,7 @@ t.test("multiple line groups reuse one commit entry", function()
|
|||||||
h.git(dir, "commit", "-q", "-m", "change middle")
|
h.git(dir, "commit", "-q", "-m", "change middle")
|
||||||
vim.cmd.edit(dir .. "/a.txt")
|
vim.cmd.edit(dir .. "/a.txt")
|
||||||
local buf = vim.api.nvim_get_current_buf()
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
local state = enable_blame(buf)
|
local state = populate_blame(buf)
|
||||||
t.eq(vim.tbl_count(state.commits), 2, "two distinct commits")
|
t.eq(vim.tbl_count(state.commits), 2, "two distinct commits")
|
||||||
t.eq(
|
t.eq(
|
||||||
state.line_sha[1],
|
state.line_sha[1],
|
||||||
@@ -153,28 +128,29 @@ end)
|
|||||||
|
|
||||||
t.test("an edited line blames as the zero sha", function()
|
t.test("an edited line blames as the zero sha", function()
|
||||||
local _, buf = setup("a\nb\nc\n")
|
local _, buf = setup("a\nb\nc\n")
|
||||||
local state = enable_blame(buf)
|
local state = populate_blame(buf)
|
||||||
t.falsy(is_zero(state.line_sha[2]), "line 2 starts committed")
|
t.falsy(is_zero(state.line_sha[2]), "line 2 starts committed")
|
||||||
vim.api.nvim_buf_set_lines(buf, 1, 2, false, { "EDITED" })
|
vim.api.nvim_buf_set_lines(buf, 1, 2, false, { "EDITED" })
|
||||||
vim.api.nvim_exec_autocmds("TextChanged", { buffer = buf })
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
blame._flush(buf)
|
local refreshed = populate_blame(buf)
|
||||||
t.wait_for(function()
|
t.truthy(
|
||||||
local s = assert(blame.state(buf))
|
is_zero(refreshed.line_sha[2]),
|
||||||
return s.line_sha[2] ~= nil and is_zero(s.line_sha[2])
|
"the edited line blames as uncommitted on the next fetch"
|
||||||
end, "the edited line to blame as uncommitted")
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("blame refreshes after a git event", function()
|
t.test("blame picks up a commit amend on the next fetch", function()
|
||||||
local dir, buf = setup("original\n")
|
local dir, buf = setup("original\n")
|
||||||
local state = enable_blame(buf)
|
local state = populate_blame(buf)
|
||||||
local sha1 = state.line_sha[1]
|
local sha1 = state.line_sha[1]
|
||||||
h.git(dir, "commit", "--amend", "-m", "amended")
|
h.git(dir, "commit", "--amend", "-m", "amended")
|
||||||
local sha2 = h.git(dir, "rev-parse", "HEAD").stdout
|
local sha2 = h.git(dir, "rev-parse", "HEAD").stdout
|
||||||
t.truthy(sha1 ~= sha2, "the amend produced a new commit")
|
t.truthy(sha1 ~= sha2, "the amend produced a new commit")
|
||||||
t.wait_for(function()
|
t.wait_for(function()
|
||||||
local s = blame.state(buf)
|
return assert(blame.state(buf)).tick == nil
|
||||||
return s ~= nil and s.line_sha[1] == sha2
|
end, "the cache to be invalidated by the repo change")
|
||||||
end, "blame to pick up the amended commit", 2000)
|
local refreshed = populate_blame(buf)
|
||||||
|
t.eq(refreshed.line_sha[1], sha2, "blame picks up the amended sha")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("an untracked file blames every line as uncommitted", function()
|
t.test("an untracked file blames every line as uncommitted", function()
|
||||||
@@ -182,7 +158,7 @@ t.test("an untracked file blames every line as uncommitted", function()
|
|||||||
t.write(dir, "new.txt", "one\ntwo\nthree\n")
|
t.write(dir, "new.txt", "one\ntwo\nthree\n")
|
||||||
vim.cmd.edit(dir .. "/new.txt")
|
vim.cmd.edit(dir .. "/new.txt")
|
||||||
local buf = vim.api.nvim_get_current_buf()
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
local state = enable_blame(buf)
|
local state = populate_blame(buf)
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
t.truthy(is_zero(state.line_sha[i]), "line " .. i .. " is uncommitted")
|
t.truthy(is_zero(state.line_sha[i]), "line " .. i .. " is uncommitted")
|
||||||
end
|
end
|
||||||
@@ -196,8 +172,6 @@ t.test("blame actions are no-ops off a worktree", function()
|
|||||||
end)
|
end)
|
||||||
t.quietly(function()
|
t.quietly(function()
|
||||||
blame.line_popup(buf)
|
blame.line_popup(buf)
|
||||||
blame.toggle_inline(buf)
|
|
||||||
blame.toggle_view(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)
|
||||||
@@ -273,127 +247,6 @@ t.test("line popup works in a git:// object buffer", function()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("inline toggle adds and removes the annotation", 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")
|
|
||||||
t.eq(assert(inline_marks(buf)[1])[2], 0, "annotation on line 1")
|
|
||||||
blame.toggle_inline(buf)
|
|
||||||
t.eq(#inline_marks(buf), 0, "annotation cleared when toggled off")
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("inline annotation follows the cursor", 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, "the initial annotation")
|
|
||||||
t.eq(assert(inline_marks(buf)[1])[2], 0)
|
|
||||||
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
|
||||||
vim.api.nvim_exec_autocmds("CursorMoved", { buffer = buf })
|
|
||||||
t.eq(#inline_marks(buf), 1, "still one annotation")
|
|
||||||
t.eq(assert(inline_marks(buf)[1])[2], 2, "annotation moved to line 3")
|
|
||||||
end)
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
---@return integer view_win
|
|
||||||
local function wait_view(buf)
|
|
||||||
local win ---@type integer?
|
|
||||||
t.wait_for(function()
|
|
||||||
for _, w in ipairs(vim.api.nvim_list_wins()) do
|
|
||||||
local b = vim.api.nvim_win_get_buf(w)
|
|
||||||
if b ~= buf and vim.bo[b].filetype == "gitblame" then
|
|
||||||
win = w
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end, "the blame side window to open")
|
|
||||||
return (assert(win))
|
|
||||||
end
|
|
||||||
|
|
||||||
t.test("toggle_view opens and closes a side split", function()
|
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
blame.toggle_view(buf)
|
|
||||||
local view_win = wait_view(buf)
|
|
||||||
t.defer(function()
|
|
||||||
if vim.api.nvim_win_is_valid(view_win) then
|
|
||||||
vim.api.nvim_win_close(view_win, true)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
blame.toggle_view(buf)
|
|
||||||
t.falsy(
|
|
||||||
vim.api.nvim_win_is_valid(view_win),
|
|
||||||
"toggling again closes the view"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("the side view shows sha, author and an absolute date", function()
|
|
||||||
local dir, buf = setup("alpha\nbeta\ngamma\n")
|
|
||||||
local sha = h.git(dir, "rev-parse", "HEAD").stdout
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
blame.toggle_view(buf)
|
|
||||||
local view_win = wait_view(buf)
|
|
||||||
t.defer(function()
|
|
||||||
if vim.api.nvim_win_is_valid(view_win) then
|
|
||||||
vim.api.nvim_win_close(view_win, true)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
local view_buf = vim.api.nvim_win_get_buf(view_win)
|
|
||||||
local row = vim.api.nvim_buf_get_lines(view_buf, 0, 1, false)[1] or ""
|
|
||||||
t.truthy(
|
|
||||||
vim.startswith(row, sha:sub(1, 8)),
|
|
||||||
"the row starts with the short sha"
|
|
||||||
)
|
|
||||||
t.truthy(row:find("t", 1, true), "the row includes the author")
|
|
||||||
t.truthy(
|
|
||||||
row:match("%d%d%d%d%-%d%d%-%d%d$"),
|
|
||||||
"the row ends with a YYYY-MM-DD date"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("<CR> on the view row opens the commit", function()
|
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
blame.toggle_view(buf)
|
|
||||||
local view_win = wait_view(buf)
|
|
||||||
t.defer(function()
|
|
||||||
if vim.api.nvim_win_is_valid(view_win) then
|
|
||||||
vim.api.nvim_win_close(view_win, true)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
vim.api.nvim_set_current_win(view_win)
|
|
||||||
vim.api.nvim_win_set_cursor(view_win, { 1, 0 })
|
|
||||||
t.press("<CR>")
|
|
||||||
wait_buf_name("^git://%x+$")
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("closing the source window tears down the view", function()
|
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
local source_win = vim.api.nvim_get_current_win()
|
|
||||||
vim.cmd("split")
|
|
||||||
vim.api.nvim_set_current_win(source_win)
|
|
||||||
blame.toggle_view(buf)
|
|
||||||
local view_win = wait_view(buf)
|
|
||||||
t.defer(function()
|
|
||||||
if vim.api.nvim_win_is_valid(view_win) then
|
|
||||||
vim.api.nvim_win_close(view_win, true)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
vim.api.nvim_win_close(source_win, true)
|
|
||||||
t.wait_for(function()
|
|
||||||
return not vim.api.nvim_win_is_valid(view_win)
|
|
||||||
end, "the view to close when the source window closes")
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.test("open_commit opens the commit that last touched the line", function()
|
t.test("open_commit opens the commit that last touched the line", function()
|
||||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
local _, buf = setup("alpha\nbeta\ngamma\n")
|
||||||
vim.api.nvim_set_current_buf(buf)
|
vim.api.nvim_set_current_buf(buf)
|
||||||
@@ -451,11 +304,9 @@ t.test("drilling chains through git:// buffers", function()
|
|||||||
wait_buf_name("^git://%x+$")
|
wait_buf_name("^git://%x+$")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("detach clears blame state and annotations", function()
|
t.test("detach drops the blame state", function()
|
||||||
local _, buf = setup("a\nb\nc\n")
|
local _, buf = setup("a\nb\nc\n")
|
||||||
vim.api.nvim_set_current_buf(buf)
|
populate_blame(buf)
|
||||||
enable_blame(buf)
|
|
||||||
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")
|
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user