diff --git a/lua/codediff/ui/auto_refresh.lua b/lua/codediff/ui/auto_refresh.lua index 192225e7..b56814f3 100644 --- a/lua/codediff/ui/auto_refresh.lua +++ b/lua/codediff/ui/auto_refresh.lua @@ -114,8 +114,6 @@ local function do_diff_update(bufnr, skip_watcher_check) -- Re-sync scrollbind after filler changes -- This ensures all windows stay aligned even if fillers were added/removed local original_win, modified_win, result_win = nil, nil, nil - local lifecycle = require("codediff.ui.lifecycle") - local tabpage = vim.api.nvim_get_current_tabpage() local _, stored_result_win = lifecycle.get_result(tabpage) for _, win in ipairs(vim.api.nvim_list_wins()) do diff --git a/lua/codediff/ui/explorer/render.lua b/lua/codediff/ui/explorer/render.lua index 3007f444..c4829584 100644 --- a/lua/codediff/ui/explorer/render.lua +++ b/lua/codediff/ui/explorer/render.lua @@ -327,6 +327,13 @@ function M.create(status_result, git_root, tabpage, width, base_revision, target local is_same_file = (session.modified_path == abs_path or session.modified_path == file_path or (session.git_root and session.original_path == file_path)) if is_same_file and not opts.force then + -- Conflict mode: skip if already showing the same conflict file + -- (revisions :2/:3 are mutable so the staged-base-change logic below + -- would incorrectly force a re-render on every refresh cycle) + if group == "conflicts" and session.result_win and vim.api.nvim_win_is_valid(session.result_win) then + return + end + -- Check if it's the same diff comparison local is_staged_diff = group == "staged" local current_is_staged = session.modified_revision == ":0" diff --git a/tests/ui/conflict/conflict_dedup_spec.lua b/tests/ui/conflict/conflict_dedup_spec.lua new file mode 100644 index 00000000..07d6f1b1 --- /dev/null +++ b/tests/ui/conflict/conflict_dedup_spec.lua @@ -0,0 +1,109 @@ +-- Test: Conflict file deduplication prevents unnecessary view.update() calls (#317) +local assert = require("luassert") + +describe("Conflict dedup in on_file_select", function() + local lifecycle, config + + before_each(function() + lifecycle = require("codediff.ui.lifecycle") + config = require("codediff.config") + end) + + it("should detect conflict mode via result_win on session", function() + -- Simulate a conflict session with result_win set + local tabpage = vim.api.nvim_get_current_tabpage() + local result_win = vim.api.nvim_get_current_win() + + -- Create a mock session with conflict mode indicators + local mock_session = { + modified_path = "file.txt", + original_path = "file.txt", + git_root = "/tmp/test", + modified_revision = ":2", + original_revision = ":3", + result_win = result_win, + result_bufnr = vim.api.nvim_get_current_buf(), + } + + -- The dedup check logic from on_file_select: + -- For conflict files (group="conflicts"), if session has valid result_win, + -- the same file should be detected as already displayed + local group = "conflicts" + local file_path = "file.txt" + local abs_path = "/tmp/test/file.txt" + + local is_same_file = (mock_session.modified_path == abs_path + or mock_session.modified_path == file_path + or (mock_session.git_root and mock_session.original_path == file_path)) + + assert.is_true(is_same_file, "Should detect same conflict file") + + -- The fix: conflict-specific early return + local should_skip = (group == "conflicts" + and mock_session.result_win + and vim.api.nvim_win_is_valid(mock_session.result_win)) + + assert.is_true(should_skip, "Should skip update for same conflict file with active result_win") + end) + + it("should NOT skip when result_win is nil (conflict not yet loaded)", function() + local mock_session = { + modified_path = "file.txt", + original_path = "file.txt", + git_root = "/tmp/test", + modified_revision = nil, + original_revision = nil, + result_win = nil, + } + + local group = "conflicts" + local should_skip = (group == "conflicts" + and mock_session.result_win + and vim.api.nvim_win_is_valid(mock_session.result_win)) + + assert.is_falsy(should_skip, "Should NOT skip when result_win is nil") + end) + + it("should NOT skip for non-conflict groups even with result_win", function() + local result_win = vim.api.nvim_get_current_win() + + local mock_session = { + modified_path = "file.txt", + original_path = "file.txt", + git_root = "/tmp/test", + modified_revision = ":0", + original_revision = "abc123", + result_win = result_win, + } + + local group = "staged" + local should_skip = (group == "conflicts" + and mock_session.result_win + and vim.api.nvim_win_is_valid(mock_session.result_win)) + + assert.is_false(should_skip, "Should NOT skip for staged group") + end) + + it("demonstrates the original bug: mutable revision check fails for conflict revisions", function() + -- This is the original buggy check that caused flickering + -- Conflict revisions :2/:3 match ^:[0-3]$ causing false positive + local mock_session = { + original_revision = ":3", -- conflict THEIRS revision + } + + local current_is_mutable = mock_session.original_revision + and mock_session.original_revision:match("^:[0-3]$") + + assert.is_truthy(current_is_mutable, + "Conflict revision :3 matches mutable pattern (the root cause of the bug)") + + -- And conflict files are NOT in staged list + local file_has_staged = false -- conflict files aren't in status_result.staged + + -- This evaluates to true, meaning "comparison base needs to change" (WRONG for conflicts) + local would_skip = not (file_has_staged ~= (current_is_mutable and true or false)) + + assert.is_false(would_skip, + "Original check incorrectly thinks comparison base needs to change for conflict files") + end) +end)