From fa5a716f8284a5f1a68e0f9557c59ad24c2d388b Mon Sep 17 00:00:00 2001 From: David Yonge-Mallo Date: Tue, 28 Jul 2026 15:22:41 +0200 Subject: [PATCH] fix(fold): skip `foldmethod=diff` override on solo-content side of added/deleted files --- lua/diffview/scene/window.lua | 47 +++++++++++-- lua/diffview/tests/functional/window_spec.lua | 70 +++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/lua/diffview/scene/window.lua b/lua/diffview/scene/window.lua index 53b14efe..fb315d70 100644 --- a/lua/diffview/scene/window.lua +++ b/lua/diffview/scene/window.lua @@ -216,13 +216,17 @@ Window.open_file = async.void(function(self) return end + -- On the content side of an added/deleted file `foldmethod=diff` + -- produces no folds; leave the buffer's own folding alone. See #299. + local solo_content_side = self:_paired_side_is_null() + -- Apply the configured foldlevel before `_save_winopts` so the saved -- value covers the key we're about to override. Scope this to diff -- buffers (where `diff` is not explicitly false), matching the -- documented purpose of `view.foldlevel`; non-diff layouts like -- `diff1_raw` opt out via `diff = false` and supply their own -- foldlevel via the layout winopts. - if self.file.winopts and self.file.winopts.diff ~= false then + if self.file.winopts and self.file.winopts.diff ~= false and not solo_content_side then self.file.winopts.foldlevel = conf.view.foldlevel end @@ -233,7 +237,7 @@ Window.open_file = async.void(function(self) if self:is_nulled() then self:apply_null_winopts() else - self:apply_file_winopts() + self:apply_file_winopts({ skip_fold = solo_content_side }) end local view = lib.get_current_view() @@ -366,13 +370,48 @@ function Window:_restore_winopts() end end -function Window:apply_file_winopts() +-- Fold-related winopts skipped by `apply_file_winopts` when `skip_fold` +-- is set. See #299. +local FOLD_OPTS = { foldmethod = true, foldenable = true, foldlevel = true } + +---@param opt? { skip_fold?: boolean } +function Window:apply_file_winopts(opt) assert(self.file) - if self.file.winopts then + if not self.file.winopts then + return + end + + if opt and opt.skip_fold then + local filtered = {} + for k, v in pairs(self.file.winopts) do + if not FOLD_OPTS[k] then + filtered[k] = v + end + end + utils.set_local(self.id, filtered) + else utils.set_local(self.id, self.file.winopts) end end +---True when a sibling window in the parent layout is nulled: the +---content side of an added/deleted file. +---@return boolean +function Window:_paired_side_is_null() + local windows = self.parent and self.parent.windows + if not windows then + return false + end + + for _, other in ipairs(windows) do + if other ~= self and other.file and other.file.nulled then + return true + end + end + + return false +end + function Window:apply_null_winopts() if File.NULL_FILE.winopts then utils.set_local(self.id, File.NULL_FILE.winopts) diff --git a/lua/diffview/tests/functional/window_spec.lua b/lua/diffview/tests/functional/window_spec.lua index c11a130a..05b0dcf7 100644 --- a/lua/diffview/tests/functional/window_spec.lua +++ b/lua/diffview/tests/functional/window_spec.lua @@ -304,6 +304,76 @@ describe("diffview.scene.window", function() end) ) + -- Regression (#299): fold winopts must be skipped on the content + -- side of an added/deleted file, so the buffer's own folding + -- (ftplugin, treesitter, ufo, ...) survives. + it( + "does not override fold options when the paired window's file is nulled", + helpers.async_test(function() + config.setup({ view = { foldlevel = 0 } }) + + local adapter = mock_adapter() + local bufnr = vim.api.nvim_create_buf(false, true) + local win, file = make_window(adapter) + file.bufnr = bufnr + file.loaded = true + + local sibling_file = { nulled = true } + local sibling_win = { file = sibling_file } + win.parent = vim.tbl_extend("force", stub_parent(), { windows = { win, sibling_win } }) + + -- Values distinct from what `winopts` would install, so survival + -- proves the override was skipped rather than coincidentally matching. + vim.wo[win.id].foldmethod = "marker" + vim.wo[win.id].foldlevel = 42 + vim.wo[win.id].foldenable = false + + async.await(win:open_file()) + + assert.equals("marker", vim.wo[win.id].foldmethod) + assert.equals(42, vim.wo[win.id].foldlevel) + assert.is_false(vim.wo[win.id].foldenable) + + if vim.api.nvim_win_is_valid(test_winid) then + vim.api.nvim_win_close(test_winid, true) + end + test_winid = nil + Window.winopt_store[bufnr] = nil + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + ) + + it( + "still applies fold options when no sibling window is nulled", + helpers.async_test(function() + config.setup({ view = { foldlevel = 0 } }) + + local adapter = mock_adapter() + local bufnr = vim.api.nvim_create_buf(false, true) + local win, file = make_window(adapter) + file.bufnr = bufnr + file.loaded = true + + local sibling_file = { nulled = false } + local sibling_win = { file = sibling_file } + win.parent = vim.tbl_extend("force", stub_parent(), { windows = { win, sibling_win } }) + + vim.wo[win.id].foldmethod = "marker" + + async.await(win:open_file()) + + assert.equals("diff", vim.wo[win.id].foldmethod) + assert.equals(0, vim.wo[win.id].foldlevel) + + if vim.api.nvim_win_is_valid(test_winid) then + vim.api.nvim_win_close(test_winid, true) + end + test_winid = nil + Window.winopt_store[bufnr] = nil + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + ) + -- Regression: `Layout.open_files` yields between its load loop and its -- open loop. A rapid navigation in that gap deactivates the in-flight -- file; bailing on `active=false` here used to leave the window holding