diff --git a/lua/codediff/ui/view/compact.lua b/lua/codediff/ui/view/compact.lua index ce60c409..5299a27c 100644 --- a/lua/codediff/ui/view/compact.lua +++ b/lua/codediff/ui/view/compact.lua @@ -227,6 +227,24 @@ local function apply_folds(session, tabpage) setup_fold_sync(session, tabpage) end +--- Temporarily stop evaluating compact folds while a view swaps buffers. +--- The previous file's visible-line table is keyed by window, so leaving +--- folds enabled during the swap can render the new file with stale folds +--- until its diff has been computed. apply_folds() re-enables them once the +--- new render is ready. +--- @param tabpage number +function M.suspend(tabpage) + local session = lifecycle.get_session(tabpage) + if not session or not session.compact_mode then + return + end + for _, entry in ipairs(pane_entries(session)) do + if entry.win and vim.api.nvim_win_is_valid(entry.win) then + vim.wo[entry.win].foldenable = false + end + end +end + --- Enable compact mode for a tabpage --- @param tabpage number --- @return boolean success @@ -320,7 +338,8 @@ end --- * applies the configured "open in compact mode" default once, when the --- first real diff is ready; --- * re-folds the current diff while compact is active; ---- * exits compact if the diff no longer has any changes. +--- * suspends folds while the current view has no changes, preserving the +--- user's compact-mode choice for the next file. --- gc uses toggle()/enable()/disable() directly. --- @param tabpage number function M.refresh(tabpage) @@ -353,7 +372,7 @@ function M.refresh(tabpage) local changes = session.stored_diff_result and session.stored_diff_result.changes if not changes or #changes == 0 then - M.disable(tabpage) + M.suspend(tabpage) return end diff --git a/lua/codediff/ui/view/side_by_side.lua b/lua/codediff/ui/view/side_by_side.lua index 78209c9f..407dd376 100644 --- a/lua/codediff/ui/view/side_by_side.lua +++ b/lua/codediff/ui/view/side_by_side.lua @@ -21,6 +21,7 @@ local view_keymaps = require("codediff.ui.view.keymaps") local conflict_window = require("codediff.ui.view.conflict_window") local panel = require("codediff.ui.view.panel") local welcome_window = require("codediff.ui.view.welcome_window") +local compact = require("codediff.ui.view.compact") local is_virtual_revision = helpers.is_virtual_revision local prepare_buffer = helpers.prepare_buffer @@ -428,6 +429,12 @@ function M.update(tabpage, session_config, auto_scroll_to_first_hunk) return false end + -- The windows still carry the previous file's compact fold expression and + -- visible-line table. Disable fold evaluation until the new diff render + -- refreshes those tables, otherwise the replacement buffers can briefly be + -- displayed using stale folds. + compact.suspend(tabpage) + -- Disable auto-refresh temporarily auto_refresh.disable(old_original_buf) auto_refresh.disable(old_modified_buf) diff --git a/tests/ui/view/compact_spec.lua b/tests/ui/view/compact_spec.lua index 91b8bbcd..4262478c 100644 --- a/tests/ui/view/compact_spec.lua +++ b/tests/ui/view/compact_spec.lua @@ -11,6 +11,7 @@ local lifecycle = require("codediff.ui.lifecycle") local diff = require("codediff.core.diff") local highlights = require("codediff.ui.highlights") local path = require("codediff.core.path") +local side_by_side = require("codediff.ui.view.side_by_side") local function get_temp_path(name) local is_win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 @@ -66,10 +67,15 @@ describe("compact mode (#344)", function() while vim.fn.tabpagenr("$") > 1 do vim.cmd("tabclose") end - -- The two scratch buffers backing create_session persist across tests - -- because their on-disk paths are the same; delete them so each test - -- starts with a fresh keymap state. - for _, name in ipairs({ "compact_spec_left.txt", "compact_spec_right.txt" }) do + -- Scratch buffers persist across tests because their on-disk paths are + -- reused; delete them so each test starts with fresh window/keymap state. + for _, name in ipairs({ + "compact_spec_left.txt", + "compact_spec_right.txt", + "compact_spec_swap_left.txt", + "compact_spec_swap_right.txt", + "compact_spec_empty.txt", + }) do local path = get_temp_path(name) local bufnr = vim.fn.bufnr(path) if bufnr ~= -1 and vim.api.nvim_buf_is_valid(bufnr) then @@ -270,6 +276,105 @@ describe("compact mode (#344)", function() "second enable must not overwrite the saved fold state") end) + it("does not apply the previous file's folds while update swaps buffers", function() + local original = {} + local modified = {} + for i = 1, 100 do + original[i] = "line " .. i + modified[i] = "line " .. i + end + modified[83] = "line 83 CHANGED" + + local tabpage, session = create_session(original, modified) + assert.is_true(compact.enable(tabpage)) + assert.equal(1, vim.api.nvim_win_call(session.modified_win, function() + return vim.fn.foldclosed(1) + end)) + + local left = get_temp_path("compact_spec_swap_left.txt") + local right = get_temp_path("compact_spec_swap_right.txt") + vim.fn.writefile({ "one" }, left) + vim.fn.writefile({ "one", "two", "three" }, right) + + local foldenable_during_swap + local group = vim.api.nvim_create_augroup("CodeDiffCompactSwapSpec", { clear = true }) + vim.api.nvim_create_autocmd("BufWinEnter", { + group = group, + pattern = "compact_spec_swap_right.txt", + callback = function() + local current = lifecycle.get_session(tabpage) + local current_name = current and vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(current.modified_win)) or "" + if vim.fn.fnamemodify(current_name, ":t") == "compact_spec_swap_right.txt" then + foldenable_during_swap = vim.wo[current.modified_win].foldenable + end + end, + }) + + assert.is_true(view.update(tabpage, { + mode = "standalone", + git_root = nil, + original = path.make_ref(left, nil), + modified = path.make_ref(right, nil), + }, false)) + assert.is_true(vim.wait(5000, function() + local current = lifecycle.get_session(tabpage) + return foldenable_during_swap ~= nil and current + and vim.fn.fnamemodify(vim.api.nvim_buf_get_name(current.modified_bufnr), ":t") == "compact_spec_swap_right.txt" + and current.stored_diff_result and current.stored_diff_result.changes + end, 20)) + + session = lifecycle.get_session(tabpage) + assert.is_false(foldenable_during_swap, "stale compact folds were enabled during the buffer swap") + assert.is_true(vim.wo[session.modified_win].foldenable, "new compact folds were not restored after render") + assert.equal(-1, vim.api.nvim_win_call(session.modified_win, function() + return vim.fn.foldclosed(2) + end), "new changed line was hidden after render") + vim.api.nvim_del_augroup_by_id(group) + end) + + it("restores compact folds after displaying an empty single-file view", function() + local original = {} + local modified = {} + for i = 1, 100 do + original[i] = "line " .. i + modified[i] = "line " .. i + end + modified[83] = "line 83 CHANGED" + + local tabpage, session, left, right = create_session(original, modified) + assert.is_true(compact.enable(tabpage)) + assert.equal(1, vim.api.nvim_win_call(session.modified_win, function() + return vim.fn.foldclosed(1) + end)) + + local empty = get_temp_path("compact_spec_empty.txt") + vim.fn.writefile({}, empty) + side_by_side.show_untracked_file(tabpage, empty) + + session = lifecycle.get_session(tabpage) + assert.is_true(session.compact_mode, "empty view should preserve the compact-mode choice") + assert.is_false(vim.wo[session.modified_win].foldenable, "empty view should suspend compact folds") + + assert.is_true(view.update(tabpage, { + mode = "standalone", + git_root = nil, + original = path.make_ref(left, nil), + modified = path.make_ref(right, nil), + }, false)) + assert.is_true(vim.wait(5000, function() + local current = lifecycle.get_session(tabpage) + return current and not current.single_pane and current.stored_diff_result + and current.stored_diff_result.changes and #current.stored_diff_result.changes > 0 + end, 20)) + + session = lifecycle.get_session(tabpage) + assert.is_true(session.compact_mode) + assert.is_true(vim.wo[session.modified_win].foldenable, "compact folds were not restored") + assert.equal(1, vim.api.nvim_win_call(session.modified_win, function() + return vim.fn.foldclosed(1) + end), "unchanged region was not folded after leaving the empty view") + end) + it("returns false when there are no changes to compact", function() -- Identical files → no hunks. local lines = { "a", "b", "c" }