Inline, lazygit-style unified diff overlay for the file you're editing.
Toggle :Lazydiff and the buffer turns into a Claude/lazygit-style view of
your uncommitted changes — deleted lines with -, added lines with +,
hunk headers in between — rendered in place via extmarks. No split, no
tab, no separate buffer.
Sometimes reading a diff inside lazygit is harder to scan than I'd like — I wanted to see the change in the file itself, with the surrounding code and indent intact, so it's obvious at a glance what actually moved. And ideally still be able to edit while the diff is on screen.
This matters more now that AI writes so much of the code I'm reading. When Claude hands me a 100-line patch, I want to actually look at it before I commit — not skim a separate diff panel and hope. Seeing the change inline, with the surrounding code and my own syntax highlighting intact, makes it obvious at a glance what's moved and lets me fix the bits I don't like without leaving the buffer.
A handful of existing Neovim plugins get close, but none quite hit that mark, so this one does.
Early development. v0.1: working tree vs HEAD, full-buffer overlay,
auto-refresh on save.
- Neovim ≥ 0.10 (uses
vim.diffwithresult_type = "indices"andvim.system) giton$PATH
{
"rashedInt32/lazydiff.nvim",
cmd = { "Lazydiff", "LazydiffOff", "LazydiffRefresh" },
config = function()
require("lazydiff").setup()
end,
}use({
"rashedInt32/lazydiff.nvim",
config = function()
require("lazydiff").setup()
end,
})| Command | What it does |
|---|---|
:Lazydiff |
Toggle the overlay on the current buffer |
:LazydiffOff |
Disable the overlay on the current buffer |
:LazydiffRefresh |
Recompute the diff and re-render (also runs auto on save) |
require("lazydiff").setup(opts) -- merge opts on top of defaults
require("lazydiff").toggle(bufnr) -- bufnr defaults to current
require("lazydiff").enable(bufnr)
require("lazydiff").disable(bufnr)
require("lazydiff").refresh(bufnr)require("lazydiff").setup({
ref = "HEAD", -- ref to diff the working tree against
signs = {
add = "+ ", -- prefix on added lines (inline virt_text)
delete = "- ", -- prefix on virt_lines for deleted content
context = " ",
},
show_hunk_header = true, -- show "@@ -a,b +c,d @@" between hunks
read_only = false, -- lock the buffer while overlay is on (off by default)
auto_refresh = true, -- refresh on BufWritePost / FileChangedShellPost
live_refresh = true, -- also refresh on TextChanged / TextChangedI
debounce_ms = 100, -- debounce window for live refresh
})The overlay is editable by default — the primary workflow this plugin
targets is reviewing an AI-generated patch and tweaking lines without
leaving diff view. While typing, the diff repaints automatically via a
debounced TextChanged listener (default 100 ms), and the HEAD blob
is fetched once per toggle and reused for every refresh, so live
updates don't shell out to git on every keystroke.
Two caveats to know about:
- Virtual deleted lines look like content but aren't part of the
buffer —
ddon one is a no-op. The line you actually want to delete is the buffer line, not the red preview above it. - If you commit / checkout while the overlay is active, the cached baseline goes stale. Toggle off and on again to refresh it.
To get the lock-the-buffer / viewer-style behaviour instead, set
read_only = true. To disable the live repaint and only refresh on
save, set live_refresh = false.
All defined as default = true so your overrides win. Foreground and
background are pulled from DiffAdd / DiffDelete / DiffChange /
Function at setup time and re-applied on ColorScheme so theme
switches still work.
LazydiffAdd / LazydiffChange paint the in-buffer line via
line_hl_group, so they're bg-only — setting a fg there would
override treesitter / syntax colours on the added line. LazydiffDelete
colours virtual deleted lines (which have no syntax of their own), so
it carries both fg and bg.
| Group | Default |
|---|---|
LazydiffAdd |
bg of DiffAdd (no fg, preserves syntax) |
LazydiffChange |
bg of DiffChange (no fg, preserves syntax) |
LazydiffDelete |
fg + bg of DiffDelete |
LazydiffAddSign |
bold, fg + bg of DiffAdd |
LazydiffDeleteSign |
bold, fg + bg of DiffDelete |
LazydiffHunkHeader |
fg of Function, no background |
If you want fg-only (Claude-style) instead of the lazygit/git diff
look, override the bg to NONE after setup():
vim.api.nvim_set_hl(0, "LazydiffAdd", { fg = "#a6e3a1", bg = "NONE" })
vim.api.nvim_set_hl(0, "LazydiffDelete", { fg = "#f38ba8", bg = "NONE" })The plugin doesn't bind keys by default. A common choice:
vim.keymap.set("n", "<leader>dd", "<cmd>Lazydiff<cr>", { desc = "Toggle lazydiff" })- On toggle, lazydiff runs
git show HEAD:<relpath>to fetch the committed version of the file. - The buffer contents and the committed blob are fed into
vim.diffwithalgorithm = "histogram"andctxlen = 0. - Hunk boundaries are normalized to strip equal leading/trailing lines.
- The renderer paints, in a single namespace:
- inline
virt_text+prefixes on added lines, withline_hl_group = LazydiffAddfor fg-only line tinting, virt_lines_abovefor deleted lines (-prefix +LazydiffDelete),- a
@@ -a,b +c,d @@virt_line above each hunk.
- inline
- The buffer is set to
nomodifiablewhile the overlay is active, so typos don't accidentally shred the diff. Toggle off to edit again.
| Plugin | Why this still exists |
|---|---|
| gitsigns.nvim | gutter signs only; no inline deletion content |
| mini.diff | overlay tints whole lines; no +/- prefix |
| diffview.nvim | separate tabpage, side-by-side |
| unified.nvim | ships with a mandatory file-tree explorer |
| inline-diff.nvim | word-level strikethrough — different mental model |
lazydiff is deliberately the smallest possible thing that gives you lazygit's diff layout in the file you're already editing.
MIT.
