diff --git a/lua/diffview/scene/views/diff/diff_view.lua b/lua/diffview/scene/views/diff/diff_view.lua index 25d01407..03592b2d 100644 --- a/lua/diffview/scene/views/diff/diff_view.lua +++ b/lua/diffview/scene/views/diff/diff_view.lua @@ -886,6 +886,19 @@ local update_files_impl = debounce.debounce_trailing( if new_head then old_file:update_heads(new_head) end + + -- Give the adapter a chance to refresh LOCAL buffer content in + -- place. Jj rewrites working-copy files even when the LOCAL + -- rev's `object_name()` is stable (it's always "UNKNOWN"), so + -- `refresh_revs` can't catch that drift and the NOOP branch + -- would otherwise leave the LOCAL side showing stale content. + -- `on_local_buffer_reused` is a no-op for adapters that don't + -- rewrite working-copy files (git, hg). + for _, f in ipairs(old_file.layout:files()) do + if f.rev.type == RevType.LOCAL and f.bufnr and api.nvim_buf_is_valid(f.bufnr) then + self.adapter:on_local_buffer_reused(f.bufnr) + end + end end end diff --git a/lua/diffview/tests/functional/diff_view_spec.lua b/lua/diffview/tests/functional/diff_view_spec.lua index 8d7028c9..03192a66 100644 --- a/lua/diffview/tests/functional/diff_view_spec.lua +++ b/lua/diffview/tests/functional/diff_view_spec.lua @@ -185,6 +185,99 @@ describe("diffview.scene.views.diff.DiffView", function() end) ) + -- The NOOP-keep path routes LOCAL sides through + -- `adapter:on_local_buffer_reused` so working-copy content is refreshed + -- in place, without destroying and re-loading the diff buffers. Jj's + -- adapter relies on this (its LOCAL `object_name()` is always "UNKNOWN", + -- so `refresh_revs` can't detect a rewritten working copy) and dropped + -- its `force_entry_refresh_on_noop` override so this path is taken + -- instead of the entry-swap path that briefly attaches diff windows to + -- `diffview://null`. + it( + "calls on_local_buffer_reused for LOCAL files kept across a NOOP refresh", + test_utils.async_test(function() + 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 entry = view.files.working[1] + assert.is_truthy(entry) + + -- Stamp a scratch bufnr on the LOCAL side. The CDiffView test + -- harness's `get_file_data` is a no-op and the async `set_file` + -- that would normally load LOCAL buffers is fire-and-forget + -- from `update_files_impl`, so `bufnr` may still be nil at this + -- point. Stamping it deterministically makes the + -- `nvim_buf_is_valid` gate pass and gives us a stable value to + -- assert on. + local local_file + for _, f in ipairs(entry.layout:files()) do + if f.rev.type == RevType.LOCAL then + local_file = f + break + end + end + assert.is_truthy(local_file) + local scratch = vim.api.nvim_create_buf(false, true) + local_file.bufnr = scratch + + local reused_bufnrs = {} + view.adapter.on_local_buffer_reused = function(_, bufnr) + table.insert(reused_bufnrs, bufnr) + 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) + + -- Kept-entry path: the entry survives (git's default) and the + -- LOCAL bufnr was handed to `on_local_buffer_reused`. Jj drops + -- its `force_entry_refresh_on_noop` override so this is the + -- path it takes too. + eq(entry, view.files.working[1]) + assert.is_true(#reused_bufnrs >= 1) + eq(scratch, reused_bufnrs[#reused_bufnrs]) + 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 diff --git a/lua/diffview/tests/functional/jj_adapter_spec.lua b/lua/diffview/tests/functional/jj_adapter_spec.lua index ad66d17a..e3d1980b 100644 --- a/lua/diffview/tests/functional/jj_adapter_spec.lua +++ b/lua/diffview/tests/functional/jj_adapter_spec.lua @@ -145,24 +145,27 @@ describe("diffview.vcs.adapters.jj", function() end) describe("force_entry_refresh_on_noop()", function() - it("returns true for ranges that include LOCAL", function() + -- Jj no longer forces an entry rebuild on NOOP: the diff view's + -- NOOP-keep path now routes LOCAL sides through `on_local_buffer_reused` + -- (checktime), which reloads any jj-rewritten working-copy content in + -- place without briefly attaching diff windows to `diffview://null`. + it("inherits the base 'false' for every rev pair", function() local adapter = new_adapter() - local ok = adapter:force_entry_refresh_on_noop( - adapter.Rev(RevType.COMMIT, "left_hash"), - adapter.Rev(RevType.LOCAL) - ) - - eq(true, ok) - end) - it("returns false for commit-to-commit ranges", function() - local adapter = new_adapter() - local ok = adapter:force_entry_refresh_on_noop( - adapter.Rev(RevType.COMMIT, "left_hash"), - adapter.Rev(RevType.COMMIT, "right_hash") + eq( + false, + adapter:force_entry_refresh_on_noop( + adapter.Rev(RevType.COMMIT, "left_hash"), + adapter.Rev(RevType.LOCAL) + ) + ) + eq( + false, + adapter:force_entry_refresh_on_noop( + adapter.Rev(RevType.COMMIT, "left_hash"), + adapter.Rev(RevType.COMMIT, "right_hash") + ) ) - - eq(false, ok) end) end) diff --git a/lua/diffview/vcs/adapters/jj/init.lua b/lua/diffview/vcs/adapters/jj/init.lua index 5897c9d8..67dda760 100644 --- a/lua/diffview/vcs/adapters/jj/init.lua +++ b/lua/diffview/vcs/adapters/jj/init.lua @@ -677,15 +677,12 @@ function JjAdapter:refresh_revs(rev_arg, left, right) return new_left, new_right end ----@param left Rev ----@param right Rev ----@return boolean -function JjAdapter:force_entry_refresh_on_noop(left, right) - return self:has_local(left, right) -end - ----Jj may rewrite working-copy files when revisions change, so reload the ----buffer from disk when it is reused. +---Jj may rewrite working-copy files (its working copy is a commit) even +---when the LOCAL rev's `object_name()` looks unchanged, so the diff view's +---NOOP-keep path routes every LOCAL side through this hook to catch the +---rewrite via `checktime`. Overriding `force_entry_refresh_on_noop` to +---force a full entry rebuild would also work but tears down and re-loads +---the diff buffers, briefly attaching them to `diffview://null`. ---@param bufnr integer function JjAdapter:on_local_buffer_reused(bufnr) local api = vim.api