Skip to content
zion-offPublic

About

vscode style neovim plugin for code review

Resources

Stars

16 stars

Watchers

1 watching

Forks

Latest commit

 

History

190 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

diff.nvim

image

A NeoVim plugin that replicates the Git source-control UX of VSCode's SCM sidebar — with an enhanced code-review annotation feature.


Features

Feature Details
File Status Panel Staged & unstaged changes with collapsible sections, status badges, right-aligned
Commit Graph Panel Recent commit history with HEAD, branch, remote, and tag ref badges
Mode bar A strip above the sidebar shows the branch you're looking at and the file panel's mode; click to switch either
Branch changes Press <leader>gB to see everything the branch changed since it left main, like a pull request's "Files changed" tab
Branch preview Browse another branch's commits without checking it out — pick from a floating list with <leader>gb
Split Diff View Side-by-side old/new diff computed in-process with Neovim's built-in xdiff — no git diff subprocess. An edited line sits beside its new version; a rewrite, or a line split over several, reads as removed lines beside added ones, top-aligned as on GitHub
Unified Diff View GitHub-style single column: old and new line numbers, removed lines above added ones; toggle with <leader>gv
Hunk headers Each run of lines is headed by its @@ -12,7 +12,9 @@ range and the function it is in, as on GitHub; click one to reveal the lines hidden above it
Accurate syntax highlighting Tree-sitter parses each whole file once (asynchronously on 0.11+), so collapsed context never breaks highlighting
Line-level colours Subtle red for removed, green for added — layered on top of syntax colours
Word-level highlights Darker red/green marks the exact tokens that changed within a line, when the two lines are mostly alike (between different lines they would mark nearly everything)
Filler lines Grey visual-only placeholders, across the gutter too, keep both panes aligned
Real line numbers The gutter shows file line numbers, not pane row numbers
Scroll sync Split panes stay locked together, whether you scroll with the keyboard, the mouse wheel over either pane, or with smoothscroll on
Line wrapping Long lines wrap at word boundaries, GitHub-style; the shorter side of each row is padded so both panes stay row-aligned (split view needs Neovim ≥ 0.10)
Gutter indicators Changed lines' numbers on a stronger tint, then a cell of the row's tint before the code
Hunk staging Stage or unstage the change under the cursor straight from the diff
Live updates Panels follow git activity from anywhere (CLI, other tools); the open diff follows edits on disk, keeping your place
Fast navigation ]f / [f move between files, gf jumps to the real file at the same line
Go to definition, hover gd asks your language server and stays in the review: the definition's line in this diff, the diff of another changed file, or a preview of an unchanged one; <C-o> comes back. K shows what the symbol is (see Go to Definition and Hover)
Annotation notes Select lines, press <leader>n, type a note — saved to an XDG Markdown file
Notes panel Toggle with <leader>N; dd deletes a note, q closes
Pull request review With gh logged in, read and write review comments on the branch's pull request, reply, resolve, and submit the review (see Pull Request Review)

Requirements

  • NeoVim ≥ 0.9 (developed and tested on 0.11; asynchronous tree-sitter parsing needs 0.11, older versions parse synchronously)
  • git on $PATH
  • Optional: the GitHub CLI (gh), logged in, for pull request review
  • No external plugin dependencies

Installation

lazy.nvim

{
  "zion-off/diff",
  config = function()
    require("diff").setup()
  end,
}

packer.nvim

use {
  "zion-off/diff",
  config = function()
    require("diff").setup()
  end,
}

Configuration

Call require("diff").setup(opts) once from your config. All fields are optional.

require("diff").setup({
  -- Sidebar position: "left" (default) or "right"
  sidebar_position = "left",

  -- Sidebar width in columns (default: 40)
  sidebar_width = 40,

  -- Notes panel width in columns (default: 40)
  notes_width = 40,

  -- Auto-refresh panels on FocusGained / BufWritePost (default: true)
  -- Also watches .git/index via libuv fs_event for immediate refresh.
  auto_refresh = true,

  -- Branch mode compares the branch against this branch (default: nil, which
  -- uses the remote's default branch origin/HEAD, else a local main or master).
  base_branch = nil,

  -- Mouse interactivity in the sidebar (default: true). Clicking a file opens
  -- its diff; clicking a commit expands/collapses it; clicking a section header
  -- toggles it. Hovering tints whatever a click would act on (rows, and @@
  -- hunk headers that hide lines) and highlights the window edge you can drag
  -- to resize (edges need Neovim 0.11+).
  -- Only enables Neovim's 'mouse' option while the interface is open, and
  -- 'mousemoveevent' only while its tab is the current one, restoring both.
  -- With 'mousemoveevent' on, moving the mouse in the middle of a key
  -- sequence (after <leader>, say) cancels it.
  mouse = true,

  -- Lines of context around each change (nil shows whole files).
  context_lines = 3,

  -- Soft-wrap long lines in the diff panes (default: true). In the split view
  -- the shorter side of each row is padded to the taller side's height, so the
  -- panes stay aligned while scrolling. Split panes narrower than 20 text
  -- columns scroll horizontally instead. Needs Neovim 0.10+ in the split view;
  -- on 0.9 split panes stay unwrapped.
  wrap = true,

  -- How diffs are drawn: "split" (default, side by side) or "unified" (one
  -- column, as on GitHub). keymaps.toggle_layout switches for the rest of the
  -- session. A split with no room for two panes is drawn unified.
  layout = "split",

  -- Log verbosity: "trace" | "debug" | "info" | "warn" (default) | "error" | "off".
  -- The log lives at stdpath("log")/diff.nvim.log; open it with :DiffNvimLog.
  log_level = "warn",

  -- Keybinding overrides (set any to false/"" to disable)
  keymaps = {
    toggle_sidebar       = "<leader>gs",
    toggle_sidebar_panel = "<leader>gS",
    copy_notes_path      = "<leader>gy",
    open_diff            = "<CR>",
    stage_file           = "s",
    unstage_file         = "u",
    collapse             = "z",
    next_hunk            = "]c",
    prev_hunk            = "[c",
    next_file            = "]f",
    prev_file            = "[f",
    goto_file            = "gf",
    goto_definition      = "gd",
    hover                = "K",
    jump_back            = "<C-o>",  -- back from a definition jump; elsewhere what <C-o> does anyway
    stage_hunk           = "s",
    unstage_hunk         = "u",
    leave_note           = "<leader>n",
    leave_comment        = "<leader>c",
    submit_comment       = "<CR>",   -- in the comment editor, normal mode
    show_comments        = "<leader>C",
    open_review          = "<leader>gr",
    toggle_notes         = "<leader>N",
    preview_branch       = "<leader>gb",
    branch_changes       = "<leader>gB",
    expand_context       = "zo",
    expand_all           = "zR",
    collapse_all         = "zM",
    toggle_layout        = "<leader>gv",
    commit_tooltip       = "K",
  },

  -- Highlight colour overrides — any valid :hi attribute table
  highlights = {
    -- e.g. { bg = "#0d1f0d" }
  },
})

Keybindings

Global

Key Action
<leader>gs Toggle interface

While interface is open

Key Action
<leader>gS Toggle sidebar panels (show/hide file + commit panels)
<leader>gy Copy session notes file path to clipboard
<leader>N Toggle notes panel
<leader>gb Preview another branch (open branch picker)
<leader>gB Toggle branch mode (see Branch Changes)
<leader>gv Toggle the diff view between split and unified
<leader>gr Review the branch's pull request (submit, discard drafts, open on GitHub)

File Status Panel

Key Action
<CR> / click Open diff for file / toggle section or directory
s Stage file (the cursor moves on to the next file)
u Unstage file
z Toggle directory / section collapse

The file shown in the diff view is highlighted in the panel.

Commit Graph Panel

Key Action
<CR> / click Expand/collapse a commit to list its changed files / open a file's diff
K Show full commit message tooltip

Branch Picker

Opened with <leader>gb (or :DiffNvimPreviewBranch).

Key Action
<any char> Filter the branch list (substring match)
<BS> / <C-h> Delete last filter character
<Down> / <C-n> / <Tab> Next branch
<Up> / <C-p> / <S-Tab> Previous branch
<CR> Preview the selected branch
<Esc> / q / <C-c> Cancel

Diff View

Key Action
]c / [c Next / previous change
]f / [f Next / previous file (same panel section, or same commit)
gf Open the real file at this line, in the window the interface was opened from
gd Go to the definition of the symbol under the cursor (see Go to Definition and Hover)
K Show what the symbol under the cursor is, from your language server; K again moves into the float
<C-o> Back to where the last gd left from; with none, Neovim's own <C-o>
s Stage the change under the cursor (unstaged working-tree diffs)
u Unstage the change under the cursor (staged diffs)
l / zo Reveal 10 more lines at each edge of the lines hidden above the @@ header under the cursor (zo uses the nearest one)
zR Show all context
zM Collapse back to the configured context
<leader>gv Toggle split / unified, keeping the cursor on its line
<leader>n Leave a note on current / visual selection
<leader>c Comment on the pull request at the current line / visual selection
<leader>C Show the pull request comments on this line
<leader>N Toggle notes panel
q Close diff view

j / k step over filler rows. The cursor starts on the first change, and stays on its line when the file changes on disk.

Clicking a @@ header (or l / zo on it) reveals the lines hidden above it; in the split view the header's text is drawn in the left pane, its band running on through the right. In the unified view, a note left on a selection that covers both removed and added lines goes on the side of its first line.

Hunk staging uses zero-context patches. A change directly next to a final line without a trailing newline cannot be expressed that way; the plugin says so instead of guessing, and the whole file can still be staged from the panel.

Notes Panel

Key Action
dd Delete note under cursor
q Close panel

Go to Definition and Hover

gd in the diff view goes to the definition of the symbol under the cursor without leaving the review:

  • In this file: the cursor moves to the definition, revealing it first if it is in collapsed context.
  • In another file the diff covers (same panel section, or same commit): the diff view opens that file at the definition.
  • In a file the diff does not cover: a float previews it. <CR> opens the file in the editor; q or <Esc> closes the float.
  • Several definitions: you pick one from a list.

<C-o> goes back to where each jump left from, across files.

Your language server answers about the file as the editor has it, so the line under the cursor is matched to its line in that file first. The first gd in a file loads it in the background, which starts your language server for it as opening it would. Nothing is loaded before then. At most 10 files loaded this way stay loaded, the least recently asked about going first, and the rest are unloaded when the diff view closes. A file you opened yourself in the meantime is never unloaded.

K shows what the symbol under the cursor is, as your language server describes it, in a float under the cursor; K again moves into the float. It asks the same way gd does, so the first K in a file loads it too.

Some lines have no matching line in the file the language server knows: a removed or rewritten line, or any line while you preview another branch. For those lines, K says why it has no answer, and gd searches the text of that side of the diff instead. It uses tree-sitter's scopes where the language has them, and Vim's own gd search otherwise, and the message says a text search was used. The search is also used when no language server attaches to the file within 5 seconds, or when the server does not answer within 10 seconds. After a file type has gone without a server once, its lookups use the search at once, until a server attaches to a file of that type.

User Commands

Command Description
:DiffNvimOpen Open sidebar
:DiffNvimClose Close sidebar
:DiffNvimToggle Toggle sidebar
:DiffNvimPreviewBranch Preview another branch's commits without checking it out
:DiffNvimNotes Toggle notes panel
:DiffNvimLayout [split|unified] Show diffs split or unified (no argument toggles)
:DiffNvimRefresh Re-fetch the panels, and the branch's pull request from GitHub
:DiffNvimReview Review the branch's pull request
:DiffNvimLog Open the log file

Mode Bar

A two-row strip at the top of the sidebar keeps the interface's modes in view:

 ⎇ feature/x ⌃⌄                   ↑2 ↓1
      Changes          Since main
  • The first row is the branch the panels show, as a pop-up button: click anywhere on it (or press <CR> on it) to open the branch picker, the same as <leader>gb. On the right it shows how far the branch is ahead of (↑) and behind (↓) its upstream, read with the working-tree status at no extra cost. Previewing another branch, or showing another worktree, tints the row and names the mode (Preview / Worktree); Done takes you back to your own branch.
  • The second row is a segmented control switching the file panel between your working-tree changes and everything the branch changed since its base (Since main), the same as <leader>gB. The selected half is raised.

Branch Changes

Press <leader>gB to switch the file panel into branch mode: instead of staged and unstaged changes it lists every file the branch changed since its merge base with the base branch — the same set a pull request's "Files changed" tab shows (git diff <base>...HEAD). Commits that landed on the base branch after the branch left it are not included, and uncommitted changes aren't either. The commit panel is closed and the file panel takes the whole sidebar. Press <leader>gB again to go back.

Each file opens as one diff from the merge base to the branch's latest version. In preview mode, branch mode shows the previewed branch's changes.

The base is the remote's default branch (origin/HEAD), falling back to a local main or master. Set base_branch to use a different one. It is looked up each time you enter branch mode.

Branch Preview

Press <leader>gb (or run :DiffNvimPreviewBranch) to open a floating picker listing all local and remote branches, sorted by most recent commit. Selecting a branch puts the interface into preview mode:

  • The commit panel is re-sourced from the chosen branch (git log <branch>), so you can browse its history and open per-file diffs — all without checking it out and without touching your working tree.
  • The file status panel shows a Preview: <branch> header instead of changes, since uncommitted working-tree changes belong only to the branch you actually have checked out.

The picker marks your current branch with (current); selecting it returns to normal live mode. Preview mode is read-only and is cleared when the interface is closed.

Worktrees

Branches checked out in another git worktree are marked (worktree). Selecting one switches the interface to that worktree: the file panel shows its staged and unstaged changes (diffs, staging and hunk staging all act on it) and the commit panel its history. Pick the branch of the worktree you opened the interface in to switch back.


Pull Request Review

When the GitHub CLI is installed and logged in (gh auth status) and the branch the sidebar shows has an open pull request, the interface reviews it the way GitHub's "Files changed" tab does. Without gh, or without a pull request, none of this appears. Everything goes through gh, so the plugin never handles a token.

  • The mode bar names the pull request (#123) and how many draft comments you have on it. Press <leader>gr or run :DiffNvimReview to open the review.
  • Branch mode compares the branch with the pull request's base branch, so its files and lines match GitHub's. In a fork's clone it uses the remote that points at the pull request's own repository, not the fork's origin.
  • Comment threads show on their lines in the diff: a sign (● open, ◆ your draft, a dimmed ● when resolved or outdated) and the first comment at the end of the line. Press <leader>C on the line (or click the sign) to open the thread in a float under it.
  • In the thread float, the keys are r reply, R resolve or unresolve, e edit your comment, x delete it (press twice to confirm), <Tab> move between buttons, <CR> press the one under the cursor, q or <leader>C close.
  • To comment, press <leader>c on a line or a visual selection. A float opens to write the comment in Markdown. Press <Esc>, then <CR> to add it (keymaps.submit_comment) or <Esc> again to cancel; <Tab> moves to the buttons. While a float holds text you haven't sent, opening another one (a reply, the review) is refused, so nothing you typed is lost.
  • Comments are drafts in a pending review, as with GitHub's "Start a review", until you submit the review as a comment, an approval or a request for changes. Drafts live on GitHub, so they survive closing Neovim and show in the browser. Replies made while you have drafts join them.
  • The review float (<leader>gr) has the summary box and its buttons on top and your draft comments listed under them, each with its file, line and text. The list scrolls with the mouse wheel, or with j/k after <S-Tab> from the editor (<Tab> goes back).
  • Discard drafts in the review float throws the pending review away (press twice to confirm).

Where comments can go:

  • On the branch's changes (branch mode), when your local branch is at the commit GitHub has for the pull request. If you have unpushed commits, or the pull request has commits you haven't pulled, the plugin asks you to push or pull first, because the line numbers would not match GitHub's. The comments on the branch's changes are hidden then, and the mode bar says "out of sync"; the comments on single commits still show.
  • On a single commit of the pull request (from the commit panel), on new-side lines. Removed lines can't take comments there, because GitHub measures them against the pull request's base rather than the commit's parent. A commit shows the comments that were made on it.
  • Not on uncommitted changes.

A draft review is tied to one commit. While you have drafts, new comments go only on that commit's diff; submit or discard the drafts to comment elsewhere.

Actions show at once: a comment, reply, resolve, edit, delete, submit or discard changes what you see the moment you make it, and goes to GitHub in the background, in the order you made them. If GitHub rejects one, a notification says what failed and why, any text you wrote for it is put in the unnamed register (p pastes it back), and the view goes back to what GitHub has.

The pull request is looked up when the interface opens and when the branch it shows changes, including a checkout made outside Neovim. It is fetched again after each action and on :DiffNvimRefresh, never in the background.

Notes Storage Format

Notes are stored in a per-session Markdown file at:

$XDG_DATA_HOME/diff.nvim/<repo-name>_<timestamp>.md

(Falls back to ~/.local/share/diff.nvim/ when XDG_DATA_HOME is unset.)

The timestamp (YYYYMMDDTHHmmss) is captured once when the plugin first writes a note in the session. The file is created lazily — only when the first note is actually written. Each Neovim session produces its own file.

Example filename: diff_20260507T142301.md

Each note looks like:

## Note — path/to/file.py, lines 42–57 (new side)

> The context manager here should use `contextlib.suppress` instead of bare
> except.

*2025-01-15 14:23:01*

---

Copy the session file path to the clipboard with <leader>gy, then paste it directly into a coding-agent prompt.


Highlight Groups

Override any group via vim.api.nvim_set_hl after setup(), or use the highlights config table:

Group Used for
DiffNvimAdded Added-line background
DiffNvimRemoved Removed-line background
DiffNvimFiller Filler-line background
DiffNvimAddedWord Word-level added token
DiffNvimRemovedWord Word-level removed token
DiffNvimSectionHeader "Staged Changes" / "Changes" headers
DiffNvimChevron ⌄ / › before a section or a commit
DiffNvimFileName File name in a panel
DiffNvimAddedFile Added or untracked file name
DiffNvimDeletedFile Deleted file name
DiffNvimDirName Directory row of a file tree
DiffNvimHint Explanatory text (empty states, counts)
DiffNvimStatusModified M badge
DiffNvimStatusAdded A badge
DiffNvimStatusDeleted D badge
DiffNvimStatusRenamed R badge
DiffNvimStatusUntracked ? badge
DiffNvimCommitHash Commit short hash
DiffNvimCommitMeta Author and age under a commit subject
DiffNvimCommitSubject Commit subject
DiffNvimRefHead HEAD ref pill
DiffNvimRefBranch Local branch pill
DiffNvimRefRemote Remote tracking pill
DiffNvimRefTag Tag pill
DiffNvimNoteHeader ## Note heading in notes panel
DiffNvimNoteText Note body text
DiffNvimActiveFile Panel row of the file shown in the diff view
DiffNvimHover Clickable row under the mouse pointer
DiffNvimEdgeHover Draggable window separator under the mouse pointer (drawn only while hovered)
DiffNvimEdgeHoverStatus Status line between panels under the mouse pointer (when it is the draggable edge)
DiffNvimHeader Filename bar above the diff
DiffNvimHeaderName File name in the header
DiffNvimHeaderLabel Directory and notes in the header
DiffNvimPanelTitle Panel heading ("Commits", "Notes")
DiffNvimPanelTitleDim Secondary text at the end of a panel heading
DiffNvimRule Rule under the diff header (other dividers are blank and no longer use this group)
DiffNvimGap Blank status line (a gap) under the plugin's windows
DiffNvimHunkHeader @@ -12,7 +12,9 @@ hunk header
DiffNvimHunkHeaderDecl Function name after a hunk header
DiffNvimHunkHeaderHover / DiffNvimHunkHeaderDeclHover Hunk header under the mouse pointer (click to expand)
DiffNvimAddedNr / DiffNvimRemovedNr Line numbers of added / removed lines
DiffNvimAddedMarker / DiffNvimRemovedMarker Padding between the line numbers and the code, in the row's tint
DiffNvimModeBranch Branch name in the mode bar
DiffNvimModeCell / DiffNvimModeCellHover The branch row of the mode bar (under the mouse pointer)
DiffNvimModeTag ⌃⌄ and the upstream counts in the branch row
DiffNvimModeAway / DiffNvimModeAwayBranch / DiffNvimModeAwayTag The branch row while previewing another branch or worktree
DiffNvimModeDone Done, which returns to your branch
DiffNvimModeAwayHover The branch row or Done under the mouse pointer while previewing
DiffNvimModeTrack / DiffNvimModeTrackHover Unselected half of the mode control (under the mouse pointer)
DiffNvimModeSegment Selected half of the mode control
DiffNvimModePr / DiffNvimModeAwayPr The pull request label in the branch row (while previewing)
DiffNvimThreadMarker / DiffNvimThreadDraft / DiffNvimThreadResolved Sign of a line with open / draft / resolved or outdated review comments
DiffNvimThreadVirtText First comment shown at the end of the line
DiffNvimThreadAuthor / DiffNvimThreadMeta Author, and age and state, in the comments float
DiffNvimButton / DiffNvimButtonPrimary / DiffNvimButtonDanger Buttons in the review floats (the one Enter presses, ones that discard)
DiffNvimButtonHover / DiffNvimButtonCursor Button under the mouse pointer / the cursor

Events

The plugin fires User autocommands that other code can hook into:

Pattern When data
DiffNvimViewChanged The diff view opened another file, or closed { kind, path, staged, hash }, or {} when closed
DiffNvimGitChanged The repository's index or HEAD changed —
DiffNvimPrChanged The branch's pull request or its review changed —

Troubleshooting

Set log_level = "debug" and run :DiffNvimLog. The log records every git command with its duration, watcher events, and how long each diff took to load and render.


Development

Run the test suite with:

tests/run.sh            # all specs
tests/run.sh interface  # specs whose file name contains "interface"

Each spec runs in its own headless Neovim and creates throwaway git repositories, so the tests need nvim and git on $PATH and nothing else.


License

MIT

About

vscode style neovim plugin for code review

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages