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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ When Anthropic released Claude Code, they only supported VS Code and JetBrains.
"<leader>as",
"<cmd>ClaudeCodeTreeAdd<cr>",
desc = "Add file",
ft = { "NvimTree", "neo-tree", "oil", "minifiles", "netrw" },
ft = { "NvimTree", "neo-tree", "oil", "minifiles", "netrw", "snacks_picker_list" },
},
-- Diff management
{ "<leader>aa", "<cmd>ClaudeCodeDiffAccept<cr>", desc = "Accept diff" },
Expand Down Expand Up @@ -212,7 +212,8 @@ Configure the plugin with the detected path:
1. **Launch Claude**: Run `:ClaudeCode` to open Claude in a split terminal
2. **Send context**:
- Select text in visual mode and use `<leader>as` to send it to Claude
- In `nvim-tree`/`neo-tree`/`oil.nvim`/`mini.nvim`, press `<leader>as` on a file to add it to Claude's context
- In `nvim-tree`/`neo-tree`/`oil.nvim`/`mini.nvim`, or a focused snacks picker list / the Snacks Explorer sidebar, press `<leader>as` on a file to add it to Claude's context
- For modal snacks pickers (`Snacks.picker.files()`/`grep()`), which keep focus in the input box, bind a picker action that calls `require("claudecode").send_at_mention(...)` for the selected item(s) — the [claude-fzf.nvim](#-claude-fzfnvim) community extension does the equivalent for `fzf-lua`
3. **Let Claude work**: Claude can now:
- See your current file and selections in real-time
- Open files in your editor
Expand Down
85 changes: 85 additions & 0 deletions fixtures/snacks-picker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# snacks-picker fixture — triage for issue #192

Reproduces and explores [#192](https://github.com/coder/claudecode.nvim/issues/192):
make adding the **highlighted/selected file in a snacks.nvim picker** to Claude's
context work, instead of failing with:

```
[ClaudeCode] [command] [ERROR] ClaudeCodeTreeAdd: Not in a supported tree buffer (current filetype: snacks_picker_list)
```

Unlike the tree-explorer integrations (`nvim-tree`, `neo-tree`, `oil`,
`mini.files`, `netrw`), a snacks **picker** is a modal fuzzy finder. Its window
layout is:

| window | filetype | buftype | usual focus |
| ------ | --------------------- | -------- | --------------------------------- |
| box | `snacks_layout_box` | `nofile` | — |
| list | `snacks_picker_list` | `nofile` | only when you `<Tab>`/cycle to it |
| input | `snacks_picker_input` | `prompt` | **default (insert mode)** |

`:ClaudeCodeTreeAdd` only sees `snacks_picker_list` when the **list** window is
focused — but the picker normally keeps focus in the **input** box. That is why
the idiomatic fix is an in-picker _action bound to a key_, not an ex-command.

Note: `Snacks.explorer()` is built on the picker and also uses the
`snacks_picker_list` filetype, so the same code path covers both.

## Run it

```bash
source fixtures/nvim-aliases.sh
vv snacks-picker
```

Then `:ClaudeCodeStart` (or it auto-starts), and:

- `<leader>ff` → files picker, `<leader>fg` → grep picker.

### Path A — zero-change WORKAROUND (works on stock claudecode.nvim)

`lua/plugins/snacks.lua` registers a custom picker action `claude_add` bound to
`<c-o>` in both the input and list windows. From the picker (input box, insert
mode):

1. Type to filter; optionally `<Tab>` to multi-select several files.
2. Press `<c-o>` → the selected files (or the one under the cursor) are sent to
Claude via the public `require("claudecode").send_at_mention()` API, and the
picker closes.

This needs **no changes to claudecode.nvim**. It mirrors how the community
`claude-fzf.nvim` plugin integrates fzf-lua.

### Path B — built-in command path

The in-core `snacks_picker_list` handler (`integrations._get_snacks_picker_selection`)
makes `:ClaudeCodeTreeAdd` work when the list window is focused:

1. Open a picker, `<Tab>` to focus/cycle to the **list** window (filetype becomes
`snacks_picker_list`).
2. `:ClaudeCodeTreeAdd` (or `<leader>at`) → selected/cursor files are added.

## Deterministic, headless proof (no Claude client needed)

The behavior was validated headlessly:

```text
# Without the snacks_picker_list handler (the issue #192 state), list focused:
focused_filetype: snacks_picker_list
dispatch_error: Not in a supported tree buffer (current filetype: snacks_picker_list)

# With the in-core handler, 2 files multi-selected:
handler_basenames: alpha.lua,beta.lua
dispatch_files: 2 err: nil

# Key-bound action (Path A), 2 files multi-selected:
send_at_mention_results: alpha.lua=true,beta.lua=true
```

To reproduce headlessly, a small throwaway harness (not committed) opens a picker
with `Snacks.picker.files({ cwd = ... })`, `vim.wait`s until
`Snacks.picker.get()` returns a picker with items, focuses the list window, then
calls `require("claudecode.integrations").get_selected_files_from_tree()` and
inspects the returned paths. The committed unit test
`tests/unit/snacks_picker_integration_spec.lua` covers the same logic
deterministically with mocked snacks.
8 changes: 8 additions & 0 deletions fixtures/snacks-picker/example/alpha.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- example/alpha.lua — a file to highlight/select in the snacks picker.
local M = {}

function M.greet(name)
return "hello, " .. (name or "world")
end

return M
8 changes: 8 additions & 0 deletions fixtures/snacks-picker/example/beta.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- example/beta.lua — a second file, to test multi-select (<Tab>) in the picker.
local M = {}

function M.add(a, b)
return a + b
end

return M
1 change: 1 addition & 0 deletions fixtures/snacks-picker/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("config.lazy")
5 changes: 5 additions & 0 deletions fixtures/snacks-picker/lazy-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }
}
37 changes: 37 additions & 0 deletions fixtures/snacks-picker/lua/config/lazy.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)

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

