feat(blame): show commit's hunk in the line popup
This commit is contained in:
+207
-48
@@ -1,10 +1,12 @@
|
||||
local object = require("git.object")
|
||||
local popup = require("git.core.popup")
|
||||
local repo = require("git.core.repo")
|
||||
local util = require("git.core.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
local NS_POPUP = vim.api.nvim_create_namespace("ow.git.blame.popup")
|
||||
local NS_OVERFLOW = vim.api.nvim_create_namespace("ow.git.blame.popup.overflow")
|
||||
|
||||
local ZERO_SHA = string.rep("0", 40)
|
||||
|
||||
@@ -19,6 +21,8 @@ local ZERO_SHA = string.rep("0", 40)
|
||||
---@class ow.Git.Blame.Result
|
||||
---@field commits table<string, ow.Git.Blame.Commit>
|
||||
---@field line_sha table<integer, string>
|
||||
---@field line_orig table<integer, integer>
|
||||
---@field line_file table<integer, string>
|
||||
|
||||
---@class ow.Git.Blame.Source
|
||||
---@field repo ow.Git.Repo
|
||||
@@ -31,6 +35,8 @@ local ZERO_SHA = string.rep("0", 40)
|
||||
---@field revision string?
|
||||
---@field commits table<string, ow.Git.Blame.Commit>
|
||||
---@field line_sha table<integer, string>
|
||||
---@field line_orig table<integer, integer>
|
||||
---@field line_file table<integer, string>
|
||||
---@field tick integer?
|
||||
---@field epoch integer
|
||||
---@field pending fun()[]
|
||||
@@ -72,20 +78,33 @@ local function parse_porcelain(stdout)
|
||||
local commits = {}
|
||||
---@type table<integer, string>
|
||||
local line_sha = {}
|
||||
---@type table<integer, integer>
|
||||
local line_orig = {}
|
||||
---@type table<integer, string>
|
||||
local line_file = {}
|
||||
local cur_sha ---@type string?
|
||||
local cur_lnum ---@type integer?
|
||||
local cur_orig ---@type integer?
|
||||
local cur_file ---@type string?
|
||||
for _, line in ipairs(util.split_lines(stdout)) do
|
||||
if line:sub(1, 1) == "\t" then
|
||||
if cur_sha and cur_lnum then
|
||||
if cur_sha and cur_lnum and cur_orig then
|
||||
line_sha[cur_lnum] = cur_sha
|
||||
line_orig[cur_lnum] = cur_orig
|
||||
if cur_file then
|
||||
line_file[cur_lnum] = cur_file
|
||||
end
|
||||
end
|
||||
cur_sha = nil
|
||||
cur_lnum = nil
|
||||
cur_orig = nil
|
||||
cur_file = nil
|
||||
else
|
||||
local sha, final = line:match("^(%x+) %d+ (%d+)")
|
||||
local sha, orig, final = line:match("^(%x+) (%d+) (%d+)")
|
||||
if sha and #sha >= 40 then
|
||||
cur_sha = sha
|
||||
cur_lnum = tonumber(final) --[[@as integer?]]
|
||||
cur_orig = tonumber(orig) --[[@as integer]]
|
||||
cur_lnum = tonumber(final) --[[@as integer]]
|
||||
if not commits[sha] then
|
||||
commits[sha] = {
|
||||
sha = sha,
|
||||
@@ -99,7 +118,9 @@ local function parse_porcelain(stdout)
|
||||
else
|
||||
local key, value = line:match("^(%S+) (.*)$")
|
||||
local commit = cur_sha and commits[cur_sha]
|
||||
if commit and key then
|
||||
if key == "filename" and value then
|
||||
cur_file = value
|
||||
elseif commit and key then
|
||||
if key == "author" then
|
||||
commit.author = value
|
||||
elseif key == "author-mail" then
|
||||
@@ -115,7 +136,12 @@ local function parse_porcelain(stdout)
|
||||
end
|
||||
end
|
||||
end
|
||||
return { commits = commits, line_sha = line_sha }
|
||||
return {
|
||||
commits = commits,
|
||||
line_sha = line_sha,
|
||||
line_orig = line_orig,
|
||||
line_file = line_file,
|
||||
}
|
||||
end
|
||||
|
||||
---@param line_count integer
|
||||
@@ -138,6 +164,8 @@ local function synth_uncommitted(line_count)
|
||||
},
|
||||
},
|
||||
line_sha = line_sha,
|
||||
line_orig = {},
|
||||
line_file = {},
|
||||
}
|
||||
end
|
||||
|
||||
@@ -219,6 +247,8 @@ local function ensure_state(buf)
|
||||
revision = src.revision,
|
||||
commits = {},
|
||||
line_sha = {},
|
||||
line_orig = {},
|
||||
line_file = {},
|
||||
tick = nil,
|
||||
epoch = 0,
|
||||
pending = {},
|
||||
@@ -266,6 +296,8 @@ local function run_blame(state, buf, done)
|
||||
or synth_uncommitted(vim.api.nvim_buf_line_count(buf))
|
||||
state.commits = data.commits
|
||||
state.line_sha = data.line_sha
|
||||
state.line_orig = data.line_orig
|
||||
state.line_file = data.line_file
|
||||
state.tick = tick
|
||||
local pending = state.pending
|
||||
state.pending = {}
|
||||
@@ -275,20 +307,101 @@ local function run_blame(state, buf, done)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
---@return integer width
|
||||
---@return integer height
|
||||
local function size_for(lines)
|
||||
local width = 1
|
||||
for _, l in ipairs(lines) do
|
||||
local w = vim.api.nvim_strwidth(l)
|
||||
if w > width then
|
||||
width = w
|
||||
local NEW_FILE_MARKER = "(new file)"
|
||||
|
||||
---@param diff_text string
|
||||
---@param orig_line integer
|
||||
---@return string[]?
|
||||
local function extract_commit_hunk(diff_text, orig_line)
|
||||
local lines = util.split_lines(diff_text)
|
||||
for _, line in ipairs(lines) do
|
||||
if line == "--- /dev/null" then
|
||||
return { NEW_FILE_MARKER }
|
||||
end
|
||||
end
|
||||
width = math.min(math.max(width + 1, 30), vim.o.columns - 4)
|
||||
local height = math.min(math.max(#lines, 1), math.floor(vim.o.lines / 2))
|
||||
return width, height
|
||||
local hunk_starts = {} ---@type integer[]
|
||||
local target ---@type integer?
|
||||
for i, line in ipairs(lines) do
|
||||
local c, d = line:match("^@@ %-%d+,?%d* %+(%d+),?(%d*) @@")
|
||||
if c then
|
||||
table.insert(hunk_starts, i)
|
||||
if not target then
|
||||
local ns = tonumber(c) --[[@as integer]]
|
||||
local nc = (d == "" or d == nil) and 1 or tonumber(d) --[[@as integer]]
|
||||
if orig_line >= ns and orig_line < ns + nc then
|
||||
target = #hunk_starts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not target then
|
||||
return nil
|
||||
end
|
||||
local out = {} ---@type string[]
|
||||
table.insert(out, ("Hunk %d of %d"):format(target, #hunk_starts))
|
||||
table.insert(out, "")
|
||||
local start_idx = hunk_starts[target]
|
||||
local end_idx = hunk_starts[target + 1] and (hunk_starts[target + 1] - 1)
|
||||
or #lines
|
||||
for j = start_idx, end_idx do
|
||||
local line = lines[j] --[[@as string]]
|
||||
if line:match("^diff %-%-git") then
|
||||
break
|
||||
end
|
||||
table.insert(out, line)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local POPUP_SEP = "GIT_BLAME_POPUP_SEP"
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param sha string
|
||||
---@param rel string
|
||||
---@param orig_line integer
|
||||
---@param done fun(message: string[]?, hunk: string[]?)
|
||||
local function fetch_show(r, sha, rel, orig_line, done)
|
||||
util.git({
|
||||
"show",
|
||||
"--diff-merges=first-parent",
|
||||
"--format=%B%n" .. POPUP_SEP .. "%n",
|
||||
sha,
|
||||
"--",
|
||||
rel,
|
||||
}, {
|
||||
cwd = r.worktree,
|
||||
silent = true,
|
||||
on_exit = function(res)
|
||||
if res.code ~= 0 then
|
||||
done(nil, nil)
|
||||
return
|
||||
end
|
||||
local lines = util.split_lines(res.stdout or "")
|
||||
local sep_idx ---@type integer?
|
||||
for i, line in ipairs(lines) do
|
||||
if line == POPUP_SEP then
|
||||
sep_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if not sep_idx then
|
||||
done(nil, nil)
|
||||
return
|
||||
end
|
||||
local message = {} ---@type string[]
|
||||
for i = 1, sep_idx - 1 do
|
||||
table.insert(message, lines[i])
|
||||
end
|
||||
while #message > 0 and message[#message] == "" do
|
||||
table.remove(message)
|
||||
end
|
||||
local diff_text = table.concat(lines, "\n", sep_idx + 1) ---@type string
|
||||
done(
|
||||
#message > 0 and message or nil,
|
||||
extract_commit_hunk(diff_text, orig_line)
|
||||
)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local popup_win ---@type integer?
|
||||
@@ -303,14 +416,23 @@ end
|
||||
---@param pbuf integer
|
||||
---@param win integer
|
||||
---@param head string[]
|
||||
---@param body string[]?
|
||||
---@param message string[]?
|
||||
---@param diff string[]?
|
||||
---@param sha_len integer?
|
||||
---@param date_col integer?
|
||||
local function apply_popup(pbuf, win, head, body, sha_len, date_col)
|
||||
local lines = {}
|
||||
local function apply_popup(pbuf, win, head, message, diff, sha_len, date_col)
|
||||
local lines = {} ---@type string[]
|
||||
vim.list_extend(lines, head)
|
||||
if body then
|
||||
vim.list_extend(lines, body)
|
||||
if message then
|
||||
vim.list_extend(lines, message)
|
||||
end
|
||||
local diff_start ---@type integer?
|
||||
if diff then
|
||||
if #lines > #head then
|
||||
table.insert(lines, "")
|
||||
end
|
||||
diff_start = #lines
|
||||
vim.list_extend(lines, diff)
|
||||
end
|
||||
vim.bo[pbuf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines)
|
||||
@@ -334,9 +456,32 @@ local function apply_popup(pbuf, win, head, body, sha_len, date_col)
|
||||
hl_group = "GitBlameDate",
|
||||
})
|
||||
end
|
||||
local width, height = size_for(lines)
|
||||
if diff_start and diff then
|
||||
if #diff == 1 and diff[1] == NEW_FILE_MARKER then
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
pbuf,
|
||||
NS_POPUP,
|
||||
diff_start,
|
||||
0,
|
||||
{ line_hl_group = "GitHunkAdded" }
|
||||
)
|
||||
else
|
||||
util.paint_diff_lines(pbuf, NS_POPUP, diff, diff_start)
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
pbuf,
|
||||
NS_POPUP,
|
||||
diff_start,
|
||||
0,
|
||||
{ line_hl_group = "GitHunkAnnotation" }
|
||||
)
|
||||
end
|
||||
end
|
||||
local width, height = popup.size_for(lines)
|
||||
pcall(vim.api.nvim_win_set_width, win, width)
|
||||
pcall(vim.api.nvim_win_set_height, win, height)
|
||||
popup.paint_overflow(pbuf, win, NS_OVERFLOW)
|
||||
end
|
||||
|
||||
---@param watch_buf integer
|
||||
@@ -362,18 +507,23 @@ local function setup_popup_autocmds(watch_buf, pbuf, win)
|
||||
pcall(vim.api.nvim_del_augroup_by_name, "ow.git.blame.popup")
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("WinScrolled", {
|
||||
group = group,
|
||||
pattern = tostring(win),
|
||||
callback = function()
|
||||
popup.paint_overflow(pbuf, win, NS_OVERFLOW)
|
||||
end,
|
||||
})
|
||||
vim.keymap.set("n", "q", close_popup, { buffer = pbuf, nowait = true })
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param commits table<string, ow.Git.Blame.Commit>
|
||||
---@param line_sha table<integer, string>
|
||||
---@param state ow.Git.Blame.BufState
|
||||
---@param lnum integer
|
||||
---@param watch_buf integer
|
||||
local function open_popup(r, commits, line_sha, lnum, watch_buf)
|
||||
local function open_popup(state, lnum, watch_buf)
|
||||
close_popup()
|
||||
local sha = line_sha[lnum]
|
||||
local commit = sha and commits[sha]
|
||||
local sha = state.line_sha[lnum]
|
||||
local commit = sha and state.commits[sha]
|
||||
if not commit then
|
||||
util.warning("git blame: no blame information for line %d", lnum)
|
||||
return
|
||||
@@ -393,15 +543,16 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
|
||||
"",
|
||||
}
|
||||
end
|
||||
local body = sha_len and { commit.summary } or nil
|
||||
local lines = {}
|
||||
vim.list_extend(lines, head)
|
||||
if body then
|
||||
vim.list_extend(lines, body)
|
||||
end
|
||||
local width, height = size_for(lines)
|
||||
local message = sha_len and { commit.summary } or nil ---@type string[]?
|
||||
local diff ---@type string[]?
|
||||
local pbuf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[pbuf].bufhidden = "wipe"
|
||||
local initial = {} ---@type string[]
|
||||
vim.list_extend(initial, head)
|
||||
if message then
|
||||
vim.list_extend(initial, message)
|
||||
end
|
||||
local width, height = popup.size_for(initial)
|
||||
local win = vim.api.nvim_open_win(pbuf, false, {
|
||||
relative = "cursor",
|
||||
row = 1,
|
||||
@@ -411,29 +562,37 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
|
||||
style = "minimal",
|
||||
})
|
||||
popup_win = win
|
||||
apply_popup(pbuf, win, head, body, sha_len, date_col)
|
||||
|
||||
local function redraw()
|
||||
apply_popup(pbuf, win, head, message, diff, sha_len, date_col)
|
||||
end
|
||||
|
||||
redraw()
|
||||
setup_popup_autocmds(watch_buf, pbuf, win)
|
||||
if not sha_len then
|
||||
return
|
||||
end
|
||||
util.git({ "show", "-s", "--format=%B", sha }, {
|
||||
cwd = r.worktree,
|
||||
silent = true,
|
||||
on_exit = function(res)
|
||||
local orig = state.line_orig[lnum]
|
||||
local rel = state.line_file[lnum] or state.rel
|
||||
if not orig then
|
||||
return
|
||||
end
|
||||
fetch_show(state.repo, sha, rel, orig, function(msg, hunk)
|
||||
if
|
||||
popup_win ~= win
|
||||
or not vim.api.nvim_win_is_valid(win)
|
||||
or not vim.api.nvim_buf_is_valid(pbuf)
|
||||
or res.code ~= 0
|
||||
then
|
||||
return
|
||||
end
|
||||
local msg = util.split_lines(res.stdout or "")
|
||||
if #msg > 0 then
|
||||
apply_popup(pbuf, win, head, msg, sha_len, date_col)
|
||||
if msg then
|
||||
message = msg
|
||||
end
|
||||
end,
|
||||
})
|
||||
if hunk then
|
||||
diff = hunk
|
||||
end
|
||||
redraw()
|
||||
end)
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
@@ -457,7 +616,7 @@ function M.line_popup(buf)
|
||||
then
|
||||
return
|
||||
end
|
||||
open_popup(state.repo, state.commits, state.line_sha, lnum, buf)
|
||||
open_popup(state, lnum, buf)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
local M = {}
|
||||
|
||||
---@param win integer
|
||||
---@return boolean
|
||||
local function has_border(win)
|
||||
local b = vim.api.nvim_win_get_config(win).border
|
||||
if not b or b == "none" or b == "" then
|
||||
return false
|
||||
end
|
||||
if type(b) == "table" and #b == 0 then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
---@param opts? { min_width?: integer, padding?: integer }
|
||||
---@return integer width
|
||||
---@return integer height
|
||||
function M.size_for(lines, opts)
|
||||
opts = opts or {}
|
||||
local width = 1
|
||||
for _, l in ipairs(lines) do
|
||||
local w = vim.api.nvim_strwidth(l)
|
||||
if w > width then
|
||||
width = w
|
||||
end
|
||||
end
|
||||
width = math.min(
|
||||
math.max(width + (opts.padding or 1), opts.min_width or 30),
|
||||
100,
|
||||
vim.o.columns - 4
|
||||
)
|
||||
local height =
|
||||
math.min(math.max(#lines, 1), 20, math.floor(vim.o.lines / 2))
|
||||
return width, height
|
||||
end
|
||||
|
||||
---@param pbuf integer
|
||||
---@param win integer
|
||||
---@param ns integer
|
||||
function M.paint_overflow(pbuf, win, ns)
|
||||
vim.schedule(function()
|
||||
if
|
||||
not vim.api.nvim_buf_is_valid(pbuf)
|
||||
or not vim.api.nvim_win_is_valid(win)
|
||||
then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_buf_clear_namespace(pbuf, ns, 0, -1)
|
||||
local total = vim.api.nvim_buf_line_count(pbuf)
|
||||
local top, bottom = unpack(vim.api.nvim_win_call(win, function()
|
||||
return { vim.fn.line("w0"), vim.fn.line("w$") }
|
||||
end)) ---@cast top integer
|
||||
---@cast bottom integer
|
||||
local needs_top = top > 1
|
||||
local needs_bottom = bottom < total
|
||||
if has_border(win) then
|
||||
pcall(vim.api.nvim_win_set_config, win, {
|
||||
title = needs_top and { { "▲ ", "FloatBorder" } } or "",
|
||||
title_pos = needs_top and "right" or nil,
|
||||
footer = needs_bottom and { { "▼ ", "FloatBorder" } } or "",
|
||||
footer_pos = needs_bottom and "right" or nil,
|
||||
})
|
||||
return
|
||||
end
|
||||
if needs_top then
|
||||
pcall(vim.api.nvim_buf_set_extmark, pbuf, ns, top - 1, 0, {
|
||||
virt_text = { { "▲ ", "GitPopupEnd" } },
|
||||
virt_text_pos = "right_align",
|
||||
})
|
||||
end
|
||||
if needs_bottom then
|
||||
pcall(vim.api.nvim_buf_set_extmark, pbuf, ns, bottom - 1, 0, {
|
||||
virt_text = { { "▼ ", "GitPopupEnd" } },
|
||||
virt_text_pos = "right_align",
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -120,6 +120,34 @@ function M.split_lines(content)
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param ns integer
|
||||
---@param lines string[]
|
||||
---@param start_row integer 0-indexed row of the first line in `lines`
|
||||
function M.paint_diff_lines(buf, ns, lines, start_row)
|
||||
for i, line in ipairs(lines) do
|
||||
local hl ---@type string?
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
hl = "GitHunkAdded"
|
||||
elseif prefix == "-" then
|
||||
hl = "GitHunkRemoved"
|
||||
elseif vim.startswith(line, "@@") then
|
||||
hl = "GitHunkHeader"
|
||||
end
|
||||
if hl then
|
||||
pcall(
|
||||
vim.api.nvim_buf_set_extmark,
|
||||
buf,
|
||||
ns,
|
||||
start_row + i - 1,
|
||||
0,
|
||||
{ line_hl_group = hl }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class ow.Git.Util.DebounceHandle
|
||||
---@field cancel fun()
|
||||
---@field flush fun()
|
||||
|
||||
+14
-9
@@ -1,3 +1,4 @@
|
||||
local popup = require("git.core.popup")
|
||||
local repo = require("git.core.repo")
|
||||
local util = require("git.core.util")
|
||||
|
||||
@@ -5,6 +6,9 @@ local M = {}
|
||||
|
||||
local NS_SIGNS = vim.api.nvim_create_namespace("ow.git.hunks")
|
||||
local NS_OVERLAY = vim.api.nvim_create_namespace("ow.git.hunks.overlay")
|
||||
local NS_PREVIEW = vim.api.nvim_create_namespace("ow.git.hunks.preview")
|
||||
local NS_PREVIEW_OVERFLOW =
|
||||
vim.api.nvim_create_namespace("ow.git.hunks.preview.overflow")
|
||||
|
||||
---@alias ow.Git.Hunks.HunkType "add"|"change"|"delete"
|
||||
|
||||
@@ -905,16 +909,9 @@ function M.preview_hunk(buf)
|
||||
local lines = hunk_body(h)
|
||||
local pbuf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines)
|
||||
vim.bo[pbuf].filetype = "diff"
|
||||
vim.bo[pbuf].bufhidden = "wipe"
|
||||
local width = 0
|
||||
for _, l in ipairs(lines) do
|
||||
if #l > width then
|
||||
width = #l
|
||||
end
|
||||
end
|
||||
width = math.min(math.max(width + 2, 40), vim.o.columns - 4)
|
||||
local height = math.min(#lines, math.floor(vim.o.lines / 2))
|
||||
util.paint_diff_lines(pbuf, NS_PREVIEW, lines, 0)
|
||||
local width, height = popup.size_for(lines, { min_width = 40, padding = 2 })
|
||||
local win = vim.api.nvim_open_win(pbuf, false, {
|
||||
relative = "cursor",
|
||||
row = 1,
|
||||
@@ -924,6 +921,7 @@ function M.preview_hunk(buf)
|
||||
style = "minimal",
|
||||
})
|
||||
preview_win = win
|
||||
popup.paint_overflow(pbuf, win, NS_PREVIEW_OVERFLOW)
|
||||
|
||||
local function close()
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
@@ -949,6 +947,13 @@ function M.preview_hunk(buf)
|
||||
pcall(vim.api.nvim_del_augroup_by_name, "ow.git.hunks.preview")
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("WinScrolled", {
|
||||
group = group,
|
||||
pattern = tostring(win),
|
||||
callback = function()
|
||||
popup.paint_overflow(pbuf, win, NS_PREVIEW_OVERFLOW)
|
||||
end,
|
||||
})
|
||||
vim.keymap.set("n", "q", close, { buffer = pbuf, nowait = true })
|
||||
end
|
||||
|
||||
|
||||
@@ -40,12 +40,16 @@ local DEFAULT_HIGHLIGHTS = {
|
||||
GitHunkAdded = "Added",
|
||||
GitHunkChanged = "Changed",
|
||||
GitHunkRemoved = "Removed",
|
||||
GitHunkHeader = "Statement",
|
||||
GitHunkAnnotation = "Title",
|
||||
GitHunkAddLine = "DiffAdd",
|
||||
GitHunkDeleteLine = "DiffDelete",
|
||||
|
||||
GitBlameAuthor = "GitAuthor",
|
||||
GitBlameDate = "GitDate",
|
||||
GitBlameSha = "GitSha",
|
||||
|
||||
GitPopupEnd = "NonText",
|
||||
}
|
||||
local STAGED_HUNK_HL = {
|
||||
GitHunkStagedAdded = "GitHunkAdded",
|
||||
|
||||
@@ -199,6 +199,226 @@ t.test("line popup shows the commit for the cursor line", function()
|
||||
t.truthy((lines[1] or ""):find("t", 1, true), "author shown")
|
||||
end)
|
||||
|
||||
t.test("line popup appends the commit's hunk for the cursor line", function()
|
||||
local dir = h.make_repo({ ["a.txt"] = "alpha\nbeta\ngamma\n" })
|
||||
t.write(dir, "a.txt", "alpha\nBETA\ngamma\n")
|
||||
h.git(dir, "add", "a.txt")
|
||||
h.git(dir, "commit", "-q", "-m", "change beta")
|
||||
vim.cmd.edit(dir .. "/a.txt")
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
blame.line_popup(buf)
|
||||
local float = wait_float()
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
t.wait_for(function()
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
for _, l in ipairs(lines) do
|
||||
if l:match("^@@") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end, "hunk header to appear in the blame popup")
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
local has = {} ---@type table<string, boolean>
|
||||
for _, l in ipairs(lines) do
|
||||
has[l] = true
|
||||
end
|
||||
t.truthy(has["-beta"], "popup includes the pre-image of the changed line")
|
||||
t.truthy(has["+BETA"], "popup includes the post-image of the changed line")
|
||||
t.truthy(
|
||||
has["Hunk 1 of 1"],
|
||||
"popup annotates the hunk index even when the commit only has one"
|
||||
)
|
||||
local pbuf = vim.api.nvim_win_get_buf(float)
|
||||
local seen = {} ---@type table<string, boolean>
|
||||
for _, m in
|
||||
ipairs(vim.api.nvim_buf_get_extmarks(pbuf, -1, 0, -1, {
|
||||
details = true,
|
||||
}))
|
||||
do
|
||||
local hl = m[4] and m[4].line_hl_group
|
||||
if hl then
|
||||
seen[hl] = true
|
||||
end
|
||||
end
|
||||
t.truthy(seen["GitHunkAdded"], "+ line gets GitHunkAdded")
|
||||
t.truthy(seen["GitHunkRemoved"], "- line gets GitHunkRemoved")
|
||||
t.truthy(seen["GitHunkHeader"], "@@ header gets GitHunkHeader")
|
||||
t.truthy(
|
||||
seen["GitHunkAnnotation"],
|
||||
"hunk-index annotation gets GitHunkAnnotation"
|
||||
)
|
||||
end)
|
||||
|
||||
t.test(
|
||||
"line popup picks the hunk that contains the cursor, not the first one",
|
||||
function()
|
||||
local lines_initial = {}
|
||||
local lines_changed = {}
|
||||
for i = 1, 12 do
|
||||
table.insert(lines_initial, "line" .. i)
|
||||
table.insert(lines_changed, "line" .. i)
|
||||
end
|
||||
lines_changed[2] = "FIRST"
|
||||
lines_changed[10] = "SECOND"
|
||||
local initial = table.concat(lines_initial, "\n") .. "\n"
|
||||
local changed = table.concat(lines_changed, "\n") .. "\n"
|
||||
local dir = h.make_repo({ ["a.txt"] = initial })
|
||||
t.write(dir, "a.txt", changed)
|
||||
h.git(dir, "add", "a.txt")
|
||||
h.git(dir, "commit", "-q", "-m", "change line 2 and line 10")
|
||||
vim.cmd.edit(dir .. "/a.txt")
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_win_set_cursor(0, { 10, 0 })
|
||||
blame.line_popup(buf)
|
||||
local float = wait_float()
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
t.wait_for(function()
|
||||
for _, l in
|
||||
ipairs(
|
||||
vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
)
|
||||
do
|
||||
if l == "+SECOND" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end, "popup to include the second hunk's added line")
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
local has = {} ---@type table<string, boolean>
|
||||
for _, l in ipairs(lines) do
|
||||
has[l] = true
|
||||
end
|
||||
t.truthy(has["-line10"], "popup includes second hunk pre-image")
|
||||
t.truthy(has["+SECOND"], "popup includes second hunk post-image")
|
||||
t.falsy(has["-line2"], "popup omits the first hunk's pre-image")
|
||||
t.falsy(has["+FIRST"], "popup omits the first hunk's post-image")
|
||||
t.truthy(
|
||||
has["Hunk 2 of 2"],
|
||||
"popup annotates which hunk is selected and how many total"
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
t.test(
|
||||
"line popup finds the diff via the historical filename after a rename",
|
||||
function()
|
||||
local dir = h.make_repo({ ["old.txt"] = "alpha\nbeta\n" })
|
||||
t.write(dir, "old.txt", "ALPHA\nbeta\n")
|
||||
h.git(dir, "add", "old.txt")
|
||||
h.git(dir, "commit", "-q", "-m", "change line 1")
|
||||
h.git(dir, "mv", "old.txt", "new.txt")
|
||||
h.git(dir, "commit", "-q", "-m", "rename")
|
||||
vim.cmd.edit(dir .. "/new.txt")
|
||||
local buf = vim.api.nvim_get_current_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)
|
||||
t.wait_for(function()
|
||||
for _, l in
|
||||
ipairs(
|
||||
vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
)
|
||||
do
|
||||
if l == "+ALPHA" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end, "popup to include the pre-rename commit's diff via old.txt")
|
||||
end
|
||||
)
|
||||
|
||||
t.test("line popup collapses a new-file diff to a single marker", function()
|
||||
local dir = h.make_repo({ ["a.txt"] = "alpha\nbeta\ngamma\n" })
|
||||
vim.cmd.edit(dir .. "/a.txt")
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
blame.line_popup(buf)
|
||||
local float = wait_float()
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
t.wait_for(function()
|
||||
for _, l in
|
||||
ipairs(
|
||||
vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
)
|
||||
do
|
||||
if l == "(new file)" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end, "popup to show the new-file marker")
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
for _, l in ipairs(lines) do
|
||||
t.falsy(
|
||||
vim.startswith(l, "+alpha")
|
||||
or vim.startswith(l, "+beta")
|
||||
or vim.startswith(l, "+gamma"),
|
||||
"popup does not dump the file content as + lines"
|
||||
)
|
||||
end
|
||||
local pbuf = vim.api.nvim_win_get_buf(float)
|
||||
local seen = {} ---@type table<string, boolean>
|
||||
for _, m in
|
||||
ipairs(vim.api.nvim_buf_get_extmarks(pbuf, -1, 0, -1, {
|
||||
details = true,
|
||||
}))
|
||||
do
|
||||
local hl = m[4] and m[4].line_hl_group
|
||||
if hl then
|
||||
seen[hl] = true
|
||||
end
|
||||
end
|
||||
t.truthy(seen["GitHunkAdded"], "new-file marker gets GitHunkAdded")
|
||||
end)
|
||||
|
||||
t.test("re-invoking the line popup focuses the open float", function()
|
||||
local _, buf = setup("alpha\nbeta\n")
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
|
||||
@@ -608,6 +608,32 @@ t.test("preview_hunk shows the hunk body without file headers", function()
|
||||
end
|
||||
end)
|
||||
|
||||
t.test("preview_hunk highlights diff lines via per-line extmarks", function()
|
||||
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
hunks.preview_hunk(buf)
|
||||
local float = assert(find_float(), "preview float should open")
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
local pbuf = vim.api.nvim_win_get_buf(float)
|
||||
local seen = {} ---@type table<string, boolean>
|
||||
for _, m in
|
||||
ipairs(vim.api.nvim_buf_get_extmarks(pbuf, -1, 0, -1, {
|
||||
details = true,
|
||||
}))
|
||||
do
|
||||
local hl = m[4] and m[4].line_hl_group
|
||||
if hl then
|
||||
seen[hl] = true
|
||||
end
|
||||
end
|
||||
t.truthy(seen["GitHunkHeader"], "@@ header gets GitHunkHeader")
|
||||
t.truthy(seen["GitHunkAdded"], "+ line gets GitHunkAdded")
|
||||
t.truthy(seen["GitHunkRemoved"], "- line gets GitHunkRemoved")
|
||||
end)
|
||||
|
||||
t.test("preview_hunk re-invocation focuses the open float", function()
|
||||
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
|
||||
Reference in New Issue
Block a user