Short git hunk previews and CodeDiff in LazyVim
How I replaced diffview with CodeDiff and wired gh and ]h for wrapped gitsigns hunk previews in LazyVim.
I wanted to preview git hunks at the cursor without long leader chains, and I needed a popup that shows the full hunk when a markdown line wraps across several screen rows.
Desired workflow
My LazyVim setup had three friction points in the git diff workflow.
First, I like to read a full file with its changes in view at the same time: diffs in the same editor window as the file, not a separate pane. For structural diff in the terminal I reach for difftastic but inside Neovim I want to have the ability to get the whole buffer visible while I review changes, similar to what VSCode offers by pointing and clicking in the change indicator infile.
I already use CodeDiff and lazygit which have good UX and do a lot and can be useful, but I tend to still drift back to the git CLI for day-to-day work because familiarity and because nowadays nobody edits text anymore, just skims diffs and yell at clankers.
LazyVim’s default <leader>ghd runs gitsigns diffthis, which opens native vimdiff. That view does not close cleanly with q the way I wanted, and it never felt like the right in-editor home for full-file review. This post tightens CodeDiff and gitsigns keymaps around that habit: file-level diffs in-editor, hunk previews at the cursor.
Second, hunk preview keys were too long for daily use. I wanted a bare two-key popup map like LazyVim’s ff and fg pickers. Inline preview can stay on <leader>ghp. I use that rarely.
Third, long lines broke the preview, specially problematic with markdown files. The gitsigns popup sized itself to the longest line and clipped content past the viewport. Inline preview (preview_hunk_inline) paints diff highlights on logical buffer lines. When wrap is on, one long line spans multiple visual rows, but the diff still renders on a single logical row. That is a gitsigns limitation, not something set wrap fixes.
Keymap and UI design
The custom setup for this workflow lives in ~/.config/nvim/lua/plugins/codediff.lua. LazyVim still provides the base gitsigns on_attach maps; this file extends and overrides them. The same file also registers a which-key <leader>h group for file-diff keys.
CodeDiff replaces diffview
I disabled diffview.nvim and mini.diff, re-enabled gitsigns.nvim, and freed the LazyVim snacks_picker bindings on <leader>gd / <leader>gD (Snacks.nvim git-diff pickers by default) so CodeDiff could own them:
-- ~/.config/nvim/lua/plugins/codediff.lua (abbreviated)
{ "folke/which-key.nvim", opts = { spec = { { "<leader>h", group = "hunks" } } } },
{ "sindrets/diffview.nvim", enabled = false },
{ "nvim-mini/mini.diff", enabled = false },
{ "mini.diff", enabled = false },
{
"lewis6991/gitsigns.nvim",
enabled = true,
opts = function(_, opts)
-- preview_config, on_attach with old_on_attach, buffer wrap — see excerpt below
end,
},
{
"folke/snacks.nvim",
keys = {
{ "<leader>gd", false },
{ "<leader>gD", false },
},
},
{
"esmuellert/codediff.nvim",
cmd = { "CodeDiff" },
keys = {
{ "<leader>gd", "<cmd>CodeDiff<cr>", desc = "CodeDiff Open" },
{ "<leader>gD", "<cmd>CodeDiff history<cr>", desc = "CodeDiff History" },
{ "<leader>hd", "<cmd>CodeDiff file HEAD<cr>", desc = "Diff current file (CodeDiff)" },
{ "<leader>hD", "<cmd>CodeDiff file HEAD~1<cr>", desc = "Diff current file vs HEAD~1" },
},
opts = { -- diff, explorer, history layout
},
}
Not shown above: gitsigns preview_config, buffer wrap on attach, and CodeDiff opts for layout and panels.
CodeDiff tabs close with q. Inside a CodeDiff tab, g? shows the plugin keymap help.
Hybrid keymap layout
LazyVim’s default leader is Space, so <leader>hd is the same as Space hd in the table below.
| Key | Action | Scope |
|---|---|---|
gh | Wrapped popup hunk preview | gitsigns-attached only |
Space ghp | Inline hunk preview | gitsigns-attached only |
]h / [h | Next / prev hunk with wrapped popup | gitsigns-attached only |
]H / [H | Last / first hunk with wrapped popup | gitsigns-attached only |
Space hd | CodeDiff file vs HEAD | global |
Space hD | CodeDiff file vs HEAD~1 | global |
Hunk keys register in gitsigns on_attach with { buffer = buffer }, so they exist only in gitsigns-attached buffers. File diffs use global <leader>hd / <leader>hD so they work without gitsigns attached. I moved popup from hp to gh so cursor h is never delayed; gh is the only bare prefix map in this setup. CodeDiff file still needs the target file saved on disk and inside a git repository.
-- ~/.config/nvim/lua/plugins/codediff.lua (gitsigns on_attach excerpt)
local gs = require("gitsigns")
local function apply_hunk_popup_wrap()
vim.schedule(function()
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.w[win].gitsigns_preview == "hunk" then
vim.api.nvim_win_set_width(win, math.max(40, vim.o.columns - 4))
vim.wo[win].wrap = true
vim.wo[win].linebreak = true
break
end
end
end)
end
local function preview_hunk_wrapped()
gs.preview_hunk()
apply_hunk_popup_wrap()
end
local function nav_hunk_wrapped(direction)
if vim.wo.diff and (direction == "next" or direction == "prev") then
vim.cmd.normal({ direction == "next" and "]c" or "[c", bang = true })
return
end
gs.nav_hunk(direction, { preview = true })
apply_hunk_popup_wrap()
end
vim.keymap.set("n", "gh", preview_hunk_wrapped, {
buffer = buffer,
desc = "Preview Hunk (popup)",
silent = true,
})
for _, spec in ipairs({
{ "]h", "next", "Next Hunk" },
{ "[h", "prev", "Prev Hunk" },
{ "]H", "last", "Last Hunk" },
{ "[H", "first", "First Hunk" },
}) do
vim.keymap.del("n", spec[1], { buffer = buffer })
vim.keymap.set("n", spec[1], function()
nav_hunk_wrapped(spec[2])
end, { buffer = buffer, desc = spec[3], silent = true })
end
LazyVim’s default <leader>ghp inline map stays from old_on_attach. I unmap conflicting LazyVim maps (<leader>ghP, <leader>ghd, <leader>ghD) so CodeDiff owns file diffs.
Wrapped popup
gitsigns preview_config passes options to nvim_open_win. wrap is not a valid win config key, so I set it on the window after the popup opens.
apply_hunk_popup_wrap resizes the popup to full viewport width (columns - 4), then enables wrap and linebreak. Gitsigns sets popup height from logical line count at open time; post-open wrap does not trigger a height recalc. Widening removes horizontal clipping, and wrap folds long logical lines visually. A hunk with few logical lines but extreme length can still clip vertically. I tried capping width at 120 columns first; it worked, but 120 columns still clipped wide hunks. Full width with wrap shows the entire hunk without horizontal scrolling.
gh, inline, and hunk navigation
gh opens the wrapped popup at the cursor without moving. <leader>ghp stays as inline preview for short hunks. For long wrapped lines, common in markdown, I use the popup.
My main workflow is ]h / [h (nav_hunk with { preview = true }). Each jump reopens the popup and reruns apply_hunk_popup_wrap. ]H / [H jump to the last or first hunk the same way. In diff windows, ]h / [h still delegate to ]c / [c.
I also tried auto-fallback from inline to popup when hunk lines exceeded the viewport. It added unpredictability, so I reverted it.
CodeDiff file diffs are a separate case. Side-by-side layout uses synchronized scrolling; long lines there stay on one visual row unless you switch layout. For long lines I use horizontal scroll (zh / zl) or press t to toggle inline layout inside the CodeDiff tab.
Verification
ghfor wrapped popup at cursor;]h/[has the primary hunk-review flow<leader>ghpfor inline preview when I need itghavoids theh-prefix delayhi/hpcaused- CodeDiff replaces vimdiff for file and repo diffs; tabs close with
q
Tradeoffs
I would not ship bare hi / hp again. Bare global hd / hD had the same problem on h everywhere. The h prefix delay was worse than the leader chain I was trying to avoid. gh plus wrapped ]h / [h is the layout I kept.
I have not changed CodeDiff’s default layout to inline. On narrow terminals, pressing t inside a CodeDiff tab switches to unified layout. Making that the default is still on the list.
References
Neovim
nvim_open_win. floating window config vs window options likewrap- Scroll keys .
zh/zlhorizontal scroll in nowrap diff panes
LazyVim
- Editor plugins . gitsigns defaults .
<leader>ghp,<leader>ghd, buffer-localon_attachmaps - Keymaps . snacks.nvim . original
<leader>gd/gDpicker bindings - snacks_picker extra . source of the Snacks git-diff keymaps I disabled
gitsigns
- gitsigns.nvim . hunk preview, signs, staging
- gitsigns.txt .
preview_config,preview_hunk_inline,nav_hunk,on_attach,attach_to_untracked,diffthis - PR #609 .
vim.w.gitsigns_previewwindow variable used in the wrapped popup handler
CodeDiff
- codediff.nvim . VSCode-style diff UI with side-by-side and inline layouts
- Git diff mode .
CodeDiff file HEADrequirements and behavior - Default keymaps .
q,g?,t
Prior art
- difftastic . structural terminal diff for ad-hoc file comparisons outside Neovim
- lazygit . TUI git client; I keep it but still reach for CLI + in-editor diffs
- LazyVim . base Neovim distribution
- fff.nvim . bare two-key picker pattern similar to LazyVim’s
ff/fg - Snacks.nvim . git picker keys freed for CodeDiff
- diffview.nvim . replaced with CodeDiff for file and repo diffs
This post was written with AI assistance.