Files
nvim/test/git/hunks_test.lua

422 lines
14 KiB
Lua

local h = require("test.git.helpers")
local hunks = require("git.hunks")
local t = require("test")
---@param committed string
---@param worktree string
---@param file string?
---@return string dir
---@return integer buf
---@return ow.Git.Hunks.BufState state
local function setup(committed, worktree, file)
file = file or "a.txt"
local dir = h.make_repo({ [file] = committed })
t.write(dir, file, worktree)
vim.cmd.edit(dir .. "/" .. file)
local buf = vim.api.nvim_get_current_buf()
hunks.attach(buf)
hunks._flush(buf)
t.wait_for(function()
local s = hunks.state(buf)
return s ~= nil and s.index ~= nil
end, "hunks to compute the index snapshot")
local state = assert(hunks.state(buf), "buffer state should exist")
return dir, buf, state
end
---@param buf integer
---@return { row: integer, sign: string, hl: string }[]
local function sign_marks(buf)
local ns = vim.api.nvim_get_namespaces()["ow.git.hunks"]
local out = {}
for _, m in ipairs(vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, {
details = true,
})) do
local d = assert(m[4])
table.insert(out, {
row = m[2],
sign = vim.trim(d.sign_text or ""),
hl = d.sign_hl_group,
})
end
table.sort(out, function(a, b)
return a.row < b.row
end)
return out
end
---@param buf integer
---@param ns_name string
---@return vim.api.keyset.get_extmark_item[]
local function detailed_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
---@return integer?
local function find_float()
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.api.nvim_win_get_config(w).relative ~= "" then
return w
end
end
end
t.test("pure add: hunk shape and add signs", function()
local _, buf, state = setup("a\nd\n", "a\nb\nc\nd\n")
t.eq(#state.hunks, 1, "one hunk for a pure addition")
local hk = assert(state.hunks[1])
t.eq(hk.type, "add")
t.eq(hk.new_start, 2)
t.eq(hk.new_count, 2)
t.eq(sign_marks(buf), {
{ row = 1, sign = "", hl = "GitHunkAdded" },
{ row = 2, sign = "", hl = "GitHunkAdded" },
})
end)
t.test("pure delete (middle): hunk shape and delete sign", function()
local _, buf, state = setup("a\nb\nc\n", "a\nc\n")
t.eq(#state.hunks, 1)
local hk = assert(state.hunks[1])
t.eq(hk.type, "delete")
t.eq(hk.new_count, 0)
t.eq(hk.old_lines, { "b" })
t.eq(sign_marks(buf), {
{ row = 0, sign = "", hl = "GitHunkRemoved" },
})
end)
t.test("top-of-file delete: sign anchors on line 1", function()
local _, buf, state = setup("a\nb\nc\n", "b\nc\n")
t.eq(#state.hunks, 1)
local hk = assert(state.hunks[1])
t.eq(hk.type, "delete")
t.eq(hk.new_start, 0)
t.eq(hk.old_lines, { "a" })
t.eq(sign_marks(buf), {
{ row = 0, sign = "", hl = "GitHunkRemoved" },
})
end)
t.test("change of N lines: hunk shape and change signs", function()
local _, buf, state = setup("a\nb\nc\nd\n", "a\nB\nC\nd\n")
t.eq(#state.hunks, 1)
local hk = assert(state.hunks[1])
t.eq(hk.type, "change")
t.eq(hk.old_start, 2)
t.eq(hk.old_count, 2)
t.eq(hk.new_start, 2)
t.eq(hk.new_count, 2)
t.eq(hk.old_lines, { "b", "c" })
t.eq(hk.new_lines, { "B", "C" })
t.eq(sign_marks(buf), {
{ row = 1, sign = "", hl = "GitHunkChanged" },
{ row = 2, sign = "", hl = "GitHunkChanged" },
})
end)
t.test("multi-hunk file: two separate change hunks", function()
local _, buf, state = setup("a\nb\nc\nd\ne\n", "A\nb\nc\nd\nE\n")
t.eq(#state.hunks, 2, "two hunks for two disjoint changes")
t.eq(sign_marks(buf), {
{ row = 0, sign = "", hl = "GitHunkChanged" },
{ row = 4, sign = "", hl = "GitHunkChanged" },
})
end)
t.test("clean file produces no hunks or signs", function()
local _, buf, state = setup("a\nb\nc\n", "a\nb\nc\n")
t.eq(#state.hunks, 0)
t.eq(sign_marks(buf), {})
end)
t.test("editing the buffer refreshes signs", function()
local _, buf, state = setup("a\nb\nc\n", "a\nb\nc\n")
t.eq(#state.hunks, 0)
vim.api.nvim_buf_set_lines(buf, 1, 2, false, { "CHANGED" })
vim.api.nvim_exec_autocmds("TextChanged", { buffer = buf })
hunks._flush(buf)
t.wait_for(function()
local s = assert(hunks.state(buf))
return #s.hunks == 1
end, "hunks to pick up the in-buffer edit")
local hk = assert(assert(hunks.state(buf)).hunks[1])
t.eq(hk.type, "change")
end)
t.test("overlay: change hunk shows deletion and addition", function()
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
hunks.toggle_overlay(buf)
---@type integer?
local add_row
---@type vim.api.keyset.extmark_details?
local add_d
---@type vim.api.keyset.extmark_details?
local virt_d
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
local d = assert(m[4])
if d.line_hl_group then
add_row, add_d = m[2], d
elseif d.virt_lines then
virt_d = d
end
end
add_d = assert(add_d, "the added line should get a line highlight")
t.eq(add_row, 1, "addition highlighted on the changed line")
t.eq(add_d.line_hl_group, "GitHunkAddLine")
virt_d = assert(virt_d, "the deletion should render as virtual lines")
local piece = assert(assert(assert(virt_d.virt_lines)[1])[1])
t.truthy(vim.startswith(piece[1], "b"), "deleted line shows the old content")
t.eq(piece[2], "GitHunkDeleteLine")
end)
t.test("overlay: delete hunk shows only deletion lines", function()
local _, buf = setup("a\nb\nc\n", "a\nc\n")
hunks.toggle_overlay(buf)
local marks = detailed_marks(buf, "ow.git.hunks.overlay")
t.eq(#marks, 1, "a pure delete has no addition highlight")
local d = assert(assert(marks[1])[4])
local piece = assert(assert(assert(d.virt_lines)[1])[1])
t.truthy(vim.startswith(piece[1], "b"))
t.eq(piece[2], "GitHunkDeleteLine")
end)
t.test("overlay: add hunk highlights the added lines", function()
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
hunks.toggle_overlay(buf)
local rows = {}
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
local d = assert(m[4])
t.falsy(d.virt_lines, "a pure add has no deletion virtual lines")
t.eq(d.line_hl_group, "GitHunkAddLine")
table.insert(rows, m[2])
end
table.sort(rows)
t.eq(rows, { 1, 2 }, "both added lines highlighted")
end)
t.test("overlay: deleted lines are treesitter-highlighted", function()
local _, buf = setup(
"-- a note\nlocal x = 1\nlocal y = 2\n",
"local y = 2\n",
"a.lua"
)
t.truthy(
pcall(vim.treesitter.start, buf, "lua"),
"the lua parser should be available"
)
hunks.toggle_overlay(buf)
---@type table[]?
local virt
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
local d = assert(m[4])
if d.virt_lines then
virt = d.virt_lines
end
end
virt = assert(virt, "a deletion virtual line should render")
---@type table<string, boolean>
local seen = {}
for _, line in ipairs(virt) do
for _, c in ipairs(line) do
local hl = c[2]
if
type(hl) == "table"
and hl[1] == "GitHunkDeleteLine"
and hl[2]
then
seen[hl[2]] = true
end
end
end
t.truthy(seen["@comment"], "the deleted comment keeps its @comment group")
t.truthy(seen["@keyword"], "deleted code keeps its syntax groups")
end)
t.test("overlay: toggling swaps gutter signs for the overlay", function()
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
t.truthy(
#detailed_marks(buf, "ow.git.hunks") > 0,
"gutter signs present while the overlay is off"
)
t.eq(#detailed_marks(buf, "ow.git.hunks.overlay"), 0)
hunks.toggle_overlay(buf)
t.truthy(
#detailed_marks(buf, "ow.git.hunks.overlay") > 0,
"overlay present once it is on"
)
t.eq(
#detailed_marks(buf, "ow.git.hunks"),
0,
"gutter signs replaced while the overlay is on"
)
hunks.toggle_overlay(buf)
t.eq(#detailed_marks(buf, "ow.git.hunks.overlay"), 0)
t.truthy(
#detailed_marks(buf, "ow.git.hunks") > 0,
"gutter signs restored after toggling the overlay off"
)
end)
t.test("stage_hunk stages the change into the index", function()
local dir, buf = setup("a\nb\nc\n", "a\nB\nc\n")
vim.api.nvim_win_set_cursor(0, { 2, 0 })
hunks.stage_hunk(buf)
t.wait_for(function()
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
end, "stage to land in the index")
t.eq(h.git(dir, "diff", "--cached", "--name-only").stdout, "a.txt")
t.eq(
h.git(dir, "show", ":0:a.txt").stdout,
"a\nB\nc",
"index blob reflects the staged change"
)
end)
t.test("stage_hunk stages a pure addition", function()
local dir, buf = setup("a\nb\n", "a\nb\nc\n")
vim.api.nvim_win_set_cursor(0, { 3, 0 })
hunks.stage_hunk(buf)
t.wait_for(function()
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
end, "stage to land in the index")
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "a\nb\nc")
end)
t.test("stage_hunk stages a deletion", function()
local dir, buf = setup("a\nb\nc\n", "a\nc\n")
vim.api.nvim_win_set_cursor(0, { 1, 0 })
hunks.stage_hunk(buf)
t.wait_for(function()
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
end, "stage to land in the index")
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "a\nc")
end)
t.test("stage_hunk refreshes the gutter when status stays modified", function()
local _, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\nE\n")
t.eq(#assert(hunks.state(buf)).hunks, 3)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
hunks.stage_hunk(buf)
t.wait_for(function()
return #assert(hunks.state(buf)).hunks == 2
end, "gutter to drop the first staged hunk")
vim.api.nvim_win_set_cursor(0, { 3, 0 })
hunks.stage_hunk(buf)
t.wait_for(function()
return #assert(hunks.state(buf)).hunks == 1
end, "gutter to drop the middle staged hunk")
end)
t.test("reset_hunk restores the index content for a change", function()
local _, buf, state = setup("a\nb\nc\n", "a\nB\nc\n")
vim.api.nvim_win_set_cursor(0, { 2, 0 })
hunks.reset_hunk(buf)
t.eq(
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
state.index,
"buffer matches the index after reset"
)
end)
t.test("reset_hunk re-inserts deleted lines", function()
local _, buf = setup("a\nb\nc\n", "a\nc\n")
vim.api.nvim_win_set_cursor(0, { 1, 0 })
hunks.reset_hunk(buf)
t.eq(vim.api.nvim_buf_get_lines(buf, 0, -1, false), { "a", "b", "c" })
end)
t.test("reset_hunk removes a pure addition", function()
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
vim.api.nvim_win_set_cursor(0, { 2, 0 })
hunks.reset_hunk(buf)
t.eq(vim.api.nvim_buf_get_lines(buf, 0, -1, false), { "a", "d" })
end)
t.test("git_hunk_signs overrides the sign character per kind", function()
local prev = vim.g.git_hunk_signs
vim.g.git_hunk_signs = { change = "C" }
t.defer(function()
vim.g.git_hunk_signs = prev
end)
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
t.eq(sign_marks(buf), {
{ row = 1, sign = "C", hl = "GitHunkChanged" },
})
end)
t.test("git_hunk_signs falls back to the default for unset kinds", function()
local prev = vim.g.git_hunk_signs
vim.g.git_hunk_signs = { add = "A" }
t.defer(function()
vim.g.git_hunk_signs = prev
end)
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
t.eq(sign_marks(buf), {
{ row = 1, sign = "", hl = "GitHunkChanged" },
})
end)
t.test("preview_hunk shows the hunk body without file headers", 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 lines = vim.api.nvim_buf_get_lines(
vim.api.nvim_win_get_buf(float),
0,
-1,
false
)
t.truthy(
vim.startswith(lines[1] or "", "@@ "),
"first line is the @@ header"
)
for _, l in ipairs(lines) do
t.falsy(vim.startswith(l, "--- "), "no --- file header line")
t.falsy(vim.startswith(l, "+++ "), "no +++ file header line")
end
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)
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)
t.truthy(
vim.api.nvim_get_current_win() ~= float,
"the float opens unfocused"
)
hunks.preview_hunk(buf)
t.eq(
vim.api.nvim_get_current_win(),
float,
"re-invoking focuses the existing float"
)
end)
t.test("nav jumps to next and previous hunks with wrap", function()
local _, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nc\nd\nE\n")
vim.api.nvim_set_current_buf(buf)
vim.api.nvim_win_set_cursor(0, { 1, 0 })
hunks.nav("next")
t.eq(vim.api.nvim_win_get_cursor(0)[1], 5, "next hunk is line 5")
hunks.nav("next")
t.eq(vim.api.nvim_win_get_cursor(0)[1], 1, "next wraps back to line 1")
hunks.nav("prev")
t.eq(vim.api.nvim_win_get_cursor(0)[1], 5, "prev wraps back to line 5")
end)