feat(git): add in-house hunks module, replace gitsigns.nvim
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
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)
|
||||
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
|
||||
|
||||
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 = "GitHunkAdd" },
|
||||
{ row = 2, sign = "┃", hl = "GitHunkAdd" },
|
||||
})
|
||||
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 = "GitHunkDelete" },
|
||||
})
|
||||
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 = "GitHunkDelete" },
|
||||
})
|
||||
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 = "GitHunkChange" },
|
||||
{ row = 2, sign = "┃", hl = "GitHunkChange" },
|
||||
})
|
||||
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 = "GitHunkChange" },
|
||||
{ row = 4, sign = "┃", hl = "GitHunkChange" },
|
||||
})
|
||||
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 })
|
||||
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: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 = "GitHunkChange" },
|
||||
})
|
||||
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 = "GitHunkChange" },
|
||||
})
|
||||
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)
|
||||
@@ -277,7 +277,7 @@ t.test(
|
||||
error("a non-sidebar window should remain after close")
|
||||
end
|
||||
vim.api.nvim_set_current_win(remaining)
|
||||
require("git.diff").split({ mods = { vertical = true } })
|
||||
require("git.diffsplit").open({ mods = { vertical = true } })
|
||||
t.wait_for(function()
|
||||
local count = 0
|
||||
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
|
||||
Reference in New Issue
Block a user