Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions lua/codediff/ui/view/compact.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions lua/codediff/ui/view/side_by_side.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
113 changes: 109 additions & 4 deletions tests/ui/view/compact_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down