A NeoVim plugin that replicates the Git source-control UX of VSCode's SCM sidebar — with an enhanced code-review annotation feature.
| 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 |
| 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 |
| Filler lines | Grey visual-only placeholders 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 | Coloured ▍ strip marks changed regions |
| 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 |
| 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 |
- NeoVim ≥ 0.9 (developed and tested on 0.11; asynchronous tree-sitter parsing needs 0.11, older versions parse synchronously)
giton$PATH- No external plugin dependencies
{
"zion-off/diff",
config = function()
require("diff").setup()
end,
}use {
"zion-off/diff",
config = function()
require("diff").setup()
end,
}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, "hidden
-- lines" separators) 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,
-- 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",
stage_hunk = "s",
unstage_hunk = "u",
leave_note = "<leader>n",
toggle_notes = "<leader>N",
preview_branch = "<leader>gb",
branch_changes = "<leader>gB",
expand_context = "zo",
expand_all = "zR",
collapse_all = "zM",
commit_tooltip = "K",
},
-- Highlight colour overrides — any valid :hi attribute table
highlights = {
-- e.g. { bg = "#0d1f0d" }
},
})| Key | Action |
|---|---|
<leader>gs |
Toggle interface |
| 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) |
| 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.
| Key | Action |
|---|---|
<CR> / click |
Expand/collapse commit / open commit file diff |
K |
Show full commit message tooltip |
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 |
| 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 |
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 collapsed section under the cursor (zo uses the nearest one) |
zR |
Show all context |
zM |
Collapse back to the configured context |
<leader>n |
Leave a note on current / visual selection |
<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.
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.
| Key | Action |
|---|---|
dd |
Delete note under cursor |
q |
Close panel |
| 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 |
:DiffNvimRefresh |
Re-fetch the panels |
:DiffNvimLog |
Open the log file |
A two-row strip at the top of the sidebar keeps the interface's modes in view:
⎇ feature/x ▾
Changes Branch changes
- The first row is the branch the panels show. Click it (or press
<CR>on it) to open the branch picker, the same as<leader>gb. While another branch is previewed, or another worktree shown, it is tagged(preview)/(worktree)and a✕takes you back to your own branch. - The second row switches the file panel between your working-tree changes and
the branch's changes, the same as
<leader>gB.
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.
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.
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.
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.
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 |
DiffNvimGutterAdded |
Gutter ▍ for added |
DiffNvimGutterRemoved |
Gutter ▍ for removed |
DiffNvimGutterChanged |
Gutter ▍ for changed |
DiffNvimSectionHeader |
"Staged Changes" / "Changes" headers |
DiffNvimStagedFile |
Staged file name |
DiffNvimUnstagedFile |
Unstaged file name |
DiffNvimDeletedFile |
Deleted file name |
DiffNvimStatusModified |
[M] badge |
DiffNvimStatusAdded |
[A] badge |
DiffNvimStatusDeleted |
[D] badge |
DiffNvimStatusRenamed |
[R] badge |
DiffNvimStatusUntracked |
[?] badge |
DiffNvimCommitHash |
Commit short hash |
DiffNvimCommitAuthor |
Commit author |
DiffNvimCommitTime |
Relative timestamp |
DiffNvimCommitSubject |
Commit subject |
DiffNvimRefHead |
HEAD ref badge |
DiffNvimRefBranch |
Local branch badge |
DiffNvimRefRemote |
Remote tracking badge |
DiffNvimRefTag |
Tag badge |
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 |
DiffNvimEdgeHoverStatus |
Status line between panels under the mouse pointer (when it is the draggable edge) |
DiffNvimHeader |
Filename bar above the diff |
DiffNvimSeparator |
Collapsed-context marker |
DiffNvimSeparatorHover |
Collapsed-context marker under the mouse pointer |
DiffNvimModeBranch |
Branch name in the mode bar |
DiffNvimModeTag |
(preview) / (worktree) tag and ▾ in the mode bar |
DiffNvimModeClose |
✕ that returns to your branch |
DiffNvimModeTab |
Inactive mode in the mode bar |
DiffNvimModeTabActive |
Active mode in the mode bar |
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 | — |
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.
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.
MIT