|
| 1 | +-- claudecode.nvim (local checkout) configured exactly like the reporter's |
| 2 | +-- repro.lua for issue #218, plus a deterministic, Claude-free trigger |
| 3 | +-- (:Repro218) that recreates the precise state that crashes Neovim. |
| 4 | +-- |
| 5 | +-- Root cause (verified): accepting (:w) a NEW-file *markdown* diff that was |
| 6 | +-- opened in a NEW TAB (diff_opts.open_in_new_tab = true) tears the diff down via |
| 7 | +-- diff.close_diff_by_tab_name -> _cleanup_diff_state, which runs `:tabclose` on |
| 8 | +-- the tab whose windows are STILL in diff mode. When render-markdown.nvim is |
| 9 | +-- attached to that markdown buffer and the Claude terminal is open in the other |
| 10 | +-- tab, that `:tabclose` abnormally terminates Neovim (SIGSEGV / exit 139 for the |
| 11 | +-- reporter). Removing render-markdown, or turning diff mode off before the |
| 12 | +-- teardown (the reporter's `diffoff` workaround), avoids it. |
| 13 | +-- |
| 14 | +-- :Repro218 reproduces this with NO real Claude needed: it opens a harmless |
| 15 | +-- Claude *terminal* (a sleeping process) so the layout matches real usage, then |
| 16 | +-- opens the new-tab markdown diff through the SAME coroutine machinery the |
| 17 | +-- openDiff MCP tool uses (diff.open_diff_blocking), wiring the deferred response |
| 18 | +-- so that accepting the diff simulates Claude writing the file and sending |
| 19 | +-- close_tab. Focus is left in the proposed pane: just press `:w` to crash. |
| 20 | +-- |
| 21 | +-- We load eagerly (lazy = false) so the diff module is configured at startup. |
| 22 | +return { |
| 23 | + "coder/claudecode.nvim", |
| 24 | + dir = vim.g.claudecode_dev_dir, |
| 25 | + dependencies = { "folke/snacks.nvim" }, |
| 26 | + lazy = false, |
| 27 | + keys = { |
| 28 | + { "<C-,>", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" }, |
| 29 | + }, |
| 30 | + ---@type PartialClaudeCodeConfig |
| 31 | + opts = { |
| 32 | + terminal = { split_side = "left", split_width_percentage = 0.5 }, |
| 33 | + diff_opts = { open_in_new_tab = true, hide_terminal_in_new_tab = true }, |
| 34 | + }, |
| 35 | + config = function(_, opts) |
| 36 | + require("claudecode").setup(opts) |
| 37 | + |
| 38 | + -- Markdown payload with the structures render-markdown attaches to (headings, |
| 39 | + -- fenced code, lists, blockquotes, tables). |
| 40 | + local function payload() |
| 41 | + return table.concat({ |
| 42 | + "# Issue 218 Repro", |
| 43 | + "", |
| 44 | + "This is a **new** markdown file proposed by Claude.", |
| 45 | + "", |
| 46 | + "## Section heading", |
| 47 | + "", |
| 48 | + "- item one", |
| 49 | + "- item two", |
| 50 | + " - nested", |
| 51 | + "", |
| 52 | + "```lua", |
| 53 | + "local x = 1", |
| 54 | + "print(x)", |
| 55 | + "```", |
| 56 | + "", |
| 57 | + "> A blockquote for render-markdown to decorate.", |
| 58 | + "", |
| 59 | + "| col a | col b |", |
| 60 | + "| ----- | ----- |", |
| 61 | + "| 1 | 2 |", |
| 62 | + "", |
| 63 | + "1. first", |
| 64 | + "2. second", |
| 65 | + "", |
| 66 | + }, "\n") .. "\n" |
| 67 | + end |
| 68 | + |
| 69 | + -- Open a harmless Claude *terminal* (a sleeping process) without stealing |
| 70 | + -- focus, so the window layout matches real usage. The crash only reproduces |
| 71 | + -- when the Claude terminal is present in the original tab. |
| 72 | + local function ensure_dummy_terminal() |
| 73 | + local term = require("claudecode.terminal") |
| 74 | + if term.get_active_terminal_bufnr and term.get_active_terminal_bufnr() then |
| 75 | + return -- a terminal is already open (e.g. real :ClaudeCode); leave it |
| 76 | + end |
| 77 | + -- Override the launched command so no real Claude/API is needed. |
| 78 | + term.defaults.terminal_cmd = "sh -c 'while :; do sleep 3600; done'" |
| 79 | + pcall(vim.cmd, "ClaudeCodeStart") |
| 80 | + pcall(function() |
| 81 | + term.toggle_open_no_focus() |
| 82 | + end) |
| 83 | + end |
| 84 | + |
| 85 | + -- Recreate the exact openDiff flow the server runs: open_diff_blocking inside |
| 86 | + -- a coroutine, with _G.claude_deferred_responses wired so the :w resolution |
| 87 | + -- resumes the coroutine and then simulates Claude (write the file to disk, |
| 88 | + -- then send close_tab via close_diff_by_tab_name on a later tick). |
| 89 | + local function repro_218() |
| 90 | + ensure_dummy_terminal() |
| 91 | + local diff = require("claudecode.diff") |
| 92 | + local new_file = vim.fn.tempname() .. "_issue218.md" |
| 93 | + pcall(os.remove, new_file) -- ensure it does not exist => is_new_file = true |
| 94 | + local contents = payload() |
| 95 | + local tab_name = "✻ [Claude Code] issue218.md ⧉" |
| 96 | + |
| 97 | + _G.claude_deferred_responses = _G.claude_deferred_responses or {} |
| 98 | + local co = coroutine.create(function() |
| 99 | + return diff.open_diff_blocking(new_file, new_file, contents, tab_name, nil) |
| 100 | + end) |
| 101 | + _G.claude_deferred_responses[tostring(co)] = function() |
| 102 | + -- Claude received FILE_SAVED: write the file, then send close_tab. |
| 103 | + vim.schedule(function() |
| 104 | + local fh = io.open(new_file, "w") |
| 105 | + if fh then |
| 106 | + fh:write(contents) |
| 107 | + fh:close() |
| 108 | + end |
| 109 | + vim.schedule(function() |
| 110 | + pcall(diff.close_diff_by_tab_name, tab_name) |
| 111 | + end) |
| 112 | + end) |
| 113 | + end |
| 114 | + |
| 115 | + -- Defer the open so the terminal split settles first, then resume the |
| 116 | + -- coroutine (which sets up the diff) and leave focus in the proposed pane. |
| 117 | + vim.schedule(function() |
| 118 | + coroutine.resume(co) |
| 119 | + vim.schedule(function() |
| 120 | + for _, b in ipairs(vim.api.nvim_list_bufs()) do |
| 121 | + local name = vim.api.nvim_buf_get_name(b) |
| 122 | + if name:match("proposed") and vim.bo[b].buftype == "acwrite" then |
| 123 | + local win = vim.fn.win_findbuf(b)[1] |
| 124 | + if win then |
| 125 | + vim.api.nvim_set_current_win(win) |
| 126 | + end |
| 127 | + end |
| 128 | + end |
| 129 | + vim.api.nvim_echo({ |
| 130 | + { |
| 131 | + "Repro218 ready. Press :w in the proposed pane to accept (Neovim crashes — issue #218).", |
| 132 | + "WarningMsg", |
| 133 | + }, |
| 134 | + }, false, {}) |
| 135 | + end) |
| 136 | + end) |
| 137 | + end |
| 138 | + |
| 139 | + vim.api.nvim_create_user_command("Repro218", repro_218, { |
| 140 | + desc = "Set up the issue #218 crash: open a NEW-file markdown diff in a new tab; then :w", |
| 141 | + }) |
| 142 | + |
| 143 | + vim.api.nvim_create_user_command("Repro218Reset", function() |
| 144 | + require("claudecode.diff")._cleanup_all_active_diffs("repro reset") |
| 145 | + vim.cmd("silent! tabonly!") |
| 146 | + vim.cmd("silent! only!") |
| 147 | + vim.cmd("silent! enew!") |
| 148 | + end, { desc = "Reset the #218 repro layout" }) |
| 149 | + end, |
| 150 | +} |
0 commit comments