Skip to content
Fran Gonzalez
← Back to blog
(updated Jul 16, 2026)·Clanker·8 min read

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.

Some matmuls wrote this slop, sorry. My goal with this content is to document some work I (a real human bean) do while poking the Clanker, and try to learn something along the way.

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.

KeyActionScope
ghWrapped popup hunk previewgitsigns-attached only
Space ghpInline hunk previewgitsigns-attached only
]h / [hNext / prev hunk with wrapped popupgitsigns-attached only
]H / [HLast / first hunk with wrapped popupgitsigns-attached only
Space hdCodeDiff file vs HEADglobal
Space hDCodeDiff file vs HEAD~1global

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

  • gh for wrapped popup at cursor; ]h / [h as the primary hunk-review flow
  • <leader>ghp for inline preview when I need it
  • gh avoids the h-prefix delay hi / hp caused
  • 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 like wrap
  • Scroll keys . zh / zl horizontal scroll in nowrap diff panes

LazyVim

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_preview window variable used in the wrapped popup handler

CodeDiff

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.