claude CLI you are already signed in to, so replies come out of your Claude Code subscription (or whatever the CLI is set up with).u undoes it), or shown as a diff you accept first.setup() loads one module and takes 0.74 ms, nothing runs while Claude is not replying, and a streamed answer costs 0.71 ms of CPU per piece, even on a 10,000-line file.claude CLI on your PATH, signed in (run claude once in a terminal).With lazy.nvim:
{
"kova1max/float-claude.nvim",
opts = {},
}
Any other plugin manager works too: add the repository and call require("float-claude").setup(). Then run :checkhealth float-claude to see that the CLI is found and no other mapping took the plugin's keys. The full manual is :help float-claude.
V), or a block (<C-v>).<leader>ai and type what you want.Enter.Claude decides whether your prompt asks for an answer or an edit:
two_sum.lua · L5–13 · 9 lines
❯ what's the time complexity?
O(n²): the two loops compare every pair in nums.
Space is O(1).
❯ █
The header names the file and the lines Claude sees. Past questions are dimmed, replies are indented under them, and a fresh ❯ waits for a follow-up. In Normal mode, <leader>ai asks about the current line, or goes back to the open popup.
The answer streams in. Names from the code and code blocks are highlighted in the file's language. Keep asking: follow-ups continue the same conversation.
Ask for a change ("use a table of seen numbers, one pass", the GIF at the top) and the selected lines are rewritten in the buffer, streaming in line by line. The popup stays open with refactored 9 → 9 lines · u to undo, so you can keep going ("also rename x"). Each edit is one u. Select with <C-v> and the edit replaces just the block's columns on each line.
With review_edits = true, an edit shows as an inline diff (removed lines red, new lines green) and the buffer stays untouched until you decide: <leader>yy accepts, <leader>nn discards.
<C-q> hides the popup and <C-o> steps back into the code with the popup still on screen. Either way Claude keeps answering or editing. <leader>ai brings you back to the same conversation, and require("float-claude").status() puts a spinner (then ● for an unseen reply) in your statusline. Up to 5 chats live side by side, one per selection (<leader>an / <leader>ap / <leader>al), and <leader>aH reopens a saved chat, also after restarting Neovim.
With an LSP server attached, the names in an answer that the server knows are underlined. In the conversation (<C-w>w from the prompt, or a click), Enter or a click jumps to the definition and K shows the hover.
<leader>ae explain, <leader>af fix the diagnostics, <leader>at tests, <leader>ay types, <leader>ad doc comments, <leader>as simplify; for Markdown, commit messages and comments <leader>ar reword, <leader>ah shorten, <leader>ag grammar. Add your own in actions.<leader>am collects separate selections, then one question (or one edit, one u) covers them all.@ mentions: @diagnostics, @<buffer> or @<path> sends more context, with completion.:FloatClaudeModel sonnet, :FloatClaudeEffort high, shown in the header, switched without restarting the chat.:FloatClaude lists everything through vim.ui.select, so a picker that replaces it (telescope, fzf-lua, snacks) shows it.prompt = "answer in Ukrainian" (or a function of the filetype), and an opt-in per-project file.theme = "auto" follows your colorscheme; or tokyonight, catppuccin, gruvbox, nord, onedark, rose-pine, dracula, night-owl, float-claude; :FloatClaudeTheme to try them.Every feature has its section in :help float-claude (doc/float-claude.txt).
| Key | Where | Action |
|---|---|---|
<leader>ai |
Visual | Ask about the selection (v, V: lines; <C-v>: the block's columns) |
<leader>ai |
Normal | Back to the open popup; with none open, ask about the current line |
<CR> |
Popup | Send the prompt |
<S-CR> or <C-j> |
Popup | New line in the prompt (<C-j> if your terminal does not send Shift-Enter) |
<C-u> / <C-d>, wheel, <Up> / <Down> |
Popup | Scroll the conversation |
q |
Popup, Normal mode | Close and end the chat; a half-written edit is undone |
<C-q> |
Popup | Hide it; the chat goes on |
<C-o> |
Popup | Back to the code; the popup stays on screen |
<leader>yy / <leader>nn |
Popup, with review_edits |
Accept / discard the diff |
<CR>, click / K |
Conversation, on an underlined name | Jump to its definition / show its hover |
<leader>ae, <leader>af, ... |
Normal, Visual, popup | One-key actions |
<leader>am / <leader>ax |
Visual, Normal | Add the selection to the ranges to ask about together / clear them |
<leader>an / <leader>ap, <leader>al |
Normal | Next / previous open chat, or pick one |
<leader>aH |
Normal | Reopen a saved chat |
@ |
Popup | Mention a file, a buffer or @diagnostics |
:FloatClaude |
Normal, Visual | Pick any of the above from a list |
Every key is a setup option, and any of them can be false.
Every option, with its default (opts for lazy.nvim, or require("float-claude").setup({...})). A table option (like cursor_offset) is replaced as a whole, so give all of its fields; actions is the exception, merged action by action.
{
keymap = "<leader>ai", -- Visual + Normal mode: ask about the selection / back to the popup; false to map require("float-claude").ask yourself
cmd = { "claude" }, -- the CLI, with extra arguments if you like
model = false, -- each chat's model to start with: "opus", "sonnet", "haiku", "fable" or a model name; false: the CLI's default
effort = false, -- each chat's effort to start with: "low", "medium", "high", "xhigh" or "max"; false: the CLI's default
mentions = { max_lines = 500 }, -- @ mentions in the prompt, each sending at most max_lines lines; false: off
prompt = false, -- your instructions for every chat: a string, or function(ctx) returning one (ctx: buf, file, filetype)
project_prompt = false, -- e.g. ".float-claude.md": that file, from the file's directory or above, is added
context_lines = 40, -- unselected lines sent above and below the selection
diagnostics = true, -- send the diagnostics in the selected lines too
actions = { -- one-key actions; each is merged on its own, false removes one
explain = { key = "<leader>ae", prompt = "Explain what this code does." },
fix = { key = "<leader>af", prompt = "Fix the problems the diagnostics report in this code; with none listed, fix the bugs you find." },
tests = { key = "<leader>at", prompt = "Write unit tests for this code, in the project's test style, as an answer with a code block; do not edit the code." },
types = { key = "<leader>ay", prompt = "Add type annotations to this code, in the language's usual way. Change nothing else." },
docstring = { key = "<leader>ad", prompt = "Add a doc comment to each function in this code, in the language's usual style. Change nothing else." },
simplify = { key = "<leader>as", prompt = "Simplify this code without changing what it does." },
reword = { key = "<leader>ar", prompt = "Reword this text so it reads clearly; keep its meaning and tone.", prose = true },
shorten = { key = "<leader>ah", prompt = "Shorten this text; keep its meaning.", prose = true },
grammar = { key = "<leader>ag", prompt = "Fix the spelling, grammar and punctuation of this text. Change nothing else.", prose = true },
},
prose_filetypes = { "markdown", "gitcommit", "text" }, -- buffers offering the prose actions instead of the code ones
max_chats = 5, -- chats open at once (one shown, the others hidden); one more ends the one used longest ago
history = { max = 100, days = 30 }, -- save chats to reopen them; either can be false; false: off
status = { spinner = { "◐", "◓", "◑", "◒" }, unread = "●" }, -- what status() shows
width = 50, -- popup width in columns, capped to the screen; longer lines wrap
max_height = false, -- popup height cap in rows; false: as tall as the screen allows
placement = "top-right", -- or "cursor": next to the cursor
top_right_offset = { row = 0, col = 0 }, -- "top-right": rows from the top, columns from the right edge
cursor_offset = { row = 1, col = 3 }, -- "cursor": rows below, columns right of the cursor (above it when there is no room below)
theme = "auto", -- or a preset: night-owl, dracula, tokyonight, catppuccin, gruvbox, nord, onedark, rose-pine, float-claude
colors = {}, -- single colors over the theme, e.g. { bg = "#000000", prompt = "#ff9e64" }
review_edits = false, -- true: show an edit as a diff and apply it only once accepted
close_after_edit = false, -- true: close the popup (ending the chat) once an edit is applied
close_on_leave = false, -- true: leaving the popup (other than with unfocus_key or a jump) closes it
-- Normal mode keys for the open chats:
next_chat_key = "<leader>an", -- show the next chat
prev_chat_key = "<leader>ap", -- show the previous one
chats_key = "<leader>al", -- pick an open chat from a list
history_key = "<leader>aH", -- pick a saved chat to reopen
add_range_key = "<leader>am", -- Visual and Normal mode: add the selection (or the current line) to the ranges to ask about together
clear_ranges_key = "<leader>ax", -- Normal mode: clear them
-- Keys in the popup:
submit_key = "<CR>", -- send the prompt (Insert and Normal mode)
newline_keys = { "<S-CR>", "<C-j>" }, -- Insert mode: a new line in the prompt
close_key = "q", -- Normal mode: close the popup and end the chat
hide_key = "<C-q>", -- hide it; the chat goes on, keymap brings it back
unfocus_key = "<C-o>", -- back to the code, the popup stays on screen
accept_key = "<leader>yy", -- with review_edits: accept the diff (Normal mode)
discard_key = "<leader>nn", -- with review_edits: discard it
scroll_up_key = "<C-u>", -- scroll the conversation half a screen
scroll_down_key = "<C-d>",
line_up_key = "<Up>", -- scroll it a line (in a multi-line prompt, first move between its lines)
line_down_key = "<Down>",
jump_key = "<CR>", -- in the conversation, on an underlined name: go to its definition
hover_key = "K", -- in the conversation, on an underlined name: show its hover
insert_keys = { "i", "a" }, -- in the conversation: back to the prompt
}
For a statusline, require("float-claude").status() returns a spinner while a hidden chat answers, ● for a reply you have not seen, and "" otherwise. With lualine:
sections = {
lualine_x = { function() return require("float-claude").status() end, "filetype" },
}
The file name and filetype, the selected lines, up to 40 lines around them, and the diagnostics in the selected lines, plus whatever you @-mention. Every follow-up resends the selected lines as they are at that moment, so Claude always works on the current code, and starts with diffs of what changed since its last reply: its own edits (applied, accepted or discarded) apart from yours.
The CLI runs with no tools, MCP servers, hooks or settings files: Claude reads only what the popup sends and changes only the selected lines, in your buffer, one u away. Your CLAUDE.md files and settings.json are not used; auth comes from your claude login or from environment variables such as ANTHROPIC_API_KEY in the shell Neovim runs in.
Facts from each project's README (November 2026). Pick by the shape you want: float-claude is for quick questions and edits about the code in front of you; for an agent that runs commands and edits across files, use Claude Code itself (claudecode.nvim puts it in a split), avante.nvim or codecompanion.nvim.
| float-claude.nvim | claudecode.nvim | avante.nvim | codecompanion.nvim | CopilotChat.nvim | |
|---|---|---|---|---|---|
| UI | popup at the selection | Claude Code's terminal UI in a split | sidebar | chat buffer, inline | chat window (split or float) |
| Talks to | claude CLI, headless |
claude CLI, over its IDE protocol |
API providers (Claude, OpenAI, Gemini, Copilot, ...), ACP agents | HTTP adapters (Anthropic, OpenAI, Copilot, Ollama, ...), ACP agents | GitHub Copilot, plus custom providers |
| Claude subscription without an API key | yes | yes | yes (auth_type = "max", or ACP) |
through the Claude Code ACP agent | no (Claude models through Copilot) |
| Edits | in place, streamed, one u; optional diff review |
diffs to accept or deny | diffs applied to the file | approve or reject agent changes | diffs to accept |
| Dependencies | none | none (snacks.nvim optional) | plenary.nvim, nui.nvim, a make build step |
plenary.nvim, nvim-treesitter | plenary.nvim, curl, make tiktoken |
| Neovim | 0.10+ | 0.8+ | 0.11+ (its badge: 0.12+) | not stated | 0.10+ |
float-claude and claudecode.nvim work side by side; their suggested keys both live under <leader>a, so check :checkhealth float-claude for clashes.
Do I need an API key? No. float-claude starts the claude CLI, which uses whatever it is signed in with: a Claude Pro or Max subscription, or an API key, Bedrock or Vertex set up in its environment.
Does it send my whole file? No: the selection, up to context_lines (40) lines around it, its diagnostics, and what you @-mention.
Can Claude run commands or touch other files? No. The CLI runs with no tools; a reply is either an answer or the replacement for the selected lines.
Which model does it use? The CLI's default, unless you set model. :FloatClaudeModel and :FloatClaudeEffort switch the open chat's.
Does it work in tmux or over SSH? Yes, it is all in the terminal. If Shift-Enter does not reach Neovim, <C-j> adds a new line.
Is it slow on big files? No. Measured on a 10,000-line file with an LSP server attached: 0.71 ms of CPU per streamed piece of an answer and 0.5 ms per piece of an edit; while an answer streams and its names are looked up, the editor is never busy for more than 61 ms at a time. The claude process ends when its chat closes or Neovim quits. tests/bench.lua measures it on your machine.
:checkhealth float-claude. It checks the Neovim version, that setup() ran, that the CLI runs, and which of the plugin's keys another mapping took.claude in a terminal and sign in. Behind a custom install, point cmd at it.:checkhealth float-claude names it); change the key in setup().@ completion menu on Neovim 0.10: add noselect to completeopt, or press <C-x><C-o>.Run the tests (headless; a fake CLI stands in for Claude and a fake LSP server for a language server, so no network or account is needed):
nvim --clean --headless -l tests/run.lua
ONLY=<text> runs just the tests whose name contains the text.
Measure what the plugin costs (setup time, what runs while idle, CPU per streamed piece of a reply on a 10,000-line file, the longest the editor is blocked, memory), with the same fake CLI and LSP server:
nvim --clean --headless -l tests/bench.lua
The GIFs are recorded with VHS from the tapes in assets/, with the fake CLI scripting Claude's replies: assets/record.sh (needs vhs, and lua-language-server for lsp.tape).