Skip to content
Merged
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
90 changes: 90 additions & 0 deletions fixtures/issue-289/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Fixture: issue #289 — path-substring tree misclassification

Reproduces [#289](https://github.com/coder/claudecode.nvim/issues/289):

> `ClaudeCodeSend` misclassifies a regular file buffer as a "tree buffer" when
> the **file path** (not filetype) contains the substring `neo-tree`,
> `NvimTree`, or `minifiles://`.

## Root cause

`handle_send_normal` and `handle_send_visual` in `lua/claudecode/init.lua`
decide whether the current buffer is a file-explorer ("tree") buffer using both
the filetype **and** a substring match against the buffer **name**:

```lua
local is_tree_buffer = current_ft == "NvimTree"
or current_ft == "neo-tree"
or current_ft == "oil"
or current_ft == "minifiles"
or current_ft == "netrw"
or current_ft == "snacks_picker_list" -- (normal handler only)
or string.match(current_bufname, "neo%-tree") -- ← false positive
or string.match(current_bufname, "NvimTree") -- ← false positive
or string.match(current_bufname, "minifiles://")
```

A perfectly ordinary `lua` file whose **path** merely contains one of those
substrings (e.g. a plugin spec named `_neo-tree_.lua`, or anything under a
`nvim-tree-config/` directory) is therefore treated as a tree buffer.

## Files

| File | Path contains | Classified as tree? |
| ---------------------------- | ------------- | ------------------- |
| `lua/plugins/_neo-tree_.lua` | `neo-tree` | **yes (bug)** |
| `lua/NvimTree_settings.lua` | `NvimTree` | **yes (bug)** |
| `lua/regular_plugin.lua` | _(none)_ | no (control) |

All three are plain `filetype=lua` files. Only the path differs.

## Reproduce manually

```bash
source fixtures/nvim-aliases.sh
vv issue-289 'lua/plugins/_neo-tree_.lua' # AFFECTED
# (or) vv issue-289 'lua/regular_plugin.lua' # CONTROL
```

or directly:

```bash
NVIM_APPNAME=issue-289 XDG_CONFIG_HOME=fixtures \
nvim fixtures/issue-289/lua/plugins/_neo-tree_.lua
```

Then, in the buffer:

1. **Visual path** (the README-default `<leader>as`, which maps to
`<cmd>ClaudeCodeSend<cr>` and keeps visual mode): visually select a few lines
(`Vjj`) and press `<leader>as`.
→ `ClaudeCode Error [ClaudeCode] [command] [ERROR] ClaudeCodeSend_visual->TreeAdd: Not in visual mode (current mode: n)`
2. **Range path**: visually select a few lines, then `:'<,'>ClaudeCodeSend`.
→ `ClaudeCode Error [ClaudeCode] [command] [ERROR] ClaudeCodeSend->TreeAdd: Not in a supported tree buffer (current filetype: lua)`

Doing the same in `regular_plugin.lua` sends the selection with no error.

## Helper commands (provided by this fixture's `init.lua`)

- `:ReproState [path]` — write the current buffer's tree-classification state
(`filetype`, `bufname`, `matches_filetype`, `legacy_path_substring_match`,
`is_tree_buffer`, …) as JSON. `is_tree_buffer` mirrors the plugin's current
(filetype-only) predicate. On the affected files you will see
`legacy_path_substring_match=true` (the pre-#289 root cause) while
`matches_filetype=false`; on **unfixed** code `is_tree_buffer` is `true`
(the bug), and on **fixed** code it is `false` (correctly a normal buffer).
- `:ReproDump [path]` — write all captured ClaudeCode notifications as JSON.
- `:ReproClear` — clear the captured-notification buffer.

## Headless / automated reproduction

For a deterministic, CI-style check that drives the real `:ClaudeCodeSend`
command and exits non-zero when the bug is present:

```bash
nvim --headless -u NONE -l scripts/repro_issue_289.lua; echo "exit=$?"
```

`exit=1` ⇒ reproduced (a `lua` buffer whose path contains `neo-tree` is
misrouted into tree extraction while the control buffer sends correctly).
`exit=0` ⇒ fixed.
145 changes: 145 additions & 0 deletions fixtures/issue-289/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
-- Repro fixture for issue #289:
-- "[BUG] ClaudeCodeSend misclassifies regular buffers as tree buffers when
-- file path contains 'neo-tree' or 'NvimTree'"
--
-- Root cause (lua/claudecode/init.lua, handle_send_normal + handle_send_visual):
-- a buffer is classified as a file-explorer ("tree") buffer not only by its
-- FILETYPE but also by a substring match against its BUFFER NAME:
--
-- local is_tree_buffer = current_ft == "NvimTree"
-- or current_ft == "neo-tree"
-- or ...
-- or string.match(current_bufname, "neo%-tree") -- false positive
-- or string.match(current_bufname, "NvimTree") -- false positive
-- or string.match(current_bufname, "minifiles://")
--
-- So a perfectly ordinary file whose PATH happens to contain one of those
-- substrings (e.g. a Neovim plugin spec at lua/plugins/_neo-tree_.lua, or any
-- file under a directory called nvim-tree-config/) is mistaken for a tree.
--
-- Symptom, visual path (the README-default `<leader>as` keymap uses
-- `<cmd>ClaudeCodeSend<cr>`, which KEEPS the buffer in visual mode):
-- 1. wrapper sees mode == "v" -> exit_visual_and_schedule(visual_handler)
-- 2. capture_visual_selection_data() returns nil (get_tree_state() is nil for
-- a real `lua` buffer) and <Esc> is fed, dropping us into normal mode
-- 3. handle_send_visual: is_tree_buffer == true (bufname match) -> takes the
-- tree branch -> get_files_from_visual_selection(nil) -> validate_visual_mode()
-- now fails because the mode is "n" -> logs:
-- ClaudeCodeSend_visual->TreeAdd: Not in visual mode (current mode: n)
-- ...and nothing is ever sent to Claude.
--
-- Symptom, normal/range path (`:'<,'>ClaudeCodeSend`): mode is "n", so
-- handle_send_normal runs; is_tree_buffer is still true, so it calls
-- integrations.get_selected_files_from_tree(), which fails with:
-- ClaudeCodeSend->TreeAdd: Not in a supported tree buffer (current filetype: lua)
--
-- A control file whose path has NONE of those substrings works correctly.
--
-- Usage (from repo root):
-- source fixtures/nvim-aliases.sh
-- vv issue-289 'lua/plugins/_neo-tree_.lua' # AFFECTED (path has 'neo-tree')
-- vv issue-289 'lua/regular_plugin.lua' # CONTROL (no substring)
-- or directly:
-- NVIM_APPNAME=issue-289 XDG_CONFIG_HOME=fixtures \
-- nvim fixtures/issue-289/lua/plugins/_neo-tree_.lua
--
-- Commands provided for automation:
-- :ReproDump [path] write captured ClaudeCode notifications as JSON to `path`
-- (defaults to stdpath('cache')/issue289_notifications.json)
-- :ReproClear clear the captured-notification buffer

local config_dir = vim.fn.stdpath("config")
local repo_root = vim.fn.fnamemodify(config_dir, ":h:h")
vim.opt.rtp:prepend(repo_root)

vim.g.mapleader = " "
vim.g.maplocalleader = "\\"

-- A little extra command height keeps long error notifications from triggering
-- the blocking hit-enter prompt while driving Neovim with agent-tty.
vim.o.cmdheight = 3
vim.o.more = false

-- Capture every ClaudeCode notification so automation can assert on it without
-- scraping the screen. We still forward to the original handler so the error is
-- also visible in a screenshot.
_G._repro_notifications = {}
local original_notify = vim.notify
vim.notify = function(msg, level, opts)
table.insert(_G._repro_notifications, {
message = tostring(msg),
level = level,
title = opts and opts.title or nil,
})
return original_notify(msg, level, opts)
end

local ok, claudecode = pcall(require, "claudecode")
assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode))

