Skip to content

feat(nvim): send the fzf-lua selection to Claude with <C-y> - #318

Merged
Gerrrt merged 2 commits into
mainfrom
claude/neovim-plugin-exploration-mw39iv
Aug 1, 2026
Merged

feat(nvim): send the fzf-lua selection to Claude with <C-y>#318
Gerrrt merged 2 commits into
mainfrom
claude/neovim-plugin-exploration-mw39iv

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #316 / #317.

Why

The tree-based @-mention (<leader>as in nvim-tree/oil) adds one file, so putting six files into Claude's context meant six tree navigations. fzf is already multi-select — --multi is on for the file pickers, <Tab> marks, <A-a> toggles all — so this turns "the files involved in this change" into one gesture.

Two workflows it enables:

  • <leader>ff<Tab> through the files in a refactor → <C-y> → all of them in context.
  • <leader>fg → search a symbol → mark the call sites you care about → each is sent as its own location, not as the whole file.

How

One actions.files entry covers <leader>ff / fg / fb / fr, since that's fzf-lua's shared action set for file-ish pickers.

Entries go through fzf-lua.path.entry_to_file rather than being parsed by hand — they carry devicon prefixes, and grep entries are file:line:col:text. fzf-lua reports 1-indexed lines while send_at_mention documents its range as 0-indexed, so the action converts.

The bug this nearly shipped with

My first version set actions.files = { ["ctrl-y"] = … }, which replaces fzf-lua's default action set instead of extending it. Diffing the resolved actions against a baseline build:

BASELINE : alt-Q alt-f alt-h alt-i alt-q ctrl-s ctrl-t ctrl-v enter esc
MINE     : ctrl-y esc                                      ← enter is gone

That would have broken opening a file at all, plus every split/tab/quickfix action — and make audit would not have caught it.

The fix is the leading true in the action table, fzf-lua's inheritance marker (config.lua switches the merge to tbl_deep_extend("keep", yours, defaults) when [1] == true, then strips it). After:

files    : alt-Q alt-f alt-h alt-i alt-q ctrl-s ctrl-t ctrl-v ctrl-y enter esc
grep     : … ctrl-g … ctrl-y enter esc
buffers  : … ctrl-x … ctrl-y enter esc     missing-defaults = NONE

Key choice

ctrl-y is free in both layers — fzf's own keymap binds ctrl-a to beginning-of-line and alt-a to toggle-all, and fzf-lua's default file actions never claim ctrl-y (only the git pickers do, for yank-commit, which is a different action set).

Not cond-gated, unlike the plugin spec

fzf-lua is a core finder that must load everywhere. Probing for claude while building the spec would cost a startup executable() on every box in the fleet and still miss a mid-session install. The action fails soft instead: without the CLI, claudecode isn't on the runtimepath, pcall(require, …) fails, and you get one notify explaining why rather than a stack trace.

Verification

make audit246 pass, 0 skip, 0 fail. luacheck clean across 98 files. (markdownlint caught an asterisk-emphasis violation in my CHANGELOG prose; fixed.)

Exercised headlessly against the real config, with send_at_mention stubbed to observe exactly what gets sent:

Case Result
Plain entry README.md path=/…/README.md start=nil end=nil (whole file)
Grep entry nvim/init.lua:10:3:… path=/…/nvim/init.lua start=9 end=9 (0-indexed)
Both marked together 2 mentions sent — multi-select works
claude off PATH 1 notify, no error, default actions intact

Also confirmed the [1] marker is stripped from the resolved table, so it never reaches fzf as a bind.


Generated by Claude Code

The tree-based @-mention (<leader>as in nvim-tree/oil) adds one file, so putting
six files into Claude's context meant six tree navigations. fzf is already
multi-select — --multi is on for the file pickers, <Tab> marks, <A-a> toggles
all — so this turns "the files involved in this change" into one gesture.

From live_grep each entry carries a line number, so a hit is sent as its own
location rather than the whole file. fzf-lua reports 1-indexed lines and
send_at_mention documents its range as 0-indexed, so the action converts.
Entries go through fzf-lua.path.entry_to_file rather than being parsed by hand:
they carry devicon prefixes, and grep entries are file:line:col:text.

One actions.files entry covers <leader>ff / fg / fb / fr, since that is
fzf-lua's shared action set for file-ish pickers.

THE LEADING `true` IN THE ACTION TABLE IS LOAD-BEARING. Without it, a user
action table REPLACES fzf-lua's defaults wholesale instead of extending them —
dropping enter, ctrl-s/v/t and alt-q/Q/i/h/f, i.e. the ability to open a file at
all. `[1] == true` is fzf-lua's inheritance marker (config.lua switches the
merge to tbl_deep_extend("keep", yours, defaults) and then strips the [1]). This
was caught by diffing the resolved action set against a baseline build, not by
reading the docs — the audit would not have caught it.