-- Resolve the claudecode.nvim checkout that owns this fixture.
-- XDG_CONFIG_HOME is set to the `fixtures/` dir by the `vv` launcher, so the
-- repository root is its parent. This makes the fixture load the local plugin
-- copy (including git worktrees) without relying on lazy's default dev path.
local repo_root = vim.fn.fnamemodify(vim.env.XDG_CONFIG_HOME or vim.fn.getcwd(), ":h")
vim.g.claudecode_dev_dir = repo_root

require("lazy").setup({
spec = {
{ import = "plugins" },
},
install = { colorscheme = { "habamax" } },
checker = { enabled = false },
})

vim.keymap.set("n", "<leader>l", "<cmd>Lazy<cr>", { desc = "Lazy Plugin Manager" })
vim.keymap.set("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode (double esc)" })
22 changes: 22 additions & 0 deletions fixtures/snacks-picker/lua/plugins/dev-claudecode.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Development configuration for claudecode.nvim with the snacks.nvim picker.
-- Loads the local plugin checkout (resolved in lua/config/lazy.lua) via `dir`
-- so it works from a normal checkout or a git worktree.
return {
"coder/claudecode.nvim",
dir = vim.g.claudecode_dev_dir,
dependencies = { "folke/snacks.nvim" },
keys = {
{ "<leader>a", nil, desc = "AI/Claude Code" },
{ "<leader>ac", "<cmd>ClaudeCode<cr>", desc = "Toggle Claude" },
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
{ "<leader>aS", "<cmd>ClaudeCodeStart<cr>", desc = "Start Claude Server" },
{ "<leader>aQ", "<cmd>ClaudeCodeStop<cr>", desc = "Stop Claude Server" },
-- Built-in command path: focus the picker LIST window, then run this.
{ "<leader>at", "<cmd>ClaudeCodeTreeAdd<cr>", desc = "Tree/Picker Add" },
},
---@type PartialClaudeCodeConfig
opts = {
-- Keep behavior predictable for reproduction; start the server explicitly.
log_level = "debug",
},
}
11 changes: 11 additions & 0 deletions fixtures/snacks-picker/lua/plugins/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Basic plugin configuration
return {
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
config = function()
vim.cmd([[colorscheme tokyonight]])
end,
},
}
75 changes: 75 additions & 0 deletions fixtures/snacks-picker/lua/plugins/snacks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-- snacks.nvim picker fixture for issue #192.
--
-- Demonstrates BOTH integration paths for adding a picker's selected/highlighted
-- file(s) to Claude Code's context:
--
-- 1. WORKAROUND (works TODAY, no claudecode.nvim change): a custom snacks picker
-- `action` (`claude_add`) bound to <c-o> in the input AND list windows. This
-- is the idiomatic snacks way and works from the input box in insert mode.
-- It mirrors how the community claude-fzf.nvim plugin integrates fzf-lua.
--
-- 2. Built-in command path: `:ClaudeCodeTreeAdd` also works when the picker
-- LIST window is focused (vim.bo.filetype == "snacks_picker_list"), via the
-- in-core snacks_picker_list handler in lua/claudecode/integrations.lua.
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
---@type snacks.Config
opts = {
picker = {
enabled = true,
---@type table<string, snacks.picker.Action.spec>
actions = {
-- WORKAROUND action: send the selected (Tab) items, or the item under
-- the cursor when nothing is selected, to Claude as @-mentions.
claude_add = function(picker)
local items = picker:selected({ fallback = true })
local claudecode = require("claudecode")
local count = 0
for _, item in ipairs(items) do
local path = Snacks.picker.util.path(item)
if path and path ~= "" then
local ok = claudecode.send_at_mention(path, nil, nil, "snacks-picker")
if ok then
count = count + 1
end
end
end
picker:close()
vim.schedule(function()
vim.notify(("[claude_add] Added %d file(s) to Claude context"):format(count), vim.log.levels.INFO)
end)
end,
},
win = {
input = {
keys = {
["<c-o>"] = { "claude_add", mode = { "i", "n" }, desc = "Add to Claude Code" },
},
},
list = {
keys = {
["<c-o>"] = { "claude_add", desc = "Add to Claude Code" },
},
},
},
},
},
keys = {
{
"<leader>ff",
function()
Snacks.picker.files()
end,
desc = "Find Files (snacks picker)",
},
{
"<leader>fg",
function()
Snacks.picker.grep()
end,
desc = "Grep (snacks picker)",
},
},
}
1 change: 1 addition & 0 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ function M._create_commands()
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://")
Expand Down
79 changes: 78 additions & 1 deletion lua/claudecode/integrations.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- Tree integration module for ClaudeCode.nvim
--- Handles detection and selection of files from nvim-tree, neo-tree, mini.files, and oil.nvim
--- Handles detection and selection of files from nvim-tree, neo-tree, mini.files, oil.nvim, netrw, and snacks pickers
---@module 'claudecode.integrations'
local M = {}
local logger = require("claudecode.logger")
Expand All @@ -20,11 +20,88 @@ function M.get_selected_files_from_tree()
return M._get_mini_files_selection()
elseif current_ft == "netrw" then
return M._get_netrw_selection()
elseif current_ft == "snacks_picker_list" then
return M._get_snacks_picker_selection()
else
return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")"
end
end

