Review a diff in Neovim. Annotate what needs work. Send the whole review to a coding agent as one message.
Unified or split layout, a file tree, reviewed-file collapsing, and a batch submit. Neovim draws everything: no terminal, no browser.
:help codereview is the full reference. This README is the short version.
With lazy.nvim:
{
"felipeva/codereview-annotator.nvim",
cmd = { "CodeReview", "CodeReviewAnnotate", "CodeReviewSwitch" },
opts = {},
}Requires Neovim 0.12+ and git. Removing a commit from the middle of a branch review
(gc) needs git 2.38+. Treesitter parsers are optional.
:CodeReview open the diff of your branch
ab annotate the line as a bug, write the note, <C-s> to queue it
R mark the file reviewed (it collapses)
]F go to the next unreviewed file
Q read the queue, then <C-s> to submit the batch
With no send adapter, the batch goes to the + register and stays queued.
| Command | Action |
|---|---|
:CodeReview [scope] |
Open a review. No scope = the branch against its base |
:CodeReviewAnnotate [type] |
Annotate the current buffer or range, with or without a review |
:CodeReviewCopy |
Copy the batch to + without submitting it |
:CodeReviewLastBatch |
Show the last batch you sent |
:CodeReviewSwitch |
Move the review to another checkout (worktree) of this repo |
:CodeReviewBack |
Go back to the previous checkout |
:CodeReviewOverlay [margin|inline] |
Show or hide the queued annotations on the current file, in a margin or as captions |
Scopes:
:CodeReview " branch vs base (default)
:CodeReview staged " the index
:CodeReview unstaged " working tree vs index
:CodeReview worktree " everything uncommitted
:CodeReview since-batch " what changed since the last batch went out
:CodeReview HEAD~3 " any revspec
:CodeReview main...feature " any range| Key | Action |
|---|---|
ab af as an ai |
Annotate as bug / fix / suggestion / nitpick / issue |
aa |
Annotate, pick the type from a menu |
x |
Drop the annotation under the cursor |
e |
Edit its note |
ct |
Change its type |
R |
Toggle reviewed (collapses the file; in solo, goes to the next unreviewed) |
za |
Collapse / expand the file, without marking it |
<CR> |
Open the real file at this line, in a new tab |
gd |
Open the file in your diff tool (open_diff) |
gs |
Cycle scope |
gr |
Reload from git |
gc |
Choose which branch commits to review (commit list) |
gS |
Switch to another checkout |
gl |
Toggle unified / split |
gw |
Toggle wrap of long lines (unified only) |
go |
Toggle solo: show one file at a time |
gp |
Show / hide the file tree |
gA |
Show / hide annotations already sent |
gb |
Show the last batch |
Q |
Open the queue |
gy |
Copy the batch to +, without submitting |
<C-t> |
Choose the delivery target |
<C-s> |
Submit the batch |
<C-a> |
Submit the batch with a preamble (a note above all annotations) |
q |
Close |
With more than one annotation on the line, x, e and ct open a list. The newest is
first.
| Key | Action |
|---|---|
]f [f |
Next / previous file |
]F [F |
Next / previous unreviewed file (wraps) |
]h [h |
Next / previous hunk |
]a [a |
Next / previous annotation |
<C-p> |
Go to a file by name |
<Tab> |
Move between the diff and the tree |
| Key | Action |
|---|---|
<CR> o |
Open the file, or fold the directory |
h zc / l zo |
Collapse / expand the directory |
za |
Toggle the directory |
zM / zR |
Collapse / expand all |
]f [f |
Next / previous file |
R |
Toggle reviewed (on a directory: the whole subtree) |
gd <C-p> |
Same as in the diff |
gl gb gc gA gS gw go |
Same as in the diff |
<Tab> / gp |
Back to the diff / close the tree |
q |
Close |
Opens in insert mode. <C-s> and <C-t> work in insert and normal mode.
| Key | Action |
|---|---|
<C-s> |
Queue the note, save an edit, or send an immediate send |
<C-t> |
Choose where this note goes |
@ |
Reference another file (pick_file) |
<C-d> |
Discard a restored draft |
q <Esc> |
Close. Keep the text as a draft |
| Key | Action |
|---|---|
<CR> |
Go to the annotation. With no review view open, open its file at its line, in a new tab |
e |
Edit its note |
t |
Change its type |
x |
Drop it |
gy |
Copy the batch to + (float stays open) |
<C-t> |
Choose the delivery target |
<C-s> |
Submit the batch |
<C-a> |
Submit with a preamble |
q <Esc> |
Close. Keep the queue |
? |
List all keys of the float |
Read-only. q or <Esc> closes it.
gc in a branch review. Newest commit first. Each row shows its size: 3f +212 -48.
| Key | Action |
|---|---|
<Space> |
Put the commit in or out of the review |
<Space> (visual) |
Set all selected rows the same as the first row |
]c [c |
Next / previous checked commit |
<CR> |
Apply and close |
q <Esc> |
Close without changes |
Uncheck the oldest commits to start the review later in the branch. Uncheck a commit in the middle (a formatter run, a rename) to skip only that one. Uncommitted work always stays in. The plugin keeps the selection per branch.
| Type | Glyph | Key | Directive in the payload |
|---|---|---|---|
| bug | ✗ |
ab |
diagnose and fix these |
| fix | ✎ |
af |
apply these changes |
| suggestion | ✦ |
as |
evaluate; apply if sound |
| nitpick | ▫ |
an |
low priority — batch these together |
| issue | ⚑ |
ai |
do NOT fix — summarize these for tracking |
aa also offers no type: a note with no directive.
| Cursor on | Annotates |
|---|---|
| A diff line | That line |
| A visual selection | Those lines |
| A hunk header | The hunk |
| A file header, or a filler row in split | The file |
opts.types replaces the full set. Only name and key are required:
types = {
{ name = "bug", key = "b", directive = "diagnose and fix these" },
{ name = "nitpick", key = "n", directive = "ignore unless trivial" },
{ name = "question", key = "q" }, -- binds `aq`
}Start from require("codereview.types").defaults. See :help codereview-annotations.
A note is in its type's colour everywhere it is drawn: under a diff line, on a card and in a
caption. The default colours link to the diagnostic groups, so a bug reads like an error and a
nitpick like a hint. ⚠ stale keeps its own colour. An untyped note stays grey, because it
gives the agent no instruction. To change a type's colour, set its hl; the glyph and the note
change together. See :help codereview-note-colour.
No review is necessary. Normal mode captures the file; a visual selection or a range captures those lines. Errors and warnings on those lines are attached to the note.
vim.keymap.set({ "n", "x" }, "<leader>ab", function()
require("codereview").annotate("bug")
end, { desc = "Annotate as a bug" })
vim.keymap.set({ "n", "x" }, "<leader>aa", function()
require("codereview").annotate() -- pick the type
end, { desc = "Annotate" })
-- Send one annotation now, without the queue:
vim.keymap.set({ "n", "x" }, "<leader>aB", function()
require("codereview").annotate("bug", nil, { immediate = true })
end, { desc = "Send a bug now" }):12,20CodeReviewAnnotate bug:CodeReviewOverlay opens a margin to the right of the current window. It shows one card for
each queued annotation of the file, on the screen row of its first line:
▌ ✗ bug 12–15 ⚠ stale
▌ why is this branch unreachable?
The header has the type's icon, name and lines. The header and the note are in the type's colour. Cards move with the code when you scroll or edit. A card that would cover the next one pushes it down. A whole-file card stays at the top. Each covered line gets a sign in the type's colour, and a tint (see below).
The margin follows you. It is beside one window per tab page. When you open another file in
that window, or move the cursor into another split that holds a file, the margin shows that
file. A floating window does not move it, and you can move the cursor into the margin itself.
A file opened from the review with <CR> gets a margin in its new tab.
- A file with no annotation to draw (or a file of another checkout) shows one quiet line, "no annotations in this file", so the layout does not jump.
- A window that holds no file (help, a terminal, a plugin's buffer) closes the margin. It comes back when that window holds a file again.
- Closing the margin yourself (
:qor<C-w>cin it) turns the overlay off. Closing its tab page does not.
<C-w>l into the margin puts the cursor on the card of the line you came from, or on the
nearer card. There, the queue float's keys act on the card under the cursor, with no review
open:
| Key | Action |
|---|---|
<CR> |
Go to the card's line in the code window (where the line is now, after edits) |
e / t / x |
Edit the note / change the type / drop it |
gy |
Copy the batch |
<C-t> |
Choose where the batch goes |
<C-s> / <C-a> |
Submit the batch / submit it under a preamble |
q |
Turn the overlay off (there is no <Esc>) |
? |
List these keys |
They are bound on the margin's buffer only, so e, t and x hide none of your motions.
:CodeReviewOverlay inline draws the same annotations as captions: one virtual line above
each annotated line, at the column where the code starts:
╭─ ✗ ⚠ stale why is this branch unreachable? The guard above already
returns on every path that reaches it.
12 if mode == "split" then
The connector, the icon and the full note are in the type's colour, with ⚠ stale between them
when it applies. The note wraps to the window's width, less the number, sign and fold columns, and
it wraps again when you resize the window. A whole-file annotation's caption is above line 1.
Several annotations on one line stack in queue order. The covered lines get the same signs as
in the margin. A caption moves with the code when you edit above it.
Captions take no window and have no keys: to act on an annotation, use the queue float or
switch to the margin. A file with nothing to draw shows nothing. Captions are drawn when you
enter a buffer, when you resize the window, and when you write the buffer. A change to the
queue draws them again in every loaded buffer that has them, so an annotation you drop or edit
from the queue float changes in a file you are not in, and a submit clears every file of the
batch; the files you are not in cost no git process. A write checks ⚠ stale again for the annotations captured
from that file, so the flag shows after you change and write it, and goes when you write the
original content back. A file changed outside Neovim is checked when you next enter or write it.
In both styles, each line an annotation covers gets a background in its type's colour: your normal background, pulled 12% of the way toward the colour of the type's group. It is computed from your colorscheme, and computed again when you change it.
- The line the cursor is on has no tint, so your cursor line shows. When you move the cursor through a range, the tint moves off the line you enter and back onto the line you leave. Nothing else is drawn again on a cursor movement.
- Where two ranges overlap, the line takes the colour of the type that comes first in your
typeslist, as the file tree's mark does. - A whole-file annotation and an untyped annotation get no tint. Their lines keep the sign.
- A terminal without true colour (
termguicolorsoff) gets no tint. The signs stay. - Dropping the annotation or turning the overlay off takes the tint away. The tint moves with the code when you edit above it.
overlay.tint = { enabled = false } turns it off. overlay.tint.strength sets how far the
background is pulled, from 0 to 1; a value outside that range is an error at setup(). The
review view gets no tint: its lines keep the diff's colours.
:CodeReviewOverlay margin and :CodeReviewOverlay inline turn the overlay on in that style,
or change the drawing when it is on already; they never turn it off. The bare command still
toggles. The style lasts for the session; overlay.style sets where each session starts.
Not drawn, in either style: annotations on deleted lines or past the end of the file (the toggle tells you how many), sent annotations, bare notes, and other checkouts' annotations. Nothing is stored and the buffer is not changed.
vim.keymap.set("n", "<leader>ao", function()
require("codereview").overlay() -- returns true when it is now on
end, { desc = "Toggle the review overlay" })
vim.keymap.set("n", "<leader>ai", function()
require("codereview").overlay("inline") -- on, as captions
end, { desc = "Review overlay as captions" })See :help codereview-overlay.
Annotations are grouped by type, in the configured order. An optional preamble (<C-a>)
goes first.
the auth rewrite is the part to read — the route moves are mechanical
Code review — 4 annotations on branch vs origin/master (8 files, 6 reviewed)
## Bugs (2) — diagnose and fix these
### 1. @apps/api/src/main.ts#L20-21
why the rename? no callers were updated
### 2. apps/api/src/routes.ts:14 (deleted)
```diff
-router.use(legacyAuth)
```
was this dropped on purpose?
## Untyped (1)
### 3. @apps/api/src/db.ts#L8
worth a look before we ship thisAfter a submit, :CodeReview since-batch shows what the agent changed. The winbar shows
↺N: the number of annotated files the agent did not touch.
Defaults:
opts = {
context = 3, -- git diff -U
untracked = true, -- show untracked files
syntax = true, -- treesitter highlighting
max_syntax_bytes = 256 * 1024,
layout = "unified", -- or "split" (toggle: gl)
spans = true, -- highlight the changed characters inside a line
wrap = false, -- (toggle: gw)
solo = false, -- one file at a time (toggle: go)
archived = true, -- show sent annotations, dimmed (toggle: gA)
muted = { enabled = true, strength = 0.5 }, -- dim the pane without focus
faded = { enabled = true, strength = 0.35 }, -- dim the files you are not in
counterpart = { enabled = true, strength = 0.25 }, -- mark the opposite row in split
panel = { enabled = true, width = 34, position = "left" },
overlay = { -- :CodeReviewOverlay
enabled = false, width = 40, style = "margin", -- or "inline"
tint = { enabled = true, strength = 0.12 }, -- colour the lines an annotation covers
},
icons = {
reviewed = "✓", annotated = "●", unreviewed = "○",
collapsed = "▸", expanded = "▾", change_bar = "▌",
untouched = "↺", continuation = "↳",
progress_full = "█", progress_empty = "░",
caption = "╭─",
},
types = nil, -- the five types above
}All highlights are CodeReview* groups linked with default = true, so your colorscheme
can override them. See :help codereview-highlights.
All optional. They connect the plugin to your agent, pickers and icons.
| Adapter | Supplies | Default |
|---|---|---|
send(payload, target) |
Delivers the batch. Return false, reason on failure |
+ register |
pick_target(cb) |
A target { short, cwd } |
None |
pick_file(cb) |
A file for @ in the composer |
@ is literal |
compose(ctx, on_accept, label) |
The note editor | Built-in composer |
open_diff(spec) |
Opens a file in your diff tool (gd) |
gd not mapped |
pick_checkout(checkouts, cb) |
The checkout picker | Built-in picker |
file_icon(path) |
Glyph and highlight group for a file | None |
dir_icon(path) |
Glyph and highlight group for a directory | None |
opts = {
send = function(payload, target)
vim.fn.setreg("+", payload) -- replace with your agent delivery
return true
end,
open_diff = function(spec) -- { path, before, after, line }
local rev = spec.after and (spec.before .. ".." .. spec.after) or spec.before
vim.cmd(("DiffviewOpen %s -- %s"):format(rev, vim.fn.fnameescape(spec.path)))
end,
file_icon = function(path)
return require("nvim-web-devicons").get_icon(path, nil, { default = true })
end,
dir_icon = function(path)
return MiniIcons.get("directory", vim.fs.basename(path))
end,
}Full contracts: :help codereview-adapters. Wiring to a Claude session over herdr:
docs/herdr.md.
The queue, reviewed marks, commit selections and sent batches persist per checkout, under
stdpath("state")/codereview/. If a file changes, its reviewed mark is removed and its
annotations are marked ⚠ stale.
The overlay checks this again each time it draws a file, but only for annotations you captured
from a buffer, and only for the file it draws. It hashes the file on disk: a change you
have not written does not count, and it hashes again only when the file's size or modification
time changes. An annotation from the review view keeps the flag the review view gave it. No
line moves. See :help codereview-persistence-staleness.
| Where | What |
|---|---|
:help codereview |
Full reference |
docs/rationale.md |
Why the behavior is what it is |
docs/adr/ |
Architecture decisions |
CONTEXT.md |
Project vocabulary |
docs/design-notes.md |
Non-obvious constraints |
make hooks # install the commit-msg hook
make deps # clone plenary into .tests/
make all # lint + full suite (~5s). Run before every commit
make perf # timing report, not a gateSee CONTRIBUTING.md and tests/README.md. Open an
issue first for anything with a design decision behind it. Security reports go through
SECURITY.md.
This plugin was written with Claude Code. Agent-assisted
contributions are welcome; CLAUDE.md has the workflow.
MIT © Felipe Valencia
