|
| 1 | +-- Repro fixture for issue #238: "[BUG] Rejecting with `:q` does not work". |
| 2 | +-- |
| 3 | +-- Scenario this fixture is built to demonstrate: |
| 4 | +-- 1. claudecode.nvim is configured exactly like the reporter: |
| 5 | +-- - terminal.provider = "none" (Claude runs in an *external* terminal, |
| 6 | +-- e.g. sidekick.nvim — Neovim manages no terminal of its own) |
| 7 | +-- - diff_opts.open_in_new_tab = true |
| 8 | +-- - diff_opts.layout = "vertical" |
| 9 | +-- 2. Claude opens a diff via the `openDiff` MCP tool. It lands in a NEW tab |
| 10 | +-- with the original file on the left and the proposed buffer on the right. |
| 11 | +-- 3. The user tries to REJECT the change with `:q` (as the README documents: |
| 12 | +-- "Reject: `:q` or <leader>ad"). |
| 13 | +-- 4. EXPECTED: the diff is rejected (Claude is told DIFF_REJECTED) and the |
| 14 | +-- tab closes. |
| 15 | +-- ACTUAL (the bug): `:q` only closes the proposed window; the buffer is |
| 16 | +-- merely *hidden* (it is a scratch buffer => bufhidden=hide), so none of |
| 17 | +-- the BufDelete/BufUnload/BufWipeout autocmds that drive rejection fire. |
| 18 | +-- The diff stays "pending" forever and the tab lingers. |
| 19 | +-- |
| 20 | +-- This fixture mirrors `remote-diff` but uses the reporter's exact config and |
| 21 | +-- exposes a `:DiffStateFile` command that writes a machine-readable JSON |
| 22 | +-- snapshot (window/tab counts, per-diff status, and the proposed buffer's |
| 23 | +-- bufhidden) so automation can assert on the bug without scraping the screen. |
| 24 | +-- |
| 25 | +-- Usage (from repo root): |
| 26 | +-- source fixtures/nvim-aliases.sh |
| 27 | +-- vv issue-238 example/target.txt |
| 28 | +-- # or: NVIM_APPNAME=issue-238 XDG_CONFIG_HOME=fixtures nvim fixtures/issue-238/example/target.txt |
| 29 | +-- |
| 30 | +-- Then drive the MCP side (play the role of Claude) with: |
| 31 | +-- scripts/repro_issue_238.sh |
| 32 | + |
| 33 | +local config_dir = vim.fn.stdpath("config") |
| 34 | +local repo_root = vim.fn.fnamemodify(config_dir, ":h:h") |
| 35 | +vim.opt.rtp:prepend(repo_root) |
| 36 | + |
| 37 | +vim.g.mapleader = " " |
| 38 | +vim.g.maplocalleader = "\\" |
| 39 | + |
| 40 | +local ok, claudecode = pcall(require, "claudecode") |
| 41 | +assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode)) |
| 42 | + |
| 43 | +-- The reporter's exact config uses open_in_new_tab = true, but the underlying |
| 44 | +-- bug is not tab-specific. Set REPRO238_NEW_TAB=0 to probe the default |
| 45 | +-- (same-tab) layout and confirm `:q` rejection is broken there too. |
| 46 | +local open_in_new_tab = os.getenv("REPRO238_NEW_TAB") ~= "0" |
| 47 | + |
| 48 | +claudecode.setup({ |
| 49 | + auto_start = false, |
| 50 | + -- Quiet logging keeps the diff UI clean for screenshots / automation and |
| 51 | + -- avoids the hit-enter prompt that long :messages can trigger. |
| 52 | + log_level = "warn", |
| 53 | + terminal = { |
| 54 | + -- The reporter uses sidekick.nvim to run Claude in an external terminal, |
| 55 | + -- so claudecode itself manages no terminal: provider = "none". |
| 56 | + provider = "none", |
| 57 | + }, |
| 58 | + diff_opts = { |
| 59 | + layout = "vertical", |
| 60 | + open_in_new_tab = open_in_new_tab, |
| 61 | + keep_terminal_focus = false, |
| 62 | + }, |
| 63 | +}) |
| 64 | + |
| 65 | +local function ensure_started() |
| 66 | + local ok_start, started_or_err, port_or_err = pcall(function() |
| 67 | + return claudecode.start(false) |
| 68 | + end) |
| 69 | + if not ok_start then |
| 70 | + vim.notify("ClaudeCode start crashed: " .. tostring(started_or_err), vim.log.levels.ERROR) |
| 71 | + return false |
| 72 | + end |
| 73 | + if started_or_err or port_or_err == "Already running" then |
| 74 | + return true |
| 75 | + end |
| 76 | + vim.notify("ClaudeCode failed to start: " .. tostring(port_or_err), vim.log.levels.ERROR) |
| 77 | + return false |
| 78 | +end |
| 79 | + |
| 80 | +ensure_started() |
| 81 | + |
| 82 | +-- Build a snapshot of everything that matters for this bug. |
| 83 | +local function diff_state() |
| 84 | + local diff = require("claudecode.diff") |
| 85 | + local active = diff._get_active_diffs() |
| 86 | + |
| 87 | + local diffs = {} |
| 88 | + for tab_name, data in pairs(active) do |
| 89 | + local proposed_bufhidden = nil |
| 90 | + if data.new_buffer and vim.api.nvim_buf_is_valid(data.new_buffer) then |
| 91 | + proposed_bufhidden = vim.api.nvim_buf_get_option(data.new_buffer, "bufhidden") |
| 92 | + end |
| 93 | + diffs[#diffs + 1] = { |
| 94 | + tab_name = tab_name, |
| 95 | + status = data.status or "?", |
| 96 | + created_new_tab = data.created_new_tab or false, |
| 97 | + new_buffer = data.new_buffer, |
| 98 | + new_buffer_valid = data.new_buffer and vim.api.nvim_buf_is_valid(data.new_buffer) or false, |
| 99 | + new_buffer_loaded = data.new_buffer and vim.api.nvim_buf_is_loaded(data.new_buffer) or false, |
| 100 | + proposed_bufhidden = proposed_bufhidden, |
| 101 | + } |
| 102 | + end |
| 103 | + table.sort(diffs, function(a, b) |
| 104 | + return tostring(a.tab_name) < tostring(b.tab_name) |
| 105 | + end) |
| 106 | + |
| 107 | + return { |
| 108 | + windows = #vim.api.nvim_list_wins(), |
| 109 | + tabpages = #vim.api.nvim_list_tabpages(), |
| 110 | + active_diffs = #diffs, |
| 111 | + diffs = diffs, |
| 112 | + } |
| 113 | +end |
| 114 | + |
| 115 | +-- Human-readable variant. |
| 116 | +vim.api.nvim_create_user_command("DiffState", function() |
| 117 | + local s = diff_state() |
| 118 | + local lines = { |
| 119 | + ("windows=%d tabpages=%d active_diffs=%d"):format(s.windows, s.tabpages, s.active_diffs), |
| 120 | + } |
| 121 | + for _, d in ipairs(s.diffs) do |
| 122 | + lines[#lines + 1] = (" [%s] new_tab=%s bufhidden=%s loaded=%s %s"):format( |
| 123 | + d.status, |
| 124 | + tostring(d.created_new_tab), |
| 125 | + tostring(d.proposed_bufhidden), |
| 126 | + tostring(d.new_buffer_loaded), |
| 127 | + d.tab_name |
| 128 | + ) |
| 129 | + end |
| 130 | + vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO) |
| 131 | +end, { desc = "Show window/tab count + active claudecode diffs" }) |
| 132 | + |
| 133 | +-- Scriptable variant: writes the state as JSON to a file so external automation |
| 134 | +-- can assert on it without scraping the message area. |
| 135 | +vim.api.nvim_create_user_command("DiffStateFile", function(opts) |
| 136 | + local path = opts.args ~= "" and opts.args or (vim.fn.stdpath("cache") .. "/diff_state.json") |
| 137 | + local s = diff_state() |
| 138 | + vim.fn.writefile({ vim.json.encode(s) }, path) |
| 139 | +end, { nargs = "?", desc = "Write window/diff state as JSON to a file" }) |
| 140 | + |
| 141 | +vim.keymap.set("n", "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", { desc = "Accept diff" }) |
| 142 | +vim.keymap.set("n", "<leader>ad", "<cmd>ClaudeCodeDiffDeny<cr>", { desc = "Deny diff" }) |
| 143 | +vim.keymap.set("n", "<leader>as", "<cmd>DiffState<cr>", { desc = "Show diff state" }) |
0 commit comments