Skip to content
Merged
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
2 changes: 0 additions & 2 deletions lua/codediff/ui/auto_refresh.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lua/codediff/ui/explorer/render.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
109 changes: 109 additions & 0 deletions tests/ui/conflict/conflict_dedup_spec.lua
Original file line number Diff line number Diff line change
@@ -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)