Skip to content

feat: add option to control behavior when rejecting new file diffs #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: thomask33/feat_redesign_diff_view_with_horizontal_layout_and_new_tab_options
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions lua/claudecode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ M.defaults = {
open_in_new_tab = false, -- Open diff in a new tab (false = use current tab)
keep_terminal_focus = false, -- If true, moves focus back to terminal after diff opens
hide_terminal_in_new_tab = false, -- If true and opening in a new tab, do not show Claude terminal there
on_new_file_reject = "keep_empty", -- "keep_empty" leaves an empty buffer; "close_window" closes the placeholder split
},
models = {
{ name = "Claude Opus 4.1 (Latest)", value = "opus" },
Expand Down Expand Up @@ -113,6 +114,11 @@ function M.validate(config)
type(config.diff_opts.hide_terminal_in_new_tab) == "boolean",
"diff_opts.hide_terminal_in_new_tab must be a boolean"
)
assert(
type(config.diff_opts.on_new_file_reject) == "string"
and (config.diff_opts.on_new_file_reject == "keep_empty" or config.diff_opts.on_new_file_reject == "close_window"),
"diff_opts.on_new_file_reject must be 'keep_empty' or 'close_window'"
)

-- Validate env
assert(type(config.env) == "table", "env must be a table")
Expand Down
21 changes: 18 additions & 3 deletions lua/claudecode/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -892,9 +892,10 @@ function M._create_diff_view_from_window(
terminal_win_in_new_tab,
existing_buffer
)
local original_buffer_created_by_plugin = false