---Get selected files from a snacks.nvim picker
---Supports both Snacks.explorer() and modal files()/grep() pickers (they share
---the snacks_picker_list filetype), including multi-selection (Tab) and the
---item under the cursor.
---@return table files List of file paths
---@return string|nil error Error message if operation failed
function M._get_snacks_picker_selection()
local success, snacks = pcall(require, "snacks")
if not success then
return {}, "snacks.nvim picker not available"
end

-- snacks.picker and snacks.picker.util are lazily required through a metatable,
-- so merely accessing them can raise on a partial install. Probe both under
-- pcall so the handler degrades gracefully instead of throwing.
-- probe_ok is true only if snacks.picker resolved AND snacks.picker.util.path
-- evaluated without raising, so a non-function util_path is the only remaining
-- "partial install" case to reject.
local probe_ok, picker_mod, util_path = pcall(function()
return snacks.picker, snacks.picker.util.path
end)
if not probe_ok or type(util_path) ~= "function" then
return {}, "snacks.nvim picker not available"
end

-- snacks_picker_list is shared by the explorer and modal files()/grep() pickers.
-- Prefer the picker whose list window is currently focused (the buffer the user
-- ran the command in); fall back to the most-recently-opened picker on this tab.
local pickers = picker_mod.get({ tab = true })
if not pickers or #pickers == 0 then
return {}, "No active snacks picker found"
end
local current_win = vim.api.nvim_get_current_win()
local picker = pickers[#pickers]
for _, p in ipairs(pickers) do
local lw = p.list and p.list.win and p.list.win.win
if lw == current_win then
picker = p
break
end
end

-- selected({fallback=true}) returns explicitly selected (Tab) items, or the
-- item under the cursor when nothing is selected.
local ok_sel, items = pcall(function()
return picker:selected({ fallback = true })
end)
if not ok_sel or type(items) ~= "table" then
return {}, "Failed to read snacks picker selection"
end

local files = {}
for _, item in ipairs(items) do
-- Skip items without a string file path (registers/commands/unsaved buffers).
if item and type(item.file) == "string" then
-- Snacks.picker.util.path is the canonical resolver but undocumented;
-- guard it and fall back to a manual cwd/file join. An absolute item.file
-- with no cwd (e.g. the explorer) falls through to item.file unchanged.
local path_ok, path = pcall(util_path, item)
if not path_ok or not path then
path = (type(item.cwd) == "string") and (item.cwd .. "/" .. item.file) or item.file
end
if path and path ~= "" and (vim.fn.filereadable(path) == 1 or vim.fn.isdirectory(path) == 1) then
table.insert(files, path)
end
end
end

if #files > 0 then
return files, nil
end

return {}, "No file found in snacks picker selection"
end

---Get selected files from nvim-tree
---Supports both multi-selection (marks) and single file under cursor
---@return table files List of file paths
Expand Down
Loading
Loading