local h = require("test.git.helpers") local t = require("test") ---Replicate the user's global cursor-restore autocmd. Scoped to a ---named augroup + cleanup so it doesn't leak between tests. local function install_cursor_restore_autocmd() local group = vim.api.nvim_create_augroup("test.cursor_restore", { clear = true }) vim.api.nvim_create_autocmd("BufReadPost", { group = group, pattern = "*", command = 'silent! normal! g`"zv', }) t.defer(function() pcall(vim.api.nvim_del_augroup_by_name, "test.cursor_restore") end) end ---@param sidebar_buf integer ---@param needle string ---@return integer? local function find_line(sidebar_buf, needle) for i, l in ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false)) do if l:match(needle) then return i end end end ---Find the gitstatus sidebar window in the current tabpage. ---@return integer? sidebar_buf ---@return integer? sidebar_win local function find_sidebar() for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do local b = vim.api.nvim_win_get_buf(w) if vim.bo[b].filetype == "gitstatus" then return b, w end end end ---Find a diff window in the given tabpage (or current). "left" / "right" ---is determined by column position: the layout is [sidebar | left | right], ---so the leftmost &diff window is the left pane and the rightmost is the ---right pane. ---@param role "left"|"right" ---@param tab integer? ---@return integer? local function find_diff_win(role, tab) local diffs = {} for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tab or 0)) do if vim.wo[w].diff then table.insert(diffs, w) end end table.sort(diffs, function(a, b) return vim.api.nvim_win_get_position(a)[2] < vim.api.nvim_win_get_position(b)[2] end) if role == "left" then return diffs[1] end return diffs[#diffs] end ---@param file_path string ---@param committed_content string ---@param worktree_content string ---@return integer sidebar_win ---@return integer entry_line local function setup_sidebar_with_unstaged_file( file_path, committed_content, worktree_content ) local repo = h.make_repo({ [file_path] = committed_content }) t.write(repo, file_path, worktree_content) vim.cmd("cd " .. repo) require("git.status_view").open({ placement = "sidebar" }) local sidebar_buf, sidebar_win = find_sidebar() assert(sidebar_buf, "sidebar buffer should exist") assert(sidebar_win, "sidebar window should exist") local r = assert( require("git.core.repo").find(vim.fn.getcwd()), "repo should resolve for the test worktree" ) r:refresh() t.wait_for(function() return r.status and #r.status:rows("unstaged") > 0 end, "git status to report unstaged changes") local entry_line = assert( find_line(sidebar_buf, vim.pesc(file_path) .. "$"), file_path .. " should appear in sidebar" ) return sidebar_win, entry_line end t.test("stage with diff open: sidebar cursor stays put", function() install_cursor_restore_autocmd() local sidebar_win, line = setup_sidebar_with_unstaged_file( "zsh/rc", "ZSH=true\n", "ZSH=true\nmodified\n" ) vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local r = assert(require("git.core.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) t.press("s") t.wait_for(function() return #r.status:rows("staged") > 0 end, "stage to propagate to repo state") t.eq( vim.api.nvim_win_get_cursor(sidebar_win), { line, 0 }, "sidebar cursor should remain at the entry's original line" ) end) t.test( "stage with diff open: diff foldmethod is preserved on refresh", function() local sidebar_win, line = setup_sidebar_with_unstaged_file( "zsh/rc", "# vim: set ft=zsh nowrap:\nZSH=true\n", "# vim: set ft=zsh nowrap:\nZSH=true\nmodified\n" ) vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local left_win = assert(find_diff_win("left")) t.eq( vim.wo[left_win].foldmethod, "diff", "left diff foldmethod should be 'diff' after Tab" ) local r = assert(require("git.core.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) t.press("s") t.wait_for(function() return #r.status:rows("staged") > 0 end, "stage to propagate to repo state") t.eq( vim.wo[left_win].foldmethod, "diff", "left diff foldmethod should still be 'diff' after stage refresh" ) end ) t.test( " in a second tabpage opens the diff inside that tabpage", function() local sidebar_win, line = setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n") local tab1 = vim.api.nvim_get_current_tabpage() vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left", tab1) ~= nil end, "diff windows in tab1 to appear") vim.cmd("tabnew") require("git.status_view").open({ placement = "sidebar" }) local tab2 = vim.api.nvim_get_current_tabpage() t.truthy(tab2 ~= tab1, "tabnew should produce a distinct tabpage") local _, sidebar_win2 = find_sidebar() assert(sidebar_win2, "sidebar window should exist in tab2") vim.api.nvim_set_current_win(sidebar_win2) vim.api.nvim_win_set_cursor(sidebar_win2, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left", tab2) ~= nil end, "diff windows in tab2 to appear") t.truthy( find_diff_win("right", tab2), "right diff window should be in tab2" ) end ) t.test("refresh on stage updates the index URI buffer's content", function() local sidebar_win, line = setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n") vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local left_win = assert(find_diff_win("left")) local index_buf = vim.api.nvim_win_get_buf(left_win) t.eq( vim.api.nvim_buf_get_lines(index_buf, 0, -1, false), { "v1" }, "index pane should initially show committed content" ) vim.api.nvim_set_current_win(sidebar_win) t.press("s") t.wait_for(function() local first = vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1] return first == "v2" end, "index pane to refresh to staged content") t.eq( vim.api.nvim_buf_get_lines(index_buf, 0, -1, false), { "v2" }, "index pane should reflect staged content after refresh" ) end) t.test( "re-selecting same entry after close + diffsplit keeps fold state in sync", function() local committed, worktree = {}, {} for i = 1, 30 do committed[i] = "line " .. i worktree[i] = i == 15 and "CHANGED" or ("line " .. i) end local sidebar_win, line = setup_sidebar_with_unstaged_file( "foo.txt", table.concat(committed, "\n") .. "\n", table.concat(worktree, "\n") .. "\n" ) local prev_foldlevel = vim.o.foldlevel vim.o.foldlevel = 99 t.defer(function() vim.o.foldlevel = prev_foldlevel end) vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left") ~= nil and find_diff_win("right") ~= nil end, "first diff pair to appear") local first_left = assert(find_diff_win("left")) vim.api.nvim_win_close(first_left, false) local remaining for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do if w ~= sidebar_win then remaining = w break end end if not remaining then error("a non-sidebar window should remain after close") end vim.api.nvim_set_current_win(remaining) 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 if vim.wo[w].diff then count = count + 1 end end return count == 2 end, "diffsplit to produce a diff pair") vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) t.press("") t.wait_for(function() local count = 0 for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do if vim.wo[w].diff then count = count + 1 end end return count == 2 end, "diff pair after re-selecting entry") local left_win = assert(find_diff_win("left")) local right_win = assert(find_diff_win("right")) t.eq( vim.wo[left_win].foldlevel, 0, "left pane foldlevel should be 0 after re-select" ) t.eq( vim.wo[right_win].foldlevel, 0, "right pane foldlevel should be 0 after re-select" ) end ) t.test("sidebar buffer is named /GitStatus", function() local repo = h.make_repo({ ["foo.txt"] = "x\n" }) vim.cmd("cd " .. repo) require("git.status_view").open({ placement = "sidebar" }) local r = assert(require("git.core.repo").find(vim.fn.getcwd())) local buf = find_sidebar() assert(buf, "sidebar buffer should exist") t.eq( vim.api.nvim_buf_get_name(buf), r.worktree .. "/GitStatus", "buffer name should be /GitStatus" ) end) t.test( "calling open twice without closing focuses the existing sidebar", function() local repo = h.make_repo({ ["foo.txt"] = "x\n" }) vim.cmd("cd " .. repo) require("git.status_view").open({ placement = "sidebar" }) local first = find_sidebar() assert(first, "first sidebar buffer should exist") require("git.status_view").open({ placement = "sidebar" }) local second = find_sidebar() assert(second, "second sidebar buffer should exist") t.eq( first, second, "consecutive opens should reuse the visible sidebar" ) local count = 0 for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.bo[b].filetype == "gitstatus" then count = count + 1 end end t.eq(count, 1, "only one gitstatus buffer should exist") end ) t.test("opening for different worktrees creates separate buffers", function() local repo_a = h.make_repo({ ["a.txt"] = "x\n" }) local repo_b = h.make_repo({ ["b.txt"] = "y\n" }) vim.cmd("cd " .. repo_a) require("git.status_view").open({ placement = "sidebar" }) local buf_a = find_sidebar() require("git.status_view").toggle() vim.cmd("cd " .. repo_b) require("git.status_view").open({ placement = "sidebar" }) local buf_b = find_sidebar() assert(buf_a and buf_b) t.truthy( buf_a ~= buf_b, "different worktrees should produce different buffers" ) end) t.test("sidebar buffer is buftype=nofile and not buflisted", function() local repo = h.make_repo({ ["foo.txt"] = "x\n" }) vim.cmd("cd " .. repo) require("git.status_view").open({ placement = "sidebar" }) local buf = find_sidebar() assert(buf, "sidebar buffer should exist") t.eq(vim.bo[buf].buftype, "nofile", "buftype should be nofile") t.eq(vim.bo[buf].buflisted, false, "buflisted should be false") end) t.test("sidebar buffer name does not get written to disk", function() local repo = h.make_repo({ ["foo.txt"] = "x\n" }) vim.cmd("cd " .. repo) require("git.status_view").open({ placement = "sidebar" }) local buf = find_sidebar() assert(buf, "sidebar buffer should exist") local name = vim.api.nvim_buf_get_name(buf) vim.api.nvim_buf_call(buf, function() pcall(function() vim.cmd("silent! write") end) end) t.eq( vim.uv.fs_stat(name), nil, "no real file should be created at the sidebar buffer's path" ) end) t.test( "diffsplit from sidebar resets cursor so panes stay in sync", function() local committed, worktree = {}, {} for i = 1, 100 do committed[i] = "line " .. i worktree[i] = i == 10 and "CHANGED " .. i or i == 40 and "CHANGED " .. i or i == 70 and "CHANGED " .. i or i == 90 and "CHANGED " .. i or ("line " .. i) end local repo = h.make_repo({ ["file.txt"] = table.concat(committed, "\n") .. "\n", }) t.write(repo, "file.txt", table.concat(worktree, "\n") .. "\n") vim.cmd("cd " .. repo) -- Open the worktree file in a normal window and position cursor in -- what becomes a folded section after diff is set up. vim.cmd("edit file.txt") vim.api.nvim_win_set_cursor(0, { 50, 0 }) require("git.status_view").open({ placement = "sidebar" }) local sidebar_buf, sidebar_win = find_sidebar() assert(sidebar_buf and sidebar_win) local r = assert(require("git.core.repo").find(vim.fn.getcwd())) r:refresh() t.wait_for(function() return r.status and #r.status:rows("unstaged") > 0 end, "git status to report unstaged changes") local entry_line for i, l in ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false)) do if l:match("file.txt$") then entry_line = i break end end if not entry_line then error("entry line should exist") end vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_win_set_cursor(sidebar_win, { entry_line, 0 }) t.press("") t.wait_for(function() return find_diff_win("left") ~= nil and find_diff_win("right") ~= nil end, "diff pair to appear") local left_win = assert(find_diff_win("left")) local right_win = assert(find_diff_win("right")) local left_top = vim.api.nvim_win_call(left_win, function() return vim.fn.line("w0") end) local right_top = vim.api.nvim_win_call( right_win, function() return vim.fn.line("w0") end ) t.eq( left_top, right_top, "left and right panes should have the same topline after diffsplit" ) t.eq( vim.api.nvim_win_get_cursor(left_win), { 1, 0 }, "left pane should start at line 1" ) t.eq( vim.api.nvim_win_get_cursor(right_win), { 1, 0 }, "right pane should start at line 1" ) end )