Skip to content

Commit 00cc3d9

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 763ee39 commit 00cc3d9

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

lua/claudecode/config.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ M.defaults = {
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
2525
hide_terminal_in_new_tab = false, -- If true and opening in a new tab, do not show Claude terminal there
26+
on_new_file_reject = "keep_empty", -- "keep_empty" leaves an empty buffer; "close_window" closes the placeholder split
2627
},
2728
models = {
2829
{ name = "Claude Opus 4.1 (Latest)", value = "opus" },
@@ -113,6 +114,11 @@ function M.validate(config)
113114
type(config.diff_opts.hide_terminal_in_new_tab) == "boolean",
114115
"diff_opts.hide_terminal_in_new_tab must be a boolean"
115116
)
117+
assert(
118+
type(config.diff_opts.on_new_file_reject) == "string"
119+
and (config.diff_opts.on_new_file_reject == "keep_empty" or config.diff_opts.on_new_file_reject == "close_window"),
120+
"diff_opts.on_new_file_reject must be 'keep_empty' or 'close_window'"
121+
)
116122

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

lua/claudecode/diff.lua

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -892,9 +892,10 @@ function M._create_diff_view_from_window(
892892
terminal_win_in_new_tab,
893893
existing_buffer
894894
)
895+
local original_buffer_created_by_plugin = false
896+
895897
-- If no target window provided, create a new window in suitable location
896898
if not target_window then
897-
-- If we have a terminal window in the new tab, we're already positioned correctly
898899
if terminal_win_in_new_tab then
899900
-- We're already in the main area after display_terminal_in_new_tab
900901
target_window = vim.api.nvim_get_current_win()
@@ -929,8 +930,15 @@ function M._create_diff_view_from_window(
929930
original_window = choice.original_win
930931
end
931932

933+
-- For new files, we create an empty buffer for the original side
934+
if is_new_file then
935+
original_buffer_created_by_plugin = true
936+
end
937+
938+
-- Load the original-side buffer into the chosen window
932939
local original_buffer = load_original_buffer(original_window, old_file_path, is_new_file, existing_buffer)
933940

941+
-- Set up the proposed buffer and finalize the diff layout
934942
local new_win = setup_new_buffer(
935943
original_window,
936944
original_buffer,
@@ -945,6 +953,7 @@ function M._create_diff_view_from_window(
945953
new_window = new_win,
946954
target_window = original_window,
947955
original_buffer = original_buffer,
956+
original_buffer_created_by_plugin = original_buffer_created_by_plugin,
948957
}
949958
end
950959

@@ -1030,8 +1039,13 @@ function M._cleanup_diff_state(tab_name, reason)
10301039
pcall(vim.api.nvim_buf_delete, diff_data.new_buffer, { force = true })
10311040
end
10321041

1033-
-- Clean up the original buffer if it was created for a new file
1034-
if diff_data.is_new_file and diff_data.original_buffer and vim.api.nvim_buf_is_valid(diff_data.original_buffer) then
1042+
-- Clean up the original buffer only if it was created by the plugin for a new file
1043+
if
1044+
diff_data.is_new_file
1045+
and diff_data.original_buffer
1046+
and vim.api.nvim_buf_is_valid(diff_data.original_buffer)
1047+
and diff_data.original_buffer_created_by_plugin
1048+
then
10351049
pcall(vim.api.nvim_buf_delete, diff_data.original_buffer, { force = true })
10361050
end
10371051

@@ -1177,6 +1191,7 @@ function M._setup_blocking_diff(params, resolution_callback)
11771191
new_window = diff_info.new_window,
11781192
target_window = diff_info.target_window,
11791193
original_buffer = diff_info.original_buffer,
1194+
original_buffer_created_by_plugin = diff_info.original_buffer_created_by_plugin,
11801195
original_cursor_pos = original_cursor_pos,
11811196
original_tab_number = original_tab_number,
11821197
created_new_tab = created_new_tab,

lua/claudecode/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
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
2121
---@field hide_terminal_in_new_tab boolean Hide Claude terminal in newly created diff tab
22+
---@field on_new_file_reject ClaudeCodeNewFileRejectBehavior Behavior when rejecting a new-file diff
2223

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

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

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)