claudecode.setup({
auto_start = false,
-- ERROR notifications fire regardless of log_level; "info" keeps the rest quiet.
log_level = "info",
-- No in-editor terminal needed to reproduce the misclassification.
terminal = {
provider = "none",
},
})

-- Best-effort: start the server so the CONTROL path can actually queue an
-- at-mention. The bug itself does NOT require a running server or a connected
-- client -- the misclassification happens before any server interaction.
pcall(function()
claudecode.start(false)
end)

-- README-default visual keymap: this is the exact mapping the docs recommend
-- and the one the reporter uses. `<cmd>...<cr>` PRESERVES visual mode, which is
-- what routes the request into the (buggy) visual tree-extraction path.
vim.keymap.set("v", "<leader>as", "<cmd>ClaudeCodeSend<cr>", { desc = "Send to Claude" })

vim.api.nvim_create_user_command("ReproDump", function(opts)
local path = opts.args ~= "" and opts.args or (vim.fn.stdpath("cache") .. "/issue289_notifications.json")
vim.fn.writefile({ vim.json.encode(_G._repro_notifications) }, path)
original_notify("ReproDump -> " .. path .. " (" .. #_G._repro_notifications .. " notifications)", vim.log.levels.INFO)
end, { nargs = "?", desc = "Write captured ClaudeCode notifications as JSON" })

vim.api.nvim_create_user_command("ReproClear", function()
_G._repro_notifications = {}
end, { desc = "Clear captured ClaudeCode notifications" })

-- Diagnostic snapshot: records how the CURRENT buffer would be classified so
-- automation can assert on the misclassification directly (no screen-scraping).
vim.api.nvim_create_user_command("ReproState", function(opts)
local path = opts.args ~= "" and opts.args or (vim.fn.stdpath("cache") .. "/issue289_state.json")
local buf = 0
local ft = vim.bo[buf].filetype
local bufname = vim.api.nvim_buf_get_name(buf)
-- `is_tree_buffer` mirrors the plugin's CURRENT predicate (post-#289):
-- filetype only. On the fixed plugin, running this in `_neo-tree_.lua` reports
-- is_tree_buffer=false, i.e. the file is correctly treated as a normal buffer.
local matches_filetype = ft == "NvimTree" or ft == "neo-tree" or ft == "oil" or ft == "minifiles" or ft == "netrw"
-- Legacy pre-#289 signal: the buffer-NAME substring match that USED to also
-- flip is_tree_buffer to true (the root cause of #289). Reported for
-- diagnostics so the fixture still shows why ordinary files misfired before
-- the fix: legacy_path_substring_match=true while is_tree_buffer=false means
-- "this file would have been misclassified by the old code".
local legacy_path_substring_match = (string.match(bufname, "neo%-tree") ~= nil)
or (string.match(bufname, "NvimTree") ~= nil)
or (string.match(bufname, "minifiles://") ~= nil)
local state = {
filetype = ft,
bufname = bufname,
matches_filetype = matches_filetype,
legacy_path_substring_match = legacy_path_substring_match,
is_tree_buffer = matches_filetype,
has_send_command = vim.fn.exists(":ClaudeCodeSend") == 2,
server_running = (function()
local ok_cc, cc = pcall(require, "claudecode")
return ok_cc and cc.state and cc.state.server ~= nil or false
end)(),
}
vim.fn.writefile({ vim.json.encode(state) }, path)
original_notify("ReproState -> " .. path, vim.log.levels.INFO)
end, { nargs = "?", desc = "Write current-buffer tree-classification state as JSON" })
16 changes: 16 additions & 0 deletions fixtures/issue-289/lua/NvimTree_settings.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- AFFECTED FILE (NvimTree variant) for issue #289.
--
-- Same bug as `_neo-tree_.lua`, but triggered by the
-- `string.match(current_bufname, "NvimTree")` substring check. Filetype is
-- still `lua`; only the path contains "NvimTree".

local M = {}

M.opts = {
sort = { sorter = "case_sensitive" },
view = { width = 30 },
renderer = { group_empty = true },
filters = { dotfiles = true },
}

return M
27 changes: 27 additions & 0 deletions fixtures/issue-289/lua/plugins/_neo-tree_.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- AFFECTED FILE for issue #289.
--
-- This is an utterly ordinary Lua file. Its FILETYPE is `lua`. The ONLY reason
-- ClaudeCodeSend misbehaves here is that its PATH contains the substring
-- "neo-tree" (this file is named `_neo-tree_.lua`), which trips the
-- `string.match(current_bufname, "neo%-tree")` false positive.
--
-- Visually select a few of these lines and press <leader>as (or run
-- :'<,'>ClaudeCodeSend) -> you get a TreeAdd error and nothing is sent.

return {
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons",
"MunifTanjim/nui.nvim",
},
config = function()
require("neo-tree").setup({
close_if_last_window = true,
filesystem = {
follow_current_file = { enabled = true },
},
})
end,
}
20 changes: 20 additions & 0 deletions fixtures/issue-289/lua/regular_plugin.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- CONTROL FILE for issue #289.
--
-- Identical in spirit to `_neo-tree_.lua`, but its path contains NONE of the
-- magic substrings (no "neo-tree", "NvimTree", or "minifiles://"). Visually
-- selecting lines here and pressing <leader>as works correctly: the selection
-- is sent (or queued) with no TreeAdd error.

return {
"some-author/some-plugin.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
},
config = function()
require("some-plugin").setup({
enabled = true,
option_one = "value",
option_two = 42,
})
end,
}
20 changes: 10 additions & 10 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -727,17 +727,19 @@ function M._create_commands()