Deliberately NOT cond-gated, unlike plugins/claudecode-nvim.lua: fzf-lua is a
core finder that must load everywhere, and probing for `claude` while building
the spec would cost a startup executable() on every box and still miss a
mid-session install. The action fails soft instead — without the CLI, claudecode
is not on the runtimepath, the pcall(require, …) fails, and you get one notify
explaining why.

Verified: defaults survive across files/grep/buffers with ctrl-y added and the
[1] marker stripped; a plain entry sends as a whole file and a grep entry at
line 10 sends as start=end=9; multi-select sends one mention per marked entry;
and with claude off PATH the action emits a single notify, does not error, and
leaves the default actions intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvMkCtfJugUuxz6HCa1NSZ
Copilot AI review requested due to automatic review settings August 1, 2026 06:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-select fzf-lua entries to Claude Code context via <C-y>.

Changes:

  • Adds file and grep selection forwarding.
  • Documents picker shortcuts and behavior.
  • Updates the unreleased changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
nvim/lua/gerrrt/plugins/fzf-lua.lua Implements the Claude action.
nvim/lua/gerrrt/cheatsheet.lua Documents picker shortcuts.
CHANGELOG.md Records the feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nvim/lua/gerrrt/plugins/fzf-lua.lua Outdated
Comment on lines +24 to +28
-- NOT gated at spec level, unlike plugins/claudecode-nvim.lua: fzf-lua is a core finder that must
-- load everywhere, and probing for `claude` while building this spec would cost a startup
-- `executable()` on every box AND miss a mid-session install. The action instead fails soft — on a
-- box without the CLI, claudecode is not on the runtimepath, `require` fails, and you get one
-- notify saying why rather than a stack trace.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — removed in b8fcee8. The claim was wrong and, worse, contradicted a comment I wrote in the same feature: plugins/claudecode-nvim.lua already documents its cond as "Re-probed at startup only. Installing the CLI mid-session takes effect on the next launch." A spec gated off at startup is never added to the runtimepath, so a deferred require can't rescue it no matter when the key is pressed.

The remaining rationale stands without it: fzf-lua is a core finder that must load everywhere, so gating this spec would mean an executable() probe at startup on every box in the fleet.

The correction also improved the feature rather than just deleting a sentence — since the action runs only on a keypress, it can afford to probe properly, so "CLI installed after Neovim started → restart" is now one of the three distinct messages it reports (see the thread on line 39).


Generated by Claude Code

Comment thread nvim/lua/gerrrt/plugins/fzf-lua.lua Outdated
if file and file.path and file.path ~= "" then
-- fzf-lua reports 1-indexed lines; send_at_mention documents its range as 0-indexed for
-- Claude. `files` entries have no line (0 here) — pass nil so it means the whole file.
local line = (file.line and file.line > 0) and (file.line - 1) or nil

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in b8fcee8 — this was a real bug, and the most important of the five.

providers/buffers.lua:183 serializes the cursor line into the entry:

bname = make_entry.lcol({ filename = bname, lnum = buf.info.lnum }, opts):gsub(":$", "")

so <leader>fb<C-y> was sending file + the one line you happened to be parked on, as a 1-line mention, instead of the buffer.

Fixed by gating on the picker rather than enumerating one, since the grep family has several members:

local sends_location = tostring(opts and opts.__resume_key or ""):find("grep", 1, true) ~= nil

__resume_key is the provider identity. Every grep variant lands on a key containing greplive_grep routes through M.grepnormalize_opts(opts, "grep"), and grep_curbuf"grep_curbuf" — while files, buffers and oldfiles do not. Conservative on purpose: an unrecognised picker sends the whole file, because over-sending context is recoverable and silently sending one wrong line is not.

Verified per picker, feeding each an entry that carries a :lnum::

Picker Entry Line sent
buffers README.md:42: nil (whole file)
files README.md nil
oldfiles README.md:7: nil
grep README.md:42:3:some match 41 (0-indexed)

Generated by Claude Code

Comment thread CHANGELOG.md Outdated
Comment on lines +33 to +37
all. And unlike `plugins/claudecode-nvim.lua` this is **not** `cond`-gated: fzf-lua is a core
finder that must load everywhere, and probing for `claude` while building the spec would cost a
startup `executable()` on every box and still miss a mid-session install. The action fails soft
instead — without the CLI, `claudecode` is not on the runtimepath, the `pcall(require, …)` fails,
and you get one notify explaining why rather than a stack trace.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b8fcee8 — same error, same removal. The CHANGELOG paragraph now reads:

