Skip to content

Commit 0bcb677

Browse files
committed
feat: add on_new_file_reject option to control empty buffer behavior
Change-Id: Idc973b23ff2a00ce2e9142e8c2b941b114ef7059 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent f8c7db8 commit 0bcb677

File tree

4 files changed

+119
-2
lines changed

4 files changed

+119
-2
lines changed

lua/claudecode/config.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ M.defaults = {
2222
layout = "vertical",
2323
open_in_new_tab = false, -- Open diff in a new tab (false = use current tab)
2424
keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens
25+
on_new_file_reject = "keep_empty", -- "keep_empty" leaves an empty buffer; "close_window" closes the placeholder split
2526
},
2627
models = {
2728
{ name = "Claude Opus 4.1 (Latest)", value = "opus" },
@@ -108,6 +109,11 @@ function M.validate(config)
108109
)
109110
assert(type(config.diff_opts.open_in_new_tab) == "boolean", "diff_opts.open_in_new_tab must be a boolean")
110111
assert(type(config.diff_opts.keep_terminal_focus) == "boolean", "diff_opts.keep_terminal_focus must be a boolean")
112+
assert(
113+
type(config.diff_opts.on_new_file_reject) == "string"
114+
and (config.diff_opts.on_new_file_reject == "keep_empty" or config.diff_opts.on_new_file_reject == "close_window"),
115+
"diff_opts.on_new_file_reject must be 'keep_empty' or 'close_window'"
116+
)
111117

112118
-- Validate env
113119
assert(type(config.env) == "table", "env must be a table")

