The diff cookbook
“Diffing” refers to the process of comparing one thing against another, viewing the “differences” between the two.
Because comparing text is a common occurrence when writing code, there exists an entire category of tools dedicated to helping developers compare text snippets. In this post, I go over some of the tools I rely on for this task.
Who is this post for?
- You use neovim (or considering it).
- You have yet to go down the diff rabbit hole yourself.
- You’re (at least mildly) interested in improving your diff workflow.
On the other hand, if you’ve already been down this path and have found better methods, do consider sharing them.
Table of Contents
- Neovim: diffing while editing code
- Shell: diffing the output of two shell commands
- Filesize: recursively diffing file sizes in a directory
- Misc: quality of life tips for working with diffs
- Alternatives: other related tools
Neovim: diffing while editing code
The following examples require diffview.nvim and gitsigns.nvim to be installed.
Deep diffing: comparing against one or more commits
Repo
Trace the history of the entire git repository.
vim.keymap.set(
'n',
',hh',
'<cmd>DiffviewFileHistory<cr>',
{ desc = 'Repo history' }
)
File
This can be used to step through the changes of a single file (the current file). The --follow
flag tells git
to trace a file’s history through renames.
It opens in the same view as the previous example, except that the commits listed are limited to those affecting the current file.
vim.keymap.set(
'n',
',hf',
'<cmd>DiffviewFileHistory --follow %<cr>',
{ desc = 'File history' }
)
Visual selection
Trace the history of the current visual selection. Use this command if you’re only interested in tracing changes affecting a specific section of a file.
vim.keymap.set(
'v',
',hl',
"<Esc><Cmd>'<,'>DiffviewFileHistory --follow<CR>",
{ desc = 'Range history' }
)
Line
Finally, the most granular form is tracing the history of a single line.
vim.keymap.set(
'n',
',hl',
'<Cmd>.DiffviewFileHistory --follow<CR>',
{ desc = 'Line history' }
)
Shallow diffing: comparing against HEAD
Repo
Diff the working directory against HEAD (and staging area). This will open a new tab.
vim.keymap.set('n', ',d', '<cmd>DiffviewOpen<cr>', { desc = 'Repo diff' })
We can slightly alter the previous command to serve another common use case: diffing the working directory against the master branch. This is useful before merging or when first continuing work on an existing feature branch.
local function get_default_branch_name()
local res = vim
.system({ 'git', 'rev-parse', '--verify', 'main' }, { capture_output = true })
:wait()
return res.code == 0 and 'main' or 'master'
end
-- Diff against local master branch
vim.keymap.set(
'n',
',hm',
function() vim.cmd('DiffviewOpen ' .. get_default_branch_name()) end,
{ desc = 'Diff against master' }
)
-- Diff against remote master branch
vim.keymap.set(
'n',
',hM',
function() vim.cmd('DiffviewOpen HEAD..origin/' .. get_default_branch_name()) end,
{ desc = 'Diff against origin/master' }
)
File
The following commands use highlighting and virtual text to visualize changes without leaving the current buffer.
-- Highlight changed words.
vim.keymap.set(
'n',
',vw',
require('gitsigns').toggle_word_diff,
{ desc = 'Toggle word diff' }
)
-- Highlight added lines.
vim.keymap.set(
'n',
',vL',
require('gitsigns').toggle_linehl,
{ desc = 'Toggle line highlight' }
)
-- Highlight removed lines.
vim.keymap.set(
'n',
',vv',
require('gitsigns').toggle_deleted,
{ desc = 'Toggle deleted (all)' }
)
Hunk
Diff the current hunk in a floating window (against the staging area).
vim.keymap.set(
'n',
',vh',
require('gitsigns').preview_hunk,
{ desc = 'Preview hunk' }
)
Clipboard
Clipboard vs. current file
Diff the current buffer against the clipboard contents.
-- Create a new scratch buffer
vim.api.nvim_create_user_command(
'Ns',
function()
vim.cmd([[
execute 'vsplit | enew'
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
]])
end,
{ nargs = 0 }
)
-- Compare clipboard to current buffer
vim.api.nvim_create_user_command('CompareClipboard', function()
local ftype = vim.api.nvim_eval('&filetype') -- original filetype
vim.cmd([[
tabnew %
Ns
normal! P
windo diffthis
]])
vim.cmd('set filetype=' .. ftype)
end, { nargs = 0 })
-- Assign it to a keymap
vim.keymap.set(
'n',
',vc',
'<cmd>CompareClipboard<cr>',
{ desc = 'Compare Clipboard', silent = true }
)
Clipboard vs. visual selection
Diff the current visual selection against the clipboard contents.
-- Create a new scratch buffer (see previous example)
-- ...
-- Compare clipboard to visual selection
vim.api.nvim_create_user_command(
'CompareClipboardSelection',
function()
vim.cmd([[
" yank visual selection to z register
normal! gv"zy
" open new tab, set options to prevent save prompt when closing
execute 'tabnew | setlocal buftype=nofile bufhidden=hide noswapfile'
" paste z register into new buffer
normal! V"zp
Ns
normal! Vp
windo diffthis
]])
end,
{
nargs = 0,
range = true,
}
)
-- Assign it to a keymap
vim.keymap.set(
'v',
',vc',
'<esc><cmd>CompareClipboardSelection<cr>',
{ desc = 'Compare Clipboard Selection' }
)
Shell: diffing the output of two shell commands
You can use process substitution to diff the output of any two shell commands. The following works for bash
and zsh
.
nvim -d <(ls -l) <(ls -la)
# you can also use another difftool
delta <(ls -l) <(ls -la)
Filesize: recursively diffing file sizes in a directory
This bash function uses dust
to diff the sizes of two directories. I don’t use this much, but it comes in handy when I’m experimenting with different bundler options (such as in a tsconfig.json
or vite.config.js
) and want to know more about how the changes I’m making affect the size of the generated output.
# diff directory size
# Usage: diff-dirs DIR1 DIR2 [dust options]
# Example: diff-dirs dir1 dir2 --depth=1 --no-progress --apparent-size
diff-dirs() {
local dir1="$1"
local dir2="$2"
if [[ ! -d "$dir1" ]] || [[ ! -d "$dir2" ]]; then
echo "Error: both directories must exist"
return 1
fi
shift 2
delta --side-by-side <(dust --screen-reader --no-colors --full-paths --only-file --reverse "$@" "$dir1" | sed 's/\r//g' | sed 's|^[^/]*/||') <(dust --screen-reader --no-colors --full-paths --only-file --reverse "$@" "$dir2" | sed 's/\r//g' | sed 's|^[^/]*/||')
}
Depending on the output, it can be tricky to accurately detect and properly highlight moved vs changed lines. Delta has some options that you can tweak such as --word-diff-regex
.
Misc: quality of life tips for working with diffs
Syntax highlighting for code snippets
- my original line
+ my updated line
my unchanged line
You can enable this form of syntax highlighting for a markdown code block by specifying diff
as the language. Then, prefix any line with either a -
or a +
to highlight it.
This works automatically on github.com. In neovim, you might need to explicitly add diff
to the list of languages in your treesitter config (in the ensure_installed
table).
Delta
Delta makes diffs easier to read.
delta
on the command line
Using # delta can be used to diff any two files
delta file1 file2
# or directories.
delta dir1 dir2
Using delta as the default git pager
Delta can be set as git
’s default pager. See delta’s documentation for how to do so in your .gitconfig
. Additionally, below are some extra delta options I’ve added to my .gitconfig
:
# .gitconfig
[core]
pager = delta
[delta]
features = decorations
light = false
tabs = 2
line-numbers = true
navigate = true
hyperlinks = true
# side-by-side = true
Hint: For even greater readability, set
gruvmax-fang
as your custom theme.
You can find out more about these options by running delta --help
or visiting delta’s docs.
Delta can also be integrated into other tools such as lazygit.
Alternatives: other related tools
meld
If you’d rather work with a mouse, or simply prefer a GUI, meld
can compare files or entire directories recursively.
comm
comm
can print common or unique lines between two files. A helpful cheat sheet can be found here.