local function handle_send_normal(opts)
local current_ft = (vim.bo and vim.bo.filetype) or ""
local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or ""

-- Classify tree/explorer buffers by FILETYPE only. Matching the buffer name
-- (an absolute path) misfires on ordinary files whose path merely contains a
-- substring like "neo-tree"/"NvimTree" (issue #289). The downstream
-- extractors (integrations.get_selected_files_from_tree /
-- visual_commands.get_tree_state) are filetype-only, so a name-only match
-- could never extract anyway.
local is_tree_buffer = current_ft == "NvimTree"
or current_ft == "neo-tree"
or current_ft == "oil"
or current_ft == "minifiles"
or current_ft == "netrw"
or current_ft == "snacks_picker_list"
or string.match(current_bufname, "neo%-tree")
or string.match(current_bufname, "NvimTree")
or string.match(current_bufname, "minifiles://")

if is_tree_buffer then
local integrations = require("claudecode.integrations")
Expand Down Expand Up @@ -781,26 +783,24 @@ function M._create_commands()
end

local function handle_send_visual(visual_data, opts)
-- Check if we're in a tree buffer first
-- Check if we're in a tree buffer first. Classify by FILETYPE only; matching
-- the buffer name (an absolute path) misfires on ordinary files whose path
-- merely contains "neo-tree"/"NvimTree" (issue #289).
local current_ft = (vim.bo and vim.bo.filetype) or ""
local current_bufname = (vim.api and vim.api.nvim_buf_get_name and vim.api.nvim_buf_get_name(0)) or ""

local is_tree_buffer = current_ft == "NvimTree"
or current_ft == "neo-tree"
or current_ft == "oil"
or current_ft == "minifiles"
or current_ft == "netrw"
or string.match(current_bufname, "neo%-tree")
or string.match(current_bufname, "NvimTree")
or string.match(current_bufname, "minifiles://")

if is_tree_buffer then
local integrations = require("claudecode.integrations")
local visual_cmd_module = require("claudecode.visual_commands")
local files, error

-- For mini.files, try to get the range from visual marks for accuracy
if current_ft == "minifiles" or string.match(current_bufname, "minifiles://") then
if current_ft == "minifiles" then
local start_line = vim.fn.line("'<")
local end_line = vim.fn.line("'>")

Expand Down
Loading
Loading