unlike plugins/claudecode-nvim.lua this is not cond-gated: fzf-lua is a core finder that must load everywhere, so gating the spec would mean an executable() probe at startup on every box in the fleet. The action fails soft instead, and since it only runs on a keypress it can afford to probe properly and name which failure it hit — CLI absent, CLI installed after Neovim started (the cond is evaluated once at startup, so a restart is needed), or the plugin genuinely failing to load, in which case the real error is shown rather than swallowed.

It also picks up the buffers-line correction as a third recorded detail. Worth fixing in the changelog specifically rather than only in the comment: /release-notes drafts from this file and the commit history, so an inaccurate rationale here would have propagated into the next release's notes.


Generated by Claude Code

Comment thread nvim/lua/gerrrt/plugins/fzf-lua.lua Outdated
Comment on lines +30 to +39
local ok, claudecode = pcall(require, "claudecode")
if not ok then
vim.notify(
"Claude Code is not available — the `claude` CLI is not installed on this machine, "
.. "so plugins/claudecode-nvim.lua is gated off (see :checkhealth gerrrt).",
vim.log.levels.WARN,
{ title = "fzf-lua" }
)
return
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and implemented in b8fcee8. The message now separates three cases instead of assuming one:

Condition Message
claude not on PATH "the claude CLI is not installed on this machine, so plugins/claudecode-nvim.lua is gated off (see :checkhealth gerrrt)."
On PATH, but spec absent from lazy "claude is on PATH but was not there when Neovim started, and the plugin's cond is evaluated once at startup. Restart Neovim to enable it."
On PATH, spec present, require failed "claudecode.nvim failed to load: <captured error>"

The middle case falls out of the same correction you raised on line 28 — a CLI installed mid-session leaves the spec gated off, and that is worth saying explicitly rather than reporting as "not installed", since the fix (restart) is different.

Distinguishing case 2 from case 3 asks lazy whether the spec exists at all, pcall-guarded so a change in lazy's internals degrades to the load-error message rather than throwing inside the notify path:

local ok_lazy, lazy_cfg = pcall(require, "lazy.core.config")
if ok_lazy and not (lazy_cfg.plugins or {})["claudecode.nvim"] then

Verified all three by forcing each condition — stripping PATH, stubbing executable to report a just-installed binary, and injecting a package.preload that errors. Case 3 surfaces the real message: claudecode.nvim failed to load: [string ":lua"]:5: simulated: attempt to index nil (upstream regression).


Generated by Claude Code

…real failure

Two real defects from review, plus a typo.

1. <leader>fb sent the wrong thing. entry_to_file reports a positive line for
   BUFFER entries too — providers/buffers.lua serializes each buffer's current
   cursor lnum into the entry via make_entry.lcol — so forwarding it
   unconditionally made the buffers picker send the single line you happened to
   be parked on instead of the file. Line forwarding is now restricted to the
   grep family (resume key containing "grep"; live_grep routes through M.grep →
   "grep", plus "grep_curbuf"), which is the only family where the location is
   meaningful. Conservative by design: over-sending context is recoverable,
   silently sending one wrong line is not.

2. The unavailable-message always blamed a missing CLI and discarded the actual
   error, so a broken or incompatible plugin looked like an installation
   problem. Because this path only runs on a keypress it can afford to probe
   properly, and now separates three cases: CLI absent; CLI present but
   installed after Neovim started (the spec's `cond` is evaluated once at
   startup, so lazy never put it on the runtimepath — restart); and the plugin
   genuinely failing to load, where the captured error is shown.

3. "LOad-BEARING" → "LOAD-BEARING".

Also drops the claim that a deferred require rescues a mid-session install. It
does not, and it contradicted the header of plugins/claudecode-nvim.lua, which
already documents the cond as startup-only. The remaining rationale stands on
its own: fzf-lua is a core finder that must load everywhere, so gating the spec
would mean an executable() probe at startup on every box in the fleet.

Verified: buffers/files/oldfiles entries carrying a `:lnum:` now send line=nil
while a grep entry at line 42 still sends 41; and all three unavailable paths
produce distinct messages, with the load-error case surfacing the real error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvMkCtfJugUuxz6HCa1NSZ
@Gerrrt
Gerrrt merged commit 2d9bc9e into main Aug 1, 2026
14 checks passed
@Gerrrt
Gerrrt deleted the claude/neovim-plugin-exploration-mw39iv branch August 1, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants