From 0bdc0e6a8f9a9c15fc02aa664554c4781fd581d4 Mon Sep 17 00:00:00 2001 From: David Yonge-Mallo Date: Thu, 27 Aug 2026 12:50:52 +0200 Subject: [PATCH] fix(view): preserve cycled layout across `force_entry_refresh_on_noop` --- lua/diffview/scene/views/diff/diff_view.lua | 10 +++ .../tests/functional/diff_view_spec.lua | 83 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/lua/diffview/scene/views/diff/diff_view.lua b/lua/diffview/scene/views/diff/diff_view.lua index 56aa71dd..c80fb15c 100644 --- a/lua/diffview/scene/views/diff/diff_view.lua +++ b/lua/diffview/scene/views/diff/diff_view.lua @@ -853,6 +853,16 @@ local update_files_impl = debounce.debounce_trailing( end if replace_noop then + -- Preserve the user's cycled/set layout across the refresh: + -- `get_updated_files` always builds new entries with the config + -- default, so without this the layout silently resets to the + -- default whenever `force_entry_refresh_on_noop` fires (jj on + -- any LOCAL-touching range: every tab_enter, focus_gained, poll, + -- or explicit `R` refresh). + if new_file.layout.class ~= old_file.layout.class then + new_file:convert_layout(old_file.layout.class --[[@as Layout ]]) + end + if self.panel.cur_file == old_file then self.panel:set_cur_file(new_file) end diff --git a/lua/diffview/tests/functional/diff_view_spec.lua b/lua/diffview/tests/functional/diff_view_spec.lua index 16fbcb92..8d7028c9 100644 --- a/lua/diffview/tests/functional/diff_view_spec.lua +++ b/lua/diffview/tests/functional/diff_view_spec.lua @@ -102,6 +102,89 @@ describe("diffview.scene.views.diff.DiffView", function() end) ) + -- Regression: on adapters whose `force_entry_refresh_on_noop` returns + -- true (jj on any LOCAL-touching range), the NOOP branch destroys the + -- old entry and installs a fresh one built by `get_updated_files` with + -- the config default layout. Without preserving the old entry's layout + -- class across the swap, any `cycle_layout` / `set_layout` choice was + -- silently reset by every refresh (tab_enter, FocusGained, poll, R). + -- Bug: dlyongemallo/diffview-plus.nvim#312. + it( + "preserves the entry's layout class when force_entry_refresh_on_noop replaces it", + test_utils.async_test(function() + local Diff2Hor = require("diffview.scene.layouts.diff_2_hor").Diff2Hor + local Diff2Ver = require("diffview.scene.layouts.diff_2_ver").Diff2Ver + + local repo = make_repo() + local view + + local ok, err = pcall(function() + local file_spec = { + working = { + { path = "init.txt", status = "M", stats = { additions = 0, deletions = 0 } }, + }, + staged = {}, + conflicting = {}, + } + + view = CDiffView({ + git_root = repo, + left = Rev(RevType.COMMIT, run({ "git", "rev-parse", "HEAD" }, repo), true), + right = Rev(RevType.LOCAL), + files = file_spec, + update_files = function() + return file_spec + end, + get_file_data = function() + return {} + end, + }) + + view:open() + vim.wait(2000, function() + return view.initialized + end, 10) + + local original = view.files.working[1] + assert.is_truthy(original) + assert.True(original.layout:instanceof(Diff2Hor)) + + -- Simulate `cycle_layout`: swap the entry's stored layout to a + -- non-default class. + original:convert_layout(Diff2Ver) + eq(Diff2Ver, original.layout.class) + + -- Force the NOOP replace-entry branch, as jj does whenever the + -- range touches LOCAL. + view.adapter.force_entry_refresh_on_noop = function() + return true + end + + local refresh_done = false + view:update_files(function() + refresh_done = true + end) + vim.wait(2000, function() + return refresh_done + end, 10) + assert.is_true(refresh_done) + + local refreshed = view.files.working[1] + assert.is_truthy(refreshed) + -- The entry was recreated (that's the whole point of the branch), + -- but the user's chosen layout class must survive the swap. + assert.are_not.equal(original, refreshed) + eq(Diff2Ver, refreshed.layout.class) + end) + + close_view(view) + cleanup_repo(repo) + if not ok then + error(err) + end + end) + ) + -- Regression: the wrapped impl signature were changed from -- (self, callback) to (self, opts, callback). Legacy callers using -- update_files(callback) would otherwise dereference opts.force on a