405 lines
13 KiB
Lua
405 lines
13 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("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)
|