Note
finding the tool is helpful or not if it turns out good then maybe a improvment will appear right now this is pretty much llm generated code and i havnt really reviewed.
Code review in Neovim, so you don't have to open a browser.
Not a git client. It diffs, and it tries to do that very well.
Requires Neovim 0.10+ and git. Built against Neovim 0.13 and git 2.x.
Milestone 3 of 8. Working today:
:Differ [revspec]— the project-wide diff, one continuous scroll- true old/new line numbers in the gutter
- jump from any diff line into the real file at the real line
- hunk and file navigation
- sticky file header while scrolling
Not built yet: the sidebar file tree, the two-pane file diff, PR review.
:Differ " working copy — tracked changes and untracked files
:Differ main " only YOUR changes since you forked from main
:Differ HEAD~3 " against a commit
:Differ main..HEAD " explicit two-dot, tip to tip
:Differ main...feature " explicit three-dot between two refs
:Differ pr " pick from your open pull requests
:Differ pr 1234 " review that pull request
:Differ pr all " include closed and merged ones
:Differ pr author:@me " any gh search query
:Differ prune " delete the refs past PR reviews left behindA bare argument means three-dot: :Differ main shows what you changed, not
what your teammates merged into main after you branched. That's what GitHub
shows for a pull request, and it's what keeps a review readable while the base
branch moves under you. Type the dots yourself if you want tip-to-tip.
In the diff:
| key | does |
|---|---|
<CR> |
open this file at this line, in your own window |
]h / [h |
next / previous hunk |
]f / [f |
next / previous file |
dv |
two-pane diff of this file |
<Tab> |
jump to the file tree |
v |
mark this file viewed |
q |
close the review |
In the file tree:
| key | does |
|---|---|
<CR> |
show this file and move focus to the diff |
o |
show it, keep focus in the tree |
<CR> on a directory |
collapse / expand it |
dv |
two-pane diff of this file |
v |
mark viewed |
/ |
filter files by name |
<Esc> |
clear the filter |
<Tab> |
jump back to the diff |
<CR> is the point of the whole thing. The line numbers are real, so you land
in the actual file with LSP attached and go-to-definition live — which is the
one thing a browser can't do.
:Differ pr lists your open PRs; <CR> reviews one. Needs the GitHub CLI
(gh) authenticated — it's a hard requirement, not a soft fallback.
A PR diff is mechanically just a three-dot range, but knowing it's a PR buys
something a branch diff can't know: the exact commits the PR spans. gh
reports them as baseRefOid / headRefOid, and differ pins the review to
those. That matters because your local branch may have moved on from what you
pushed — reviewing local refs would show changes no reviewer on GitHub can see.
The PR head is fetched into refs/differ/pr/<n> (a default clone doesn't fetch
refs/pull/*), and the pinned SHAs are saved under stdpath("data") so
reopening reviews the same thing.
lazy.nvim:
{
"you/differ",
cmd = "Differ",
config = function() require("differ").setup({}) end,
}setup({}) is optional; the defaults are fine.
When a hunk replaces N lines with N lines, differ pairs them up and marks only
the bytes that actually differ — so changing 3000 to 5000 highlights the
number, not the whole line. Unequal runs are left alone: those are insertions or
removals, where no line-to-line correspondence exists to diff against.
The code is highlighted, with one deliberate exception: deleted lines stay diff-coloured.
That isn't laziness. A unified diff interleaves old and new lines, so the text in the buffer isn't valid source in any language — you can't just attach a parser to it. What is valid is the new side of each file: its added and context lines, in order, reconstruct the file as it exists after the change. differ rebuilds that text per file, parses it alone, and maps each captured range back to the line that produced it. A deleted line belongs to the old file and has no place in that reconstruction.
Set syntax = false to turn it off. It is budgeted (syntax_budget, default
20000 extmarks) so a huge review degrades to plain diff colouring rather than
stalling.
The review is one buffer with every file's diff concatenated, not a pair of synchronized panes. A single buffer has nothing to keep aligned, which is what makes whole-file additions and deletions render correctly.
Getting there means parsing git diff into a real model — files, hunks, and a
line-by-line record of which old and new line each row corresponds to. That
model is what pays for the gutter, the jump, the counts, and the navigation.
Two things about git's output are worth knowing, because both are silent traps
and both have regression tests in tests/run.lua:
A deleted line can look exactly like a file header. Remove a Lua doc comment
and the diff contains ---- doc comment — a - marker in front of --- doc comment. Scanning for ^--- to find file boundaries finds it, and truncates
the file list with no error. So file boundaries come from diff --git and
nothing else.
--numstat and --name-status frame renames differently. --name-status
writes R100\0old\0new, while --numstat writes 0\t0\t\0old\0new — an
empty path field with two extra NUL fields after it. Splitting one record per
NUL field reads that empty path as a filename and misattributes every file after
the first rename. Two formats, two parsers.
Every git call is run with --no-ext-diff (you may have delta configured) and
-c core.quotepath=false (or non-ASCII paths come back backslash-quoted and
break -z framing).
nvim --headless --clean -c "set rtp+=." -c "luafile tests/run.lua" -c "qa!"The parsers are pure functions and covered directly. tests/xcheck.lua
additionally validates the parser against real git output — for any repo and
range, every file's computed additions and deletions must equal what git --numstat reports:
REPO=/path/to/repo RANGE=HEAD~40...HEAD \
nvim --headless --clean -c "luafile tests/xcheck.lua" -c "qa!"Hand-written fixtures only prove the parser matches your idea of the format. This proves it matches git.