lua/claudecode/diff.lua

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,7 @@ function M._create_diff_view_from_window(
701701
terminal_win_in_new_tab,
702702
existing_buffer
703703
)
704+
local original_buffer_created_by_plugin = false
704705
-- If no target window provided, create a new window in suitable location
705706
if not target_window then
706707
-- If we have a terminal window in the new tab, we're already positioned correctly
@@ -808,6 +809,7 @@ function M._create_diff_view_from_window(
808809

809810
vim.api.nvim_win_set_buf(original_window, empty_buffer)
810811
original_buffer = empty_buffer
812+
original_buffer_created_by_plugin = true
811813
else
812814
-- Load existing file in the main window of new tab
813815
if existing_buffer then
@@ -860,6 +862,7 @@ function M._create_diff_view_from_window(
860862

861863
vim.api.nvim_win_set_buf(original_window, empty_buffer)
862864
original_buffer = empty_buffer
865+
original_buffer_created_by_plugin = true
863866
else
864867
-- Load existing file in the new window
865868
if existing_buffer then
@@ -955,6 +958,7 @@ function M._create_diff_view_from_window(
955958
new_window = new_win,
956959
target_window = original_window, -- This is now the window actually showing the original file
957960
original_buffer = original_buffer,
961+
original_buffer_created_by_plugin = original_buffer_created_by_plugin,
958962
}
959963
end
960964

@@ -1021,8 +1025,13 @@ function M._cleanup_diff_state(tab_name, reason)
10211025
pcall(vim.api.nvim_buf_delete, diff_data.new_buffer, { force = true })
10221026
end
10231027

1024-
-- Clean up the original buffer if it was created for a new file
1025-
if diff_data.is_new_file and diff_data.original_buffer and vim.api.nvim_buf_is_valid(diff_data.original_buffer) then
1028+
-- Clean up the original buffer only if it was created by the plugin for a new file
1029+
if
1030+
diff_data.is_new_file
1031+
and diff_data.original_buffer
1032+
and vim.api.nvim_buf_is_valid(diff_data.original_buffer)
1033+
and diff_data.original_buffer_created_by_plugin
1034+
then
10261035
pcall(vim.api.nvim_buf_delete, diff_data.original_buffer, { force = true })
10271036
end
10281037

@@ -1168,6 +1177,7 @@ function M._setup_blocking_diff(params, resolution_callback)
11681177
new_window = diff_info.new_window,
11691178
target_window = diff_info.target_window,
11701179
original_buffer = diff_info.original_buffer,
1180+
original_buffer_created_by_plugin = diff_info.original_buffer_created_by_plugin,
11711181
original_cursor_pos = original_cursor_pos,
11721182
original_tab_number = original_tab_number,
11731183
created_new_tab = created_new_tab,

lua/claudecode/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
---@field layout ClaudeCodeDiffLayout
1919
---@field open_in_new_tab boolean Open diff in a new tab (false = use current tab)
2020
---@field keep_terminal_focus boolean Keep focus in terminal after opening diff
21+
---@field on_new_file_reject ClaudeCodeNewFileRejectBehavior Behavior when rejecting a new-file diff
2122

2223
-- Model selection option
2324
---@class ClaudeCodeModelOption
@@ -30,6 +31,9 @@
3031
-- Diff layout type alias
3132
---@alias ClaudeCodeDiffLayout "vertical"|"horizontal"
3233

34+
-- Behavior when rejecting new-file diffs
35+
---@alias ClaudeCodeNewFileRejectBehavior "keep_empty"|"close_window"
36+
3337
-- Terminal split side positioning
3438
---@alias ClaudeCodeSplitSide "left"|"right"
3539

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
-- Verifies that rejecting a new-file diff with an empty buffer left open does not crash,
2+
-- and a subsequent write (diff setup) works again.
3+
require("tests.busted_setup")
4+
5+
describe("New file diff: reject then reopen", function()
6+
local diff
7+
8+
before_each(function()
9+
-- Fresh vim mock state
10+
if vim and vim._mock and vim._mock.reset then
11+
vim._mock.reset()
12+
end
13+
14+
-- Minimal logger stub
15+
package.loaded["claudecode.logger"] = {
16+
debug = function() end,
17+
error = function() end,
18+
info = function() end,
19+
warn = function() end,
20+
}
21+
22+
-- Reload diff module cleanly
23+
package.loaded["claudecode.diff"] = nil
24+
diff = require("claudecode.diff")
25+
26+
-- Setup config on diff
27+
diff.setup({
28+
diff_opts = {
29+
layout = "vertical",
30+
open_in_new_tab = false,
31+
keep_terminal_focus = false,
32+
on_new_file_reject = "keep_empty", -- default behavior
33+
},
34+
terminal = {},
35+
})
36+
37+
-- Create an empty unnamed buffer and set it in current window so _create_diff_view_from_window reuses it
38+
local empty_buf = vim.api.nvim_create_buf(false, true)
39+
-- Ensure name is empty and 'modified' is false
40+
vim.api.nvim_buf_set_name(empty_buf, "")
41+
vim.api.nvim_buf_set_option(empty_buf, "modified", false)
42+
43+
-- Make current window use this empty buffer
44+
local current_win = vim.api.nvim_get_current_win()
45+
vim.api.nvim_win_set_buf(current_win, empty_buf)
46+
end)
47+
48+
it("should reuse empty buffer for new-file diff, not delete it on reject, and allow reopening", function()
49+
local tab_name = "✻ [TestNewFile] new.lua ⧉"
50+
local params = {
51+
old_file_path = "/nonexistent/path/to/new.lua", -- ensure new-file scenario
52+
new_file_path = "/tmp/new.lua",
53+
new_file_contents = "print('hello')\n",
54+
tab_name = tab_name,
55+
}
56+
57+
-- Track current window buffer (the reused empty buffer)
58+
local target_win = vim.api.nvim_get_current_win()
59+
local reused_buf = vim.api.nvim_win_get_buf(target_win)
60+
assert.is_true(vim.api.nvim_buf_is_valid(reused_buf))
61+
62+
-- 1) Setup the diff (should reuse the empty buffer)
63+
local setup_ok, setup_err = pcall(function()
64+
diff._setup_blocking_diff(params, function() end)
65+
end)
66+
assert.is_true(setup_ok, "Diff setup failed unexpectedly: " .. tostring(setup_err))
67+
68+
-- Verify state registered (ownership may vary based on window conditions)
69+
local active = diff._get_active_diffs()
70+
assert.is_table(active[tab_name])
71+
-- Ensure the original buffer reference exists and is valid
72+
assert.is_true(vim.api.nvim_buf_is_valid(active[tab_name].original_buffer))
73+
74+
-- 2) Reject the diff; cleanup should NOT delete the reused empty buffer
75+
diff._resolve_diff_as_rejected(tab_name)
76+
77+
-- After reject, the diff state should be removed
78+
local active_after_reject = diff._get_active_diffs()
79+
assert.is_nil(active_after_reject[tab_name])
80+
81+
-- The reused buffer should still be valid (not deleted)
82+
assert.is_true(vim.api.nvim_buf_is_valid(reused_buf))
83+
84+
-- 3) Setup the diff again with the same conditions; should succeed
85+
local setup_ok2, setup_err2 = pcall(function()
86+
diff._setup_blocking_diff(params, function() end)
87+
end)
88+
assert.is_true(setup_ok2, "Second diff setup failed unexpectedly: " .. tostring(setup_err2))
89+
90+
-- Verify new state exists again
91+
local active_again = diff._get_active_diffs()
92+
assert.is_table(active_again[tab_name])
93+
94+
-- Clean up to avoid affecting other tests
95+
diff._cleanup_diff_state(tab_name, "test cleanup")
96+
end)
97+
end)

0 commit comments

Comments
 (0)