-- If no target window provided, create a new window in suitable location
if not target_window then
-- If we have a terminal window in the new tab, we're already positioned correctly
if terminal_win_in_new_tab then
-- We're already in the main area after display_terminal_in_new_tab
target_window = vim.api.nvim_get_current_win()
Expand Down Expand Up @@ -929,8 +930,15 @@ function M._create_diff_view_from_window(
original_window = choice.original_win
end

-- For new files, we create an empty buffer for the original side
if is_new_file then
original_buffer_created_by_plugin = true
end

-- Load the original-side buffer into the chosen window
local original_buffer = load_original_buffer(original_window, old_file_path, is_new_file, existing_buffer)

-- Set up the proposed buffer and finalize the diff layout
local new_win = setup_new_buffer(
original_window,
original_buffer,
Expand All @@ -945,6 +953,7 @@ function M._create_diff_view_from_window(
new_window = new_win,
target_window = original_window,
original_buffer = original_buffer,
original_buffer_created_by_plugin = original_buffer_created_by_plugin,
}
end

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

-- Clean up the original buffer if it was created for a new file
if diff_data.is_new_file and diff_data.original_buffer and vim.api.nvim_buf_is_valid(diff_data.original_buffer) then
-- Clean up the original buffer only if it was created by the plugin for a new file
if
diff_data.is_new_file
and diff_data.original_buffer
and vim.api.nvim_buf_is_valid(diff_data.original_buffer)
and diff_data.original_buffer_created_by_plugin
then
pcall(vim.api.nvim_buf_delete, diff_data.original_buffer, { force = true })
end

Expand Down Expand Up @@ -1177,6 +1191,7 @@ function M._setup_blocking_diff(params, resolution_callback)
new_window = diff_info.new_window,
target_window = diff_info.target_window,
original_buffer = diff_info.original_buffer,
original_buffer_created_by_plugin = diff_info.original_buffer_created_by_plugin,
original_cursor_pos = original_cursor_pos,
original_tab_number = original_tab_number,
created_new_tab = created_new_tab,
Expand Down
4 changes: 4 additions & 0 deletions lua/claudecode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
---@field open_in_new_tab boolean Open diff in a new tab (false = use current tab)
---@field keep_terminal_focus boolean Keep focus in terminal after opening diff
---@field hide_terminal_in_new_tab boolean Hide Claude terminal in newly created diff tab
---@field on_new_file_reject ClaudeCodeNewFileRejectBehavior Behavior when rejecting a new-file diff

-- Model selection option
---@class ClaudeCodeModelOption
Expand All @@ -31,6 +32,9 @@
-- Diff layout type alias
---@alias ClaudeCodeDiffLayout "vertical"|"horizontal"

-- Behavior when rejecting new-file diffs
---@alias ClaudeCodeNewFileRejectBehavior "keep_empty"|"close_window"

-- Terminal split side positioning
---@alias ClaudeCodeSplitSide "left"|"right"

Expand Down
97 changes: 97 additions & 0 deletions tests/unit/new_file_reject_then_reopen_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
-- Verifies that rejecting a new-file diff with an empty buffer left open does not crash,
-- and a subsequent write (diff setup) works again.
require("tests.busted_setup")

describe("New file diff: reject then reopen", function()
local diff

before_each(function()
-- Fresh vim mock state
if vim and vim._mock and vim._mock.reset then
vim._mock.reset()
end

-- Minimal logger stub
package.loaded["claudecode.logger"] = {
debug = function() end,
error = function() end,
info = function() end,
warn = function() end,
}

-- Reload diff module cleanly
package.loaded["claudecode.diff"] = nil
diff = require("claudecode.diff")

-- Setup config on diff
diff.setup({
diff_opts = {
layout = "vertical",
open_in_new_tab = false,
keep_terminal_focus = false,
on_new_file_reject = "keep_empty", -- default behavior
},
terminal = {},
})

-- Create an empty unnamed buffer and set it in current window so _create_diff_view_from_window reuses it
local empty_buf = vim.api.nvim_create_buf(false, true)
-- Ensure name is empty and 'modified' is false
vim.api.nvim_buf_set_name(empty_buf, "")
vim.api.nvim_buf_set_option(empty_buf, "modified", false)

-- Make current window use this empty buffer
local current_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(current_win, empty_buf)
end)

it("should reuse empty buffer for new-file diff, not delete it on reject, and allow reopening", function()
local tab_name = "✻ [TestNewFile] new.lua ⧉"
local params = {
old_file_path = "/nonexistent/path/to/new.lua", -- ensure new-file scenario
new_file_path = "/tmp/new.lua",
new_file_contents = "print('hello')\n",
tab_name = tab_name,
}

-- Track current window buffer (the reused empty buffer)
local target_win = vim.api.nvim_get_current_win()
local reused_buf = vim.api.nvim_win_get_buf(target_win)
assert.is_true(vim.api.nvim_buf_is_valid(reused_buf))

-- 1) Setup the diff (should reuse the empty buffer)
local setup_ok, setup_err = pcall(function()
diff._setup_blocking_diff(params, function() end)
end)
assert.is_true(setup_ok, "Diff setup failed unexpectedly: " .. tostring(setup_err))

-- Verify state registered (ownership may vary based on window conditions)
local active = diff._get_active_diffs()
assert.is_table(active[tab_name])
-- Ensure the original buffer reference exists and is valid
assert.is_true(vim.api.nvim_buf_is_valid(active[tab_name].original_buffer))

-- 2) Reject the diff; cleanup should NOT delete the reused empty buffer
diff._resolve_diff_as_rejected(tab_name)

-- After reject, the diff state should be removed
local active_after_reject = diff._get_active_diffs()
assert.is_nil(active_after_reject[tab_name])

-- The reused buffer should still be valid (not deleted)
assert.is_true(vim.api.nvim_buf_is_valid(reused_buf))

-- 3) Setup the diff again with the same conditions; should succeed
local setup_ok2, setup_err2 = pcall(function()
diff._setup_blocking_diff(params, function() end)
end)
assert.is_true(setup_ok2, "Second diff setup failed unexpectedly: " .. tostring(setup_err2))

-- Verify new state exists again
local active_again = diff._get_active_diffs()
assert.is_table(active_again[tab_name])

-- Clean up to avoid affecting other tests
diff._cleanup_diff_state(tab_name, "test cleanup")
end)
end)