From 566c7f42ac62308af880cc47285b2adaa1a61d5e Mon Sep 17 00:00:00 2001 From: coincheung Date: Thu, 12 Mar 2026 06:20:32 +0000 Subject: [PATCH] fix(diff): accept/deny search all windows in current tab Previously, ClaudeCodeDiffAccept and ClaudeCodeDiffDeny required the cursor to be in the diff buffer itself. If the cursor was in another window within the same tab (e.g. the terminal split, or the original file side of the diff), the commands would fail with "No active diff found in current buffer". Add a local helper `find_diff_buf_in_current_tab()` that searches all windows in the current tabpage for a buffer with `claudecode_diff_tab_name` set. Both accept and deny now fall back to this search when the current buffer is not a diff buffer. Also add `nvim_tabpage_list_wins` to the vim mock and a new test spec covering the direct and fallback paths for both commands. --- lua/claudecode/diff.lua | 39 +++- tests/mocks/vim.lua | 17 ++ .../unit/diff_accept_deny_tab_search_spec.lua | 181 ++++++++++++++++++ 3 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 tests/unit/diff_accept_deny_tab_search_spec.lua diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index 79f8bb9..208da80 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -1442,14 +1442,36 @@ function M.reload_file_buffers_manual(file_path, original_cursor_pos) return reload_file_buffers(file_path, original_cursor_pos) end +---Find a diff buffer in the current tabpage by searching all windows. +---@return integer? buf The buffer number with an active diff, or nil if not found +local function find_diff_buf_in_current_tab() + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local buf = vim.api.nvim_win_get_buf(win) + if vim.b[buf].claudecode_diff_tab_name then + return buf + end + end + return nil +end + ---Accept the current diff (user command version) ----This function reads the diff context from buffer variables +---This function reads the diff context from buffer variables. +---If the current buffer is not a diff buffer, falls back to searching +---all windows in the current tab for an active diff. function M.accept_current_diff() local current_buffer = vim.api.nvim_get_current_buf() local tab_name = vim.b[current_buffer].claudecode_diff_tab_name if not tab_name then - vim.notify("No active diff found in current buffer", vim.log.levels.WARN) + local diff_buf = find_diff_buf_in_current_tab() + if diff_buf then + tab_name = vim.b[diff_buf].claudecode_diff_tab_name + current_buffer = diff_buf + end + end + + if not tab_name then + vim.notify("No active diff found in current tab", vim.log.levels.WARN) return end @@ -1457,13 +1479,22 @@ function M.accept_current_diff() end ---Deny/reject the current diff (user command version) ----This function reads the diff context from buffer variables +---This function reads the diff context from buffer variables. +---If the current buffer is not a diff buffer, falls back to searching +---all windows in the current tab for an active diff. function M.deny_current_diff() local current_buffer = vim.api.nvim_get_current_buf() local tab_name = vim.b[current_buffer].claudecode_diff_tab_name if not tab_name then - vim.notify("No active diff found in current buffer", vim.log.levels.WARN) + local diff_buf = find_diff_buf_in_current_tab() + if diff_buf then + tab_name = vim.b[diff_buf].claudecode_diff_tab_name + end + end + + if not tab_name then + vim.notify("No active diff found in current tab", vim.log.levels.WARN) return end diff --git a/tests/mocks/vim.lua b/tests/mocks/vim.lua index 2c1e5b9..db308c1 100644 --- a/tests/mocks/vim.lua +++ b/tests/mocks/vim.lua @@ -411,6 +411,23 @@ local vim = { return vim._tabs[tab] == true end, + nvim_tabpage_list_wins = function(tabpage) + if tabpage == 0 then + tabpage = vim._current_tabpage + end + local wins = {} + local list = vim._tab_windows[tabpage] or {} + for _, winid in ipairs(list) do + if vim._windows[winid] then + table.insert(wins, winid) + end + end + if #wins == 0 then + table.insert(wins, vim._current_window) + end + return wins + end, + nvim_tabpage_get_number = function(tab) return tab end, diff --git a/tests/unit/diff_accept_deny_tab_search_spec.lua b/tests/unit/diff_accept_deny_tab_search_spec.lua new file mode 100644 index 0000000..8801389 --- /dev/null +++ b/tests/unit/diff_accept_deny_tab_search_spec.lua @@ -0,0 +1,181 @@ +-- luacheck: globals expect +-- Tests for accept_current_diff / deny_current_diff tab-wide search fallback +require("tests.busted_setup") + +describe("diff accept/deny tab-wide search", function() + local diff + + before_each(function() + package.loaded["claudecode.diff"] = nil + package.loaded["claudecode.config"] = nil + diff = require("claudecode.diff") + diff.setup({}) + + -- Reset mock state: one tab, one window showing buffer 1 + _G.vim._buffers = { + [1] = { name = "/project/file.lua", lines = { "hello" }, options = {}, b_vars = {} }, + } + _G.vim._windows = { [1000] = { buf = 1, width = 80 } } + _G.vim._win_tab = { [1000] = 1 } + _G.vim._tab_windows = { [1] = { 1000 } } + _G.vim._tabs = { [1] = true } + _G.vim._current_tabpage = 1 + _G.vim._current_window = 1000 + _G.vim._next_winid = 1001 + end) + + after_each(function() + diff._cleanup_all_active_diffs("test_teardown") + end) + + -- Helper: create a second buffer in the current tab with claudecode_diff_tab_name set + local function add_diff_buffer_to_current_tab(tab_name) + local diff_buf = #_G.vim._buffers + 1 + _G.vim._buffers[diff_buf] = { + name = "/tmp/claudecode_diff_test.lua.new", + lines = { "new content" }, + options = { eol = true }, + b_vars = { claudecode_diff_tab_name = tab_name }, + } + local diff_win = _G.vim._next_winid + _G.vim._next_winid = _G.vim._next_winid + 1 + _G.vim._windows[diff_win] = { buf = diff_buf, width = 80 } + _G.vim._win_tab[diff_win] = 1 + table.insert(_G.vim._tab_windows[1], diff_win) + return diff_buf, diff_win + end + + describe("accept_current_diff", function() + it("works when cursor is in the diff buffer", function() + local tab_name = "test_accept_direct" + local diff_buf, diff_win = add_diff_buffer_to_current_tab(tab_name) + + -- Simulate an active diff with a resolution callback + -- Put cursor in diff buffer + _G.vim._current_window = diff_win + _G.vim.api.nvim_get_current_buf = function() + return diff_buf + end + + local found_buf = nil + local orig_resolve = diff._resolve_diff_as_saved + diff._resolve_diff_as_saved = function(tn, buf) + found_buf = buf + -- don't actually resolve, just capture + end + + diff.accept_current_diff() + + diff._resolve_diff_as_saved = orig_resolve + + assert.equal(diff_buf, found_buf) + end) + + it("falls back to searching the current tab when cursor is not in a diff buffer", function() + local tab_name = "test_accept_fallback" + local diff_buf, _ = add_diff_buffer_to_current_tab(tab_name) + + -- Cursor stays in the non-diff buffer (window 1000, buf 1) + _G.vim._current_window = 1000 + _G.vim.api.nvim_get_current_buf = function() + return 1 -- no claudecode_diff_tab_name + end + + local found_buf = nil + local orig_resolve = diff._resolve_diff_as_saved + diff._resolve_diff_as_saved = function(tn, buf) + found_buf = buf + end + + diff.accept_current_diff() + + diff._resolve_diff_as_saved = orig_resolve + + assert.equal(diff_buf, found_buf) + end) + + it("notifies when no diff buffer found in current tab", function() + -- No diff buffer in the tab + _G.vim._current_window = 1000 + _G.vim.api.nvim_get_current_buf = function() + return 1 + end + + local notified = false + local orig_notify = _G.vim.notify + _G.vim.notify = function(msg, level) + notified = true + assert.is_not_nil(msg:find("No active diff")) + end + + diff.accept_current_diff() + + _G.vim.notify = orig_notify + assert.is_true(notified) + end) + end) + + describe("deny_current_diff", function() + it("works when cursor is in the diff buffer", function() + local tab_name = "test_deny_direct" + local diff_buf, diff_win = add_diff_buffer_to_current_tab(tab_name) + + _G.vim._current_window = diff_win + _G.vim.api.nvim_get_current_buf = function() + return diff_buf + end + + local resolved_name = nil + local orig_resolve = diff._resolve_diff_as_rejected + diff._resolve_diff_as_rejected = function(tn) + resolved_name = tn + end + + diff.deny_current_diff() + + diff._resolve_diff_as_rejected = orig_resolve + assert.equal(tab_name, resolved_name) + end) + + it("falls back to searching the current tab when cursor is not in a diff buffer", function() + local tab_name = "test_deny_fallback" + add_diff_buffer_to_current_tab(tab_name) + + -- Cursor in non-diff buffer + _G.vim._current_window = 1000 + _G.vim.api.nvim_get_current_buf = function() + return 1 + end + + local resolved_name = nil + local orig_resolve = diff._resolve_diff_as_rejected + diff._resolve_diff_as_rejected = function(tn) + resolved_name = tn + end + + diff.deny_current_diff() + + diff._resolve_diff_as_rejected = orig_resolve + assert.equal(tab_name, resolved_name) + end) + + it("notifies when no diff buffer found in current tab", function() + _G.vim._current_window = 1000 + _G.vim.api.nvim_get_current_buf = function() + return 1 + end + + local notified = false + local orig_notify = _G.vim.notify + _G.vim.notify = function(msg, level) + notified = true + assert.is_not_nil(msg:find("No active diff")) + end + + diff.deny_current_diff() + + _G.vim.notify = orig_notify + assert.is_true(notified) + end) + end) +end)