A Neovim plugin for reviewing git diffs and pull requests with an intuitive split-pane interface.
- Split-pane interface (file list + diff view)
- Tree or flat file list view
- Git status integration (M/A/D indicators)
- Inline review comments with counts per file
- Line change stats per file
- Vim-style navigation and folding controls
- Syntax highlighting with treesitter (strips +/- and shows as line backgrounds)
- Configurable keymaps, UI, and diff tools
- Auto-focus diff window for seamless navigation
- Statistics header with aggregate review metrics
- Dynamic comment window that resizes with content
- Neovim >= 0.8.0
- Git
- (Optional) Treesitter parsers for syntax highlighting
{
'hay-kot/diff-review.nvim',
dependencies = {
'nvim-treesitter/nvim-treesitter', -- Optional, for syntax highlighting
},
config = function()
require('diff-review').setup({
-- Configuration options (all optional)
layout = {
file_list_width = 45,
position = "center",
},
diff = {
syntax_highlighting = true, -- Enable treesitter-based syntax highlighting
},
})
end
}use {
'hay-kot/diff-review.nvim',
requires = {
'nvim-treesitter/nvim-treesitter', -- Optional, for syntax highlighting
},
config = function()
require('diff-review').setup()
end
}Clone the repository into your Neovim runtime path:
git clone https://github.com/hay-kot/diff-review.nvim ~/.local/share/nvim/site/pack/plugins/start/diff-review.nvimThen add to your init.lua:
require('diff-review').setup()Main diff review command:
:DiffReview " Open with prompt to select type
:DiffReview origin/main " Review against branch
:DiffReview pr:123 " Review pull request
:DiffReview close " Close review
:DiffReview toggle " Toggle visibility
:DiffReview list " List/switch reviews
:DiffReview copy [mode] " Copy to clipboard (comments/full/diff)
:DiffReview submit " Submit to GitHub
:DiffReview health " Health checkTop-level shortcuts:
:DiffReviewToggle " Quick toggle (preserves state)
:DiffReviewClose " Close review
:DiffReviewList " List/switch reviews
:DiffReviewCopy [mode] " Copy to clipboard (comments/full/diff)
:DiffNoteToggle [set] " Toggle note mode
:DiffNoteCopy [mode] " Copy notes to clipboard (notes/full)vim.keymap.set("n", "<leader>dr", "<cmd>DiffReviewToggle<CR>", { desc = "Toggle diff review" })
vim.keymap.set("n", "<leader>dR", "<cmd>DiffReviewList<CR>", { desc = "List/switch reviews" })
vim.keymap.set("n", "<leader>dy", "<cmd>DiffReviewCopy<CR>", { desc = "Copy review comments" })
vim.keymap.set("n", "<leader>dn", "<cmd>DiffNoteToggle<CR>", { desc = "Toggle note mode" })
vim.keymap.set("n", "<leader>dN", "<cmd>DiffNoteCopy<CR>", { desc = "Copy notes" })Open a review of your working tree, annotate as you go, then copy to clipboard for a commit message or PR description.
:DiffReview " open uncommitted changes (or pick type interactively)
" navigate with j/k, <Enter> to open a file diff
" <leader>c to add a comment, <leader>v to view all comments
:DiffReviewCopy full " copy comments with surrounding code context
:DiffReviewToggle " hide the panel when done, reopen later with state preserved:DiffReview origin/main " compare current HEAD against a branch
:DiffReview pr:123 " pull request review
" annotate files, then:
:DiffReviewCopy " copy comment summary to clipboard
:DiffReview submit " or submit directly to GitHub (requires gh CLI):DiffReviewList " open picker — select any saved review to resume itNote mode works outside of any diff context — annotate files during normal editing.
:DiffNoteToggle security-audit " start (or resume) a named note set
" navigate and edit files normally
" <leader>c to add a note, <leader>v to view all notes
:DiffNoteCopy full " copy notes with code context
:DiffNoteToggle " exit note modeUse :DiffNote switch <set> to jump between note sets without exiting mode.
You can navigate directly from the diff view to the actual file in your editor:
gf- Open file at cursor position in current window<C-w>f- Open file in horizontal split<C-w>gf- Open file in vertical split
Alternatively, use commands:
:DiffReviewOpenFile
:DiffReviewOpenFileSplit
:DiffReviewOpenFileVsplitBehavior:
- For added and context lines: Opens file at the corresponding line number
- For deleted lines: Shows notification (cannot navigate to deleted content)
- For hunk headers: Jumps to the start of the hunk
- Closes the review window and returns focus to the original window
The :DiffReviewToggle command allows you to close and reopen the review while preserving your session state:
-- Map to a convenient key
vim.keymap.set("n", "<leader>dr", ":DiffReviewToggle<CR>", { desc = "Toggle diff review" })Preserved state includes:
- Review context (PR number, branch comparison, uncommitted changes)
- Selected file and position
- Cursor and scroll position in diff view
- View mode (tree or flat)
State persists across Neovim restarts in ~/.local/share/nvim/diff-review/session_state.json (respects $XDG_DATA_HOME).
File List Panel:
j/k- Navigate between filesEnter- View file diffr- Refresh file list<Tab>- Toggle directory fold (tree view)o- Open directory (tree view)O- Close directory (tree view)<leader>t- Toggle tree/flat viewq- Close window
Diff Panel:
q- Close window- Standard Neovim navigation (
j,k,gg,G, etc.) gf- Open file at cursor in current window<C-w>f- Open file in horizontal split<C-w>gf- Open file in vertical split<leader>c- Add comment at cursor (normal) or for range (visual)<leader>e- Edit comment at cursor<leader>d- Delete comment at cursor<leader>l- List comments for current file<leader>v- View all comments in quickfix
Full configuration with defaults:
require('diff-review').setup({
-- Picker for review selection
picker = "snacks", -- "snacks" | "telescope"
-- Window layout
layout = {
file_list_width = 45,
min_width = 120,
min_height = 20,
position = "center", -- "center" | "left" | "right" | "top" | "bottom"
},
-- Keymaps
keymaps = {
next_file = "j",
prev_file = "k",
select_file = "<CR>",
close = "q",
refresh = "r",
toggle_fold = "<Tab>",
open_directory = "o",
close_directory = "O",
add_comment = "<leader>c",
edit_comment = "<leader>e",
delete_comment = "<leader>d",
list_comments = "<leader>l",
view_all_comments = "<leader>v",
},
-- File list options
file_list = {
view_mode = "tree", -- "flat" | "tree"
focus_diff_on_select = true, -- Auto-focus diff window when selecting a file
},
-- Git options
git = {
base_branch = "main",
diff_args = {},
},
-- UI options
ui = {
border = "rounded", -- "none" | "single" | "double" | "rounded"
show_icons = true,
show_stats_header = true, -- Show statistics header at top of file list
stats_header = {
separator = " | ", -- Separator between stats and file list
},
comment_window = {
initial_height = 10, -- Starting height of comment window
max_height = 30, -- Maximum height for comment window
dynamic_resize = true, -- Auto-resize based on content
},
status = {
symbols = {
modified = "M",
added = "A",
deleted = "D",
renamed = "R",
},
highlights = {
modified = "DiffChange",
added = "DiffAdd",
deleted = "DiffDelete",
renamed = "DiffReviewRenamed",
},
},
comment_line_bg = nil, -- Override comment line background (e.g., "#2f3b45")
comment_line_hl = nil, -- Link comment line highlight to another group
colors = {
added = "DiffAdd",
removed = "DiffDelete",
modified = "DiffChange",
selected = "Visual",
},
},
-- Diff display options
diff = {
context_lines = 3,
ignore_whitespace = false,
syntax_highlighting = true, -- Use treesitter to highlight code, show +/- as line backgrounds
tool = "git", -- "git" | "difftastic" | "delta" | "custom"
custom_command = "", -- used when tool = "custom", supports {args}
},
-- Persistence options
persistence = {
auto_save = true, -- Auto-save comments after each change
},
-- Note mode options
notes = {
default_set = "default", -- Default note set name
auto_restore = true, -- Auto-restore note mode on startup
},
})Auto-focus Diff Window: When enabled, selecting a file automatically focuses the diff window for immediate viewing:
require('diff-review').setup({
file_list = {
focus_diff_on_select = true, -- Default: true
},
})Statistics Header: Display aggregate review statistics at the top of the file list:
require('diff-review').setup({
ui = {
show_stats_header = true, -- Default: true
stats_header = {
separator = " | ", -- Line separator
},
},
})The statistics header shows:
- Review type (PR number, branch comparison, etc.)
- File counts by status (Modified, Added, Deleted)
- Total line additions and deletions
- Comment count
Dynamic Comment Window: The comment input window automatically resizes based on content:
require('diff-review').setup({
ui = {
comment_window = {
initial_height = 10, -- Starting height
max_height = 30, -- Maximum height
dynamic_resize = true, -- Default: true
},
},
})The file list displays the status of each file (Modified, Added, Deleted, Renamed) using single-letter indicators. You can customize both the symbols and their colors:
With brackets:
require('diff-review').setup({
ui = {
status = {
symbols = {
modified = "[M]",
added = "[A]",
deleted = "[D]",
renamed = "[R]",
},
},
},
})Longer text:
require('diff-review').setup({
ui = {
status = {
symbols = {
modified = "MOD",
added = "NEW",
deleted = "DEL",
renamed = "REN",
},
},
},
})Custom colors:
require('diff-review').setup({
ui = {
status = {
highlights = {
modified = "WarningMsg",
added = "String",
deleted = "ErrorMsg",
renamed = "Function",
},
},
},
})When syntax_highlighting = true, the plugin:
- Detects the file type from the file extension
- Applies treesitter syntax highlighting to the code
- Shows added lines with green background
- Shows deleted lines with red background
- Strips +/- prefixes for clean code display
This requires treesitter parsers for the languages you're reviewing. Install them with:
:TSInstall <language>By default the plugin uses git diff. You can switch to alternative tools:
require('diff-review').setup({
diff = {
tool = "difftastic",
},
})require('diff-review').setup({
diff = {
tool = "delta",
},
})For custom tools, provide a command with {args} placeholder:
require('diff-review').setup({
diff = {
tool = "custom",
custom_command = "my-diff-tool {args}",
},
})Comments are stored locally in .diff-review/ directory and persist across sessions. You can:
- Add comments to specific lines or ranges
- Edit and delete comments
- Export comments to markdown
- View all comments in quickfix list
When reviewing a PR (via :DiffReview pr:123), you can submit comments directly to GitHub:
:DiffReview submitThis requires the GitHub CLI to be installed and authenticated.
Note mode allows you to add comments to any files in your codebase without requiring diff or review context. Perfect for code audits, documentation, learning notes, or refactoring plans.
- Works anywhere: Comment on any file during normal editing, no special layout required
- Multiple note sets: Organize notes for different purposes (e.g., "security-audit", "refactoring")
- Persistent: Notes auto-save and persist across Neovim sessions
- Session restore: Automatically restores note mode on startup (configurable)
- Same UI: Reuses diff review keymaps and styling for consistency
All note mode operations:
:DiffNote enter [set] " Enter note mode (default set if not specified)
:DiffNote exit " Exit note mode
:DiffNote toggle [set] " Toggle note mode
:DiffNote clear " Clear all notes in current set
:DiffNote list " List and switch between sets
:DiffNote switch <set> " Switch to a different set
:DiffNote copy [mode] " Copy to clipboard (notes/full)Examples:
:DiffNote enter security-audit " Start security audit notes
:DiffNote toggle " Quick toggle
:DiffNote copy full " Export with code context- Enter note mode with
:DiffNote enter [set_name] - Navigate files normally (
:edit, buffer switches, etc.) - Add comments using the same keymaps as diff review:
<leader>c- Add comment at cursor (or range in visual mode)<leader>e- Edit comment at cursor<leader>d- Delete comment at cursor<leader>l- List comments for current file<leader>v- View all comments (across all files)
- Comments auto-save on each change
- Exit mode with
:DiffNote exitor toggle with:DiffNote toggle
Notes are stored in .diff-review/notes/ directory:
.diff-review/
├── notes/
│ ├── default.json # Default note set
│ ├── security-audit.json # Named set
│ └── refactoring.json # Another named set
Configure note mode behavior in your setup:
require('diff-review').setup({
notes = {
default_set = "default", -- Default note set name
auto_restore = true, -- Auto-restore note mode on startup
},
})Code audit:
:DiffNote enter security-audit
" Navigate files and add notes about security concerns
" Notes persist across sessionsRefactoring plan:
:DiffNote enter refactoring
" Document areas that need refactoring
" Switch between note sets as needed
:DiffNote switch technical-debtLearning codebase:
:DiffNote enter learning
" Add notes about how things work
" View all notes: <leader>vCopy all notes to clipboard in markdown format:
Notes only (with line numbers):
:DiffNote copyOutput format:
## Notes
**Note Set:** security-audit
**Date:** 2024-01-15 10:30
---
### src/auth.lua
- Line 45: Potential SQL injection vulnerability
- Lines 60-65: Missing input validation
### src/user.lua
- Line 23: TODO: Add rate limiting
---
**Total:** 3 notes across 2 filesFull export (with code context):
:DiffNote copy fullIncludes 2 lines of code context before/after each note with syntax highlighting.
Note mode and diff review mode can run simultaneously:
- Separate namespaces prevent conflicts
- Separate storage directories
- Both can be visible at the same time
- No conversion between notes and review comments
Export all review comments to markdown:
:DiffReview copy " Comments with line numbers
:DiffReview copy full " Comments with code context
:DiffReview copy diff " Annotated diff formatIf you see "Layout failed to open correctly" or the diff view doesn't appear, this is often caused by session restore plugins interfering with the layout creation.
Symptoms:
- Windows open briefly then disappear
- "Layout failed to open" warning
- More common when using
nvim -c "DiffReview ..."from command line - May happen with other buffers/tabs already open
Solution for auto-session users:
Add this to the very top of your init.lua (before plugins load):
-- Disable auto-session when using -c commands
for _, arg in ipairs(vim.v.argv) do
if arg == "-c" or arg == "+c" then
vim.g.auto_session_enabled = false
break
end
endThen update your auto-session config:
require("auto-session").setup({
-- Add DiffReview to bypass list
bypass_session_save_file_types = {
"", "blank", "alpha", "NvimTree", "nofile",
"Trouble", "dapui", "dap", "DiffReview"
},
-- Close diff-review before saving session
pre_save_cmds = {
"tabdo NvimTreeClose",
"silent! DiffReviewClose"
},
})And add conditional loading to the plugin spec:
{
"rmagatti/auto-session",
cond = function()
return vim.g.auto_session_enabled ~= false
end,
config = function()
-- your setup here
end,
}Solution for other session plugins (persisted.nvim, possession, etc.):
Disable the plugin when starting with -c commands, or defer diff-review operations:
vim.api.nvim_create_autocmd("User", {
pattern = "PersistedLoadPost", -- or your plugin's event
callback = function()
vim.defer_fn(function()
-- Safe to use diff-review now
end, 200)
end,
})Health check:
Run :DiffReviewHealth to diagnose layout issues and detect session plugins.
lua/diff-review/
├── init.lua # Main entry point
├── config.lua # Configuration management
├── layout.lua # Window/buffer management
├── file_list.lua # File list panel with tree/flat views
├── tree_view.lua # Tree structure building and flattening
├── diff.lua # Git diff execution/parsing
├── ui.lua # Comment UI rendering (diff review)
├── comments.lua # Comment storage and management
├── actions.lua # Comment actions (add/edit/delete)
├── popup.lua # Comment input popup
├── reviews.lua # Review session management
├── notes.lua # Note storage (note mode)
├── note_mode.lua # Note mode state management
├── note_persistence.lua # Note persistence layer
├── note_ui.lua # Note UI rendering
├── note_actions.lua # Note actions
└── note_export.lua # Note export functionality
Clone the repo and add it to your Neovim config:
vim.opt.runtimepath:append("~/path/to/diff-review.nvim")
require('diff-review').setup()Contributions are welcome! Please feel free to submit issues and pull requests.
MIT