Skip to content

About

Neovim code review: unified syntax-highlighted diff, typed annotations, batch submit to an agent

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

codereview-annotator.nvim

test Neovim 0.12+ License: MIT

Review a diff in Neovim. Annotate what needs work. Send the whole review to a coding agent as one message.

Annotate, mark reviewed, submit the batch

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.

Install

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.

Quick start

: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.

Commands

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

Keymaps

Diff

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.

Navigation

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

File 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

Composer

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

Queue float (Q)

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

Last-batch float (gb)

Read-only. q or <Esc> closes it.

Commit list

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.

Annotations

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

Custom types

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.

Annotate from any buffer

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

The overlay

: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 (:q or <C-w>c in 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.

Captions

: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.

The tint

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 types list, 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 (termguicolors off) 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.

The payload

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 this

After a submit, :CodeReview since-batch shows what the agent changed. The winbar shows ↺N: the number of annotated files the agent did not touch.

Configuration

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.

Adapters

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.

Persistence

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.

Documentation

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

Development

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 gate

See 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.

License

MIT © Felipe Valencia

About

Neovim code review: unified syntax-highlighted diff, typed annotations, batch submit to an agent

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages