Compare commits

..

2 Commits

Author SHA1 Message Date
warg 20be779891 feat(log): add gitlog syntax file 2026-05-26 23:25:19 +02:00
warg ff7b20ec46 feat(blame): show commit's hunk in the line popup 2026-05-26 23:25:09 +02:00
8 changed files with 615 additions and 65 deletions
+207 -48
View File
@@ -1,10 +1,12 @@
local object = require("git.object") local object = require("git.object")
local popup = require("git.core.popup")
local repo = require("git.core.repo") local repo = require("git.core.repo")
local util = require("git.core.util") local util = require("git.core.util")
local M = {} local M = {}
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_OVERFLOW = vim.api.nvim_create_namespace("ow.git.blame.popup.overflow")
local ZERO_SHA = string.rep("0", 40) local ZERO_SHA = string.rep("0", 40)
@@ -19,6 +21,8 @@ local ZERO_SHA = string.rep("0", 40)
---@class ow.Git.Blame.Result ---@class ow.Git.Blame.Result
---@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 line_orig table<integer, integer>
---@field line_file table<integer, string>
---@class ow.Git.Blame.Source ---@class ow.Git.Blame.Source
---@field repo ow.Git.Repo ---@field repo ow.Git.Repo
@@ -31,6 +35,8 @@ local ZERO_SHA = string.rep("0", 40)
---@field revision string? ---@field revision string?
---@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 line_orig table<integer, integer>
---@field line_file table<integer, string>
---@field tick integer? ---@field tick integer?
---@field epoch integer ---@field epoch integer
---@field pending fun()[] ---@field pending fun()[]
@@ -72,20 +78,33 @@ local function parse_porcelain(stdout)
local commits = {} local commits = {}
---@type table<integer, string> ---@type table<integer, string>
local line_sha = {} local line_sha = {}
---@type table<integer, integer>
local line_orig = {}
---@type table<integer, string>
local line_file = {}
local cur_sha ---@type string? local cur_sha ---@type string?
local cur_lnum ---@type integer? local cur_lnum ---@type integer?
local cur_orig ---@type integer?
local cur_file ---@type string?
for _, line in ipairs(util.split_lines(stdout)) do for _, line in ipairs(util.split_lines(stdout)) do
if line:sub(1, 1) == "\t" then 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_sha[cur_lnum] = cur_sha
line_orig[cur_lnum] = cur_orig
if cur_file then
line_file[cur_lnum] = cur_file
end
end end
cur_sha = nil cur_sha = nil
cur_lnum = nil cur_lnum = nil
cur_orig = nil
cur_file = nil
else else
local sha, final = line:match("^(%x+) %d+ (%d+)") local sha, orig, final = line:match("^(%x+) (%d+) (%d+)")
if sha and #sha >= 40 then if sha and #sha >= 40 then
cur_sha = sha 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 if not commits[sha] then
commits[sha] = { commits[sha] = {
sha = sha, sha = sha,
@@ -99,7 +118,9 @@ local function parse_porcelain(stdout)
else else
local key, value = line:match("^(%S+) (.*)$") local key, value = line:match("^(%S+) (.*)$")
local commit = cur_sha and commits[cur_sha] 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 if key == "author" then
commit.author = value commit.author = value
elseif key == "author-mail" then elseif key == "author-mail" then
@@ -115,7 +136,12 @@ local function parse_porcelain(stdout)
end end
end 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 end
---@param line_count integer ---@param line_count integer
@@ -138,6 +164,8 @@ local function synth_uncommitted(line_count)
}, },
}, },
line_sha = line_sha, line_sha = line_sha,
line_orig = {},
line_file = {},
} }
end end
@@ -219,6 +247,8 @@ local function ensure_state(buf)
revision = src.revision, revision = src.revision,
commits = {}, commits = {},
line_sha = {}, line_sha = {},
line_orig = {},
line_file = {},
tick = nil, tick = nil,
epoch = 0, epoch = 0,
pending = {}, pending = {},
@@ -266,6 +296,8 @@ local function run_blame(state, buf, done)
or synth_uncommitted(vim.api.nvim_buf_line_count(buf)) or synth_uncommitted(vim.api.nvim_buf_line_count(buf))
state.commits = data.commits state.commits = data.commits
state.line_sha = data.line_sha state.line_sha = data.line_sha
state.line_orig = data.line_orig
state.line_file = data.line_file
state.tick = tick state.tick = tick
local pending = state.pending local pending = state.pending
state.pending = {} state.pending = {}
@@ -275,20 +307,101 @@ local function run_blame(state, buf, done)
end) end)
end end
---@param lines string[] local NEW_FILE_MARKER = "(new file)"
---@return integer width
---@return integer height ---@param diff_text string
local function size_for(lines) ---@param orig_line integer
local width = 1 ---@return string[]?
for _, l in ipairs(lines) do local function extract_commit_hunk(diff_text, orig_line)
local w = vim.api.nvim_strwidth(l) local lines = util.split_lines(diff_text)
if w > width then for _, line in ipairs(lines) do
width = w if line == "--- /dev/null" then
return { NEW_FILE_MARKER }
end end
end end
width = math.min(math.max(width + 1, 30), vim.o.columns - 4) local hunk_starts = {} ---@type integer[]
local height = math.min(math.max(#lines, 1), math.floor(vim.o.lines / 2)) local target ---@type integer?
return width, height 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 end
local popup_win ---@type integer? local popup_win ---@type integer?
@@ -303,14 +416,23 @@ end
---@param pbuf integer ---@param pbuf integer
---@param win integer ---@param win integer
---@param head string[] ---@param head string[]
---@param body string[]? ---@param message string[]?
---@param diff string[]?
---@param sha_len integer? ---@param sha_len integer?
---@param date_col integer? ---@param date_col integer?
local function apply_popup(pbuf, win, head, body, sha_len, date_col) local function apply_popup(pbuf, win, head, message, diff, sha_len, date_col)
local lines = {} local lines = {} ---@type string[]
vim.list_extend(lines, head) vim.list_extend(lines, head)
if body then if message then
vim.list_extend(lines, body) 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 end
vim.bo[pbuf].modifiable = true vim.bo[pbuf].modifiable = true
vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines) 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", hl_group = "GitBlameDate",
}) })
end 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_width, win, width)
pcall(vim.api.nvim_win_set_height, win, height) pcall(vim.api.nvim_win_set_height, win, height)
popup.paint_overflow(pbuf, win, NS_OVERFLOW)
end end
---@param watch_buf integer ---@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") pcall(vim.api.nvim_del_augroup_by_name, "ow.git.blame.popup")
end, 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 }) vim.keymap.set("n", "q", close_popup, { buffer = pbuf, nowait = true })
end end
---@param r ow.Git.Repo ---@param state ow.Git.Blame.BufState
---@param commits table<string, ow.Git.Blame.Commit>
---@param line_sha table<integer, string>
---@param lnum integer ---@param lnum integer
---@param watch_buf 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() close_popup()
local sha = line_sha[lnum] local sha = state.line_sha[lnum]
local commit = sha and commits[sha] local commit = sha and state.commits[sha]
if not commit then if not commit then
util.warning("git blame: no blame information for line %d", lnum) util.warning("git blame: no blame information for line %d", lnum)
return return
@@ -393,15 +543,16 @@ local function open_popup(r, commits, line_sha, lnum, watch_buf)
"", "",
} }
end end
local body = sha_len and { commit.summary } or nil local message = sha_len and { commit.summary } or nil ---@type string[]?
local lines = {} local diff ---@type string[]?
vim.list_extend(lines, head)
if body then
vim.list_extend(lines, body)
end
local width, height = size_for(lines)
local pbuf = vim.api.nvim_create_buf(false, true) local pbuf = vim.api.nvim_create_buf(false, true)
vim.bo[pbuf].bufhidden = "wipe" 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, { local win = vim.api.nvim_open_win(pbuf, false, {
relative = "cursor", relative = "cursor",
row = 1, row = 1,
@@ -411,29 +562,37 @@ 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, 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) setup_popup_autocmds(watch_buf, pbuf, win)
if not sha_len then if not sha_len then
return return
end end
util.git({ "show", "-s", "--format=%B", sha }, { local orig = state.line_orig[lnum]
cwd = r.worktree, local rel = state.line_file[lnum] or state.rel
silent = true, if not orig then
on_exit = function(res) return
end
fetch_show(state.repo, sha, rel, orig, function(msg, hunk)
if if
popup_win ~= win popup_win ~= win
or not vim.api.nvim_win_is_valid(win) or not vim.api.nvim_win_is_valid(win)
or not vim.api.nvim_buf_is_valid(pbuf) or not vim.api.nvim_buf_is_valid(pbuf)
or res.code ~= 0
then then
return return
end end
local msg = util.split_lines(res.stdout or "") if msg then
if #msg > 0 then message = msg
apply_popup(pbuf, win, head, msg, sha_len, date_col)
end end
end, if hunk then
}) diff = hunk
end
redraw()
end)
end end
---@param buf integer? ---@param buf integer?
@@ -457,7 +616,7 @@ function M.line_popup(buf)
then then
return return
end end
open_popup(state.repo, state.commits, state.line_sha, lnum, buf) open_popup(state, lnum, buf)
end) end)
end end
+82
View File
@@ -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
+28
View File
@@ -120,6 +120,34 @@ function M.split_lines(content)
return lines return lines
end 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 ---@class ow.Git.Util.DebounceHandle
---@field cancel fun() ---@field cancel fun()
---@field flush fun() ---@field flush fun()
+14 -9
View File
@@ -1,3 +1,4 @@
local popup = require("git.core.popup")
local repo = require("git.core.repo") local repo = require("git.core.repo")
local util = require("git.core.util") 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_SIGNS = vim.api.nvim_create_namespace("ow.git.hunks")
local NS_OVERLAY = vim.api.nvim_create_namespace("ow.git.hunks.overlay") 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" ---@alias ow.Git.Hunks.HunkType "add"|"change"|"delete"
@@ -905,16 +909,9 @@ function M.preview_hunk(buf)
local lines = hunk_body(h) local lines = hunk_body(h)
local pbuf = vim.api.nvim_create_buf(false, true) local pbuf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines) vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines)
vim.bo[pbuf].filetype = "diff"
vim.bo[pbuf].bufhidden = "wipe" vim.bo[pbuf].bufhidden = "wipe"
local width = 0 util.paint_diff_lines(pbuf, NS_PREVIEW, lines, 0)
for _, l in ipairs(lines) do local width, height = popup.size_for(lines, { min_width = 40, padding = 2 })
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))
local win = vim.api.nvim_open_win(pbuf, false, { local win = vim.api.nvim_open_win(pbuf, false, {
relative = "cursor", relative = "cursor",
row = 1, row = 1,
@@ -924,6 +921,7 @@ function M.preview_hunk(buf)
style = "minimal", style = "minimal",
}) })
preview_win = win preview_win = win
popup.paint_overflow(pbuf, win, NS_PREVIEW_OVERFLOW)
local function close() local function close()
if vim.api.nvim_win_is_valid(win) then 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") pcall(vim.api.nvim_del_augroup_by_name, "ow.git.hunks.preview")
end, 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 }) vim.keymap.set("n", "q", close, { buffer = pbuf, nowait = true })
end end
+4
View File
@@ -40,12 +40,16 @@ local DEFAULT_HIGHLIGHTS = {
GitHunkAdded = "Added", GitHunkAdded = "Added",
GitHunkChanged = "Changed", GitHunkChanged = "Changed",
GitHunkRemoved = "Removed", GitHunkRemoved = "Removed",
GitHunkHeader = "Statement",
GitHunkAnnotation = "Title",
GitHunkAddLine = "DiffAdd", GitHunkAddLine = "DiffAdd",
GitHunkDeleteLine = "DiffDelete", GitHunkDeleteLine = "DiffDelete",
GitBlameAuthor = "GitAuthor", GitBlameAuthor = "GitAuthor",
GitBlameDate = "GitDate", GitBlameDate = "GitDate",
GitBlameSha = "GitSha", GitBlameSha = "GitSha",
GitPopupEnd = "NonText",
} }
local STAGED_HUNK_HL = { local STAGED_HUNK_HL = {
GitHunkStagedAdded = "GitHunkAdded", GitHunkStagedAdded = "GitHunkAdded",
+26
View File
@@ -0,0 +1,26 @@
if exists("b:current_syntax")
finish
endif
syntax match gitlogGraph contained /^[*|\\\/_ ]*/
\ nextgroup=gitlogHash
syntax match gitlogHash contained /\<\x\{7,64\}\>/
\ nextgroup=gitlogDate skipwhite
syntax match gitlogDate contained /\<\d\{4}-\d\{2}-\d\{2}\>/
\ nextgroup=gitlogAuthor skipwhite
syntax match gitlogAuthor contained /{[^}]\+}/
\ nextgroup=gitlogRef skipwhite
syntax match gitlogRef contained /([^)]\+)/
syntax match gitlogLine
\ /^[*|\\\/_ ]*\x\{7,64}\s\+\d\{4}-\d\{2}-\d\{2}\s\+{[^}]\+}.*/
\ contains=gitlogGraph
syntax match gitlogGraphLine /^[*|\\\/_ ]\+$/
\ contains=gitlogGraph
highlight default link gitlogGraph Comment
highlight default link gitlogHash GitSha
highlight default link gitlogDate GitDate
highlight default link gitlogAuthor GitAuthor
highlight default link gitlogRef Constant
let b:current_syntax = "gitlog"
+220
View File
@@ -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") t.truthy((lines[1] or ""):find("t", 1, true), "author shown")
end) 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() t.test("re-invoking the line popup focuses the open float", function()
local _, buf = setup("alpha\nbeta\n") local _, buf = setup("alpha\nbeta\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
+26
View File
@@ -608,6 +608,32 @@ t.test("preview_hunk shows the hunk body without file headers", function()
end end
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() t.test("preview_hunk re-invocation focuses the open float", function()
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n") local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)