A Neovim plugin that provides VSCode-style side-by-side diff rendering with two-tier highlighting.
https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e
Demo: Quick walkthrough of diff features
curl or wget (for automatic binary download)nui.nvim (for explorer UI)No compiler required! The plugin automatically downloads pre-built binaries from GitHub releases.
Minimal installation:
{
"esmuellert/codediff.nvim",
dependencies = { "MunifTanjim/nui.nvim" },
cmd = "CodeDiff",
}
Note: The plugin automatically adapts to your colorscheme's background (dark/light). It uses
DiffAddandDiffDeletefor line-level diffs, and auto-adjusts brightness for character-level highlights (1.4x brighter for dark themes, 0.92x darker for light themes). See Highlight Groups for customization.
With custom configuration:
{
"esmuellert/codediff.nvim",
dependencies = { "MunifTanjim/nui.nvim" },
cmd = "CodeDiff",
opts = {
-- Highlight configuration
highlights = {
-- Line-level: accepts highlight group names or hex colors (e.g., "#2ea043")
line_insert = "DiffAdd", -- Line-level insertions
line_delete = "DiffDelete", -- Line-level deletions
-- Character-level: accepts highlight group names or hex colors
-- If specified, these override char_brightness calculation
char_insert = nil, -- Character-level insertions (nil = auto-derive)
char_delete = nil, -- Character-level deletions (nil = auto-derive)
-- Brightness multiplier (only used when char_insert/char_delete are nil)
-- nil = auto-detect based on background (1.4 for dark, 0.92 for light)
char_brightness = nil, -- Auto-adjust based on your colorscheme
-- Conflict sign highlights (for merge conflict views)
-- Accepts highlight group names or hex colors (e.g., "#f0883e")
-- nil = use default fallback chain
conflict_sign = nil, -- Unresolved: DiagnosticSignWarn -> #f0883e
conflict_sign_resolved = nil, -- Resolved: Comment -> #6e7681
conflict_sign_accepted = nil, -- Accepted: GitSignsAdd -> DiagnosticSignOk -> #3fb950
conflict_sign_rejected = nil, -- Rejected: GitSignsDelete -> DiagnosticSignError -> #f85149
},
-- Diff view behavior
diff = {
disable_inlay_hints = true, -- Disable inlay hints in diff windows for cleaner view
max_computation_time_ms = 5000, -- Maximum time for diff computation (VSCode default)
hide_merge_artifacts = false, -- Hide merge tool temp files (*.orig, *.BACKUP.*, *.BASE.*, *.LOCAL.*, *.REMOTE.*)
original_position = "left", -- Position of original (old) content: "left" or "right"
conflict_ours_position = "right", -- Position of ours (:2) in conflict view: "left" or "right"
},
-- Explorer panel configuration
explorer = {
position = "left", -- "left" or "bottom"
width = 40, -- Width when position is "left" (columns)
height = 15, -- Height when position is "bottom" (lines)
indent_markers = true, -- Show indent markers in tree view (│, ├, └)
initial_focus = "explorer", -- Initial focus: "explorer", "original", or "modified"
icons = {
folder_closed = "", -- Nerd Font folder icon (customize as needed)
folder_open = "", -- Nerd Font folder-open icon
},
view_mode = "list", -- "list" or "tree"
file_filter = {
ignore = {}, -- Glob patterns to hide (e.g., {"*.lock", "dist/*"})
},
},
-- History panel configuration (for :CodeDiff history)
history = {
position = "bottom", -- "left" or "bottom" (default: bottom)
width = 40, -- Width when position is "left" (columns)
height = 15, -- Height when position is "bottom" (lines)
initial_focus = "history", -- Initial focus: "history", "original", or "modified"
view_mode = "list", -- "list" or "tree" for files under commits
},
-- Keymaps in diff view
keymaps = {
view = {
quit = "q", -- Close diff tab
toggle_explorer = "<leader>b", -- Toggle explorer visibility (explorer mode only)
next_hunk = "]c", -- Jump to next change
prev_hunk = "[c", -- Jump to previous change
next_file = "]f", -- Next file in explorer/history mode
prev_file = "[f", -- Previous file in explorer/history mode
diff_get = "do", -- Get change from other buffer (like vimdiff)
diff_put = "dp", -- Put change to other buffer (like vimdiff)
open_in_prev_tab = "gf", -- Open current buffer in previous tab (or create one before)
toggle_stage = "-", -- Stage/unstage current file (works in explorer and diff buffers)
},
explorer = {
select = "<CR>", -- Open diff for selected file
hover = "K", -- Show file diff preview
refresh = "R", -- Refresh git status
toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views
stage_all = "S", -- Stage all files
unstage_all = "U", -- Unstage all files
restore = "X", -- Discard changes (restore file)
},
history = {
select = "<CR>", -- Select commit/file or toggle expand
toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views
},
conflict = {
accept_incoming = "<leader>ct", -- Accept incoming (theirs/left) change
accept_current = "<leader>co", -- Accept current (ours/right) change
accept_both = "<leader>cb", -- Accept both changes (incoming first)
discard = "<leader>cx", -- Discard both, keep base
next_conflict = "]x", -- Jump to next conflict
prev_conflict = "[x", -- Jump to previous conflict
diffget_incoming = "2do", -- Get hunk from incoming (left/theirs) buffer
diffget_current = "3do", -- Get hunk from current (right/ours) buffer
},
},
},
}
The C library will be downloaded automatically on first use. No build step needed!
The plugin automatically manages the C library installation:
Automatic Updates:
Manual Installation Commands:
" Install/update the library manually
:CodeDiff install
" Force reinstall (useful for troubleshooting)
:CodeDiff install!
Version Management:
The installer reads the VERSION file to download the matching library version from GitHub releases. This ensures compatibility between the Lua code and C library.
If you prefer to install manually without a plugin manager:
git clone https://github.com/esmuellert/codediff.nvim ~/.local/share/nvim/codediff.nvim
init.lua:vim.opt.rtp:append("~/.local/share/nvim/codediff.nvim")
The plugin requires a C library binary in the plugin root directory. The plugin auto-detects these filenames:
libvscode_diff.so or libvscode_diff_<version>.so (Linux/BSD)libvscode_diff.dylib or libvscode_diff_<version>.dylib (macOS)libvscode_diff.dll or libvscode_diff_<version>.dll (Windows)Option A: Download from GitHub releases (recommended)
Download the appropriate binary from the GitHub releases page and place it in the plugin root directory. Rename it to match the expected format: libvscode_diff.so/.dylib/.dll or libvscode_diff_<version>.so/.dylib/.dll. Linux users: If your system lacks OpenMP, also download libgomp_linux_{arch}_{version}.so.1 and rename it to libgomp.so.1 in the same directory.
Option B: Build from source
Build requirements: C compiler (GCC/Clang/MSVC/MinGW) or CMake 3.15+
Using build scripts (no CMake required):
# Linux/macOS/BSD
./build.sh
# Windows
build.cmd
Or using CMake:
cmake -B build
cmake --build build
Both methods automatically place the library in the plugin root directory.
The :CodeDiff command supports multiple modes:
Open an interactive file explorer showing changed files:
" Show git status in explorer (default)
:CodeDiff
" Show changes for specific revision in explorer
:CodeDiff HEAD~5
" Compare against a branch
:CodeDiff main
" Compare against a specific commit
:CodeDiff abc123
" Compare two revisions (e.g. HEAD vs main)
:CodeDiff main HEAD
Show only changes introduced since branching from a base branch—exactly like a Pull Request:
" Compare merge-base(main, HEAD) vs working tree
" Shows only YOUR changes since you branched from main
:CodeDiff main...
" Compare merge-base(main, HEAD) vs HEAD (committed changes only)
:CodeDiff main...HEAD
" Compare merge-base between two branches
:CodeDiff develop...feature/new-ui
This uses git merge-base semantics (equivalent to git diff main...HEAD), showing only the changes introduced on your branch, not changes that happened on the base branch since you branched.
Compare the current buffer with a git revision:
" Compare with last commit
:CodeDiff file HEAD
" Compare with previous commit
:CodeDiff file HEAD~1
" Compare with specific commit
:CodeDiff file abc123
" Compare with branch
:CodeDiff file main
" Compare with tag
:CodeDiff file v1.0.0
" Compare two revisions for current file
:CodeDiff file main HEAD
" PR-like diff: compare merge-base(main, HEAD) vs working tree
:CodeDiff file main...
Requirements:
Behavior:
Compare two arbitrary files side-by-side:
:CodeDiff file file_a.txt file_b.txt
Compare two directories without git:
" Auto-detect directories
:CodeDiff ~/project-v1 ~/project-v2
" Explicit dir subcommand
:CodeDiff dir /path/to/dir1 /path/to/dir2
Shows files as Added (A), Deleted (D), or Modified (M) based on file size and modification time. Select a file to view its diff.
Review commits on a per-commit basis:
" Show last 50 commits
:CodeDiff history
" Show last N commits
:CodeDiff history HEAD~10
" Show commits in a range (great for PR review)
:CodeDiff history origin/main..HEAD
" Show commits for current file only
:CodeDiff history HEAD~20 %
" Show commits for a specific file
:CodeDiff history HEAD~10 path/to/file.lua
The history panel shows a list of commits. Each commit can be expanded to show its changed files. Select a file to view the diff between the commit and its parent (commit^ vs commit).
History Keymaps:
i - Toggle between list and tree view for files under commitsUse CodeDiff as your git merge tool for resolving conflicts:
git config --global merge.tool codediff
git config --global mergetool.codediff.cmd 'nvim "$MERGED" -c "CodeDiff merge \"$MERGED\""'
Use CodeDiff as your git diff tool for viewing changes:
git config --global diff.tool codediff
git config --global difftool.codediff.cmd 'nvim "$LOCAL" "$REMOTE" +"CodeDiff file $LOCAL $REMOTE"'
Then use git difftool to view diffs:
git difftool # View all uncommitted changes
git difftool HEAD~2 HEAD # Compare two commits
git difftool main feature-branch # Compare branches
git difftool -y # Skip confirmation prompts
-- Primary user API - setup configuration
require("codediff").setup({
highlights = {
line_insert = "DiffAdd",
line_delete = "DiffDelete",
char_brightness = 1.4,
},
})
-- Advanced usage - direct access to internal modules
local diff = require("codediff.diff")
local render = require("codediff.ui")
local git = require("codediff.git")
-- Example 1: Compute diff between two sets of lines
local lines_a = {"line 1", "line 2"}
local lines_b = {"line 1", "modified line 2"}
local lines_diff = diff.compute_diff(lines_a, lines_b)
-- Example 2: Get file content from git (async)
git.get_file_content("HEAD~1", "/path/to/repo", "relative/path.lua", function(err, lines)
if err then
vim.notify(err, vim.log.levels.ERROR)
return
end
-- Use lines...
end)
-- Example 3: Get git root for a file (async)
git.get_git_root("/path/to/file.lua", function(err, git_root)
if not err then
-- File is in a git repository
end
end)
C Module (libvscode-diff/): Fast diff computation and render plan generation
rangeMapping.ts data structuresLua FFI Layer (lua/vscode-diff/diff.lua): Bridge between C and Lua
Render Module (lua/vscode-diff/render/): Neovim buffer rendering
The plugin handles syntax highlighting differently based on buffer type:
Working files (editable):
Git history files (read-only):
The plugin defines highlight groups matching VSCode's diff colors:
CodeDiffLineInsert - Light green background for inserted linesCodeDiffLineDelete - Light red background for deleted linesCodeDiffCharInsert - Deep/dark green for inserted charactersCodeDiffCharDelete - Deep/dark red for deleted charactersCodeDiffFiller - Gray foreground for filler line slashes (╱╱╱)Dawnfox Light - Default configuration with auto-detected brightness (char_brightness = 0.92 for light themes):
Catppuccin Mocha - Default configuration with auto-detected brightness (char_brightness = 1.4 for dark themes):
Kanagawa Lotus - Default configuration with auto-detected brightness (char_brightness = 0.92 for light themes):
Default behavior:
DiffAdd and DiffDelete for line-level highlightsvim.o.background:background = "dark"): Brightness multiplied by 1.4 (40% brighter)background = "light"): Brightness multiplied by 0.92 (8% darker)char_brightness value if neededCustomization examples:
-- Use hex colors directly
highlights = {
line_insert = "#1d3042",
line_delete = "#351d2b",
char_brightness = 1.5, -- Override auto-detection with explicit value
}
-- Override character colors explicitly
highlights = {
line_insert = "DiffAdd",
line_delete = "DiffDelete",
char_insert = "#3fb950",
char_delete = "#ff7b72",
}
-- Mix highlight groups and hex colors
highlights = {
line_insert = "String",
char_delete = "#ff0000",
}
make clean && make
Run all tests:
make test # Run all tests (C + Lua integration)
Run specific test suites:
make test-c # C unit tests only
make test-lua # Lua integration tests only
For more details on the test structure, see tests/README.md.
codediff.nvim/
├── libvscode-diff/ # C diff engine
│ ├── src/ # C implementation
│ ├── include/ # C headers
│ └── tests/ # C unit tests
├── lua/
│ ├── codediff/ # Main Lua modules
│ │ ├── init.lua # Main API
│ │ ├── config.lua # Configuration
│ │ ├── diff.lua # FFI interface
│ │ ├── git.lua # Git operations
│ │ ├── commands.lua # Command handlers
│ │ ├── installer.lua # Binary installer
│ │ └── ui/ # UI components
│ │ ├── core.lua # Diff rendering
│ │ ├── highlights.lua # Highlight setup
│ │ ├── view/ # View management
│ │ ├── explorer/ # Git status explorer
│ │ ├── history/ # Commit history panel
│ │ ├── lifecycle/ # Lifecycle management
│ │ └── conflict/ # Conflict resolution
│ └── vscode-diff/ # Backward compatibility shims
├── plugin/ # Plugin entry point
│ └── codediff.lua # Auto-loaded on startup
├── tests/ # Test suite (plenary.nvim)
├── docs/ # Production docs
├── dev-docs/ # Development docs
├── Makefile # Build automation
└── README.md
This plugin follows VSCode's diff rendering architecture:
src/vs/editor/common/diff/rangeMapping.tssrc/vs/editor/browser/widget/diffEditor/registrations.contribution.tssrc/vs/editor/browser/widget/diffEditor/style.cssMIT
Contributions are welcome! Please ensure:
make test)