DEV Community

Vimtricks.wiki
Vimtricks.wiki

Posted on Originally published at vimtricks.wiki

Vim commands that handle the detail work without friction

Most of the time Vim does what you expect. But there is a category of editing tasks where the obvious approach quietly fails you: numeric data that sorts wrong, a diff window you can only scroll through, a macro that breaks because of your own mappings, or a regex that swallows half the file instead of a small piece of it. None of these are exotic edge cases — they come up in ordinary work.

These five commands are the ones I reach for when that category of problem shows up.


1) Jumping between diff hunks without scrolling

Why it matters

When you open two files side by side with vimdiff or :diffsplit, the changed sections are highlighted — but you still have to find them. Scrolling through a large file looking for the next colored block is slow and easy to miss. ]c and [c are motions that jump directly to diff hunks, skipping unchanged content entirely.

]c   " jump forward to the next changed hunk
[c   " jump backward to the previous changed hunk
Enter fullscreen mode Exit fullscreen mode

Real scenario

You are reviewing a pull request locally with vimdiff old.go new.go. The files are 300 lines each and there are seven changes scattered through them. Pressing ]c from the top takes you directly to each hunk in sequence. Pressing [c steps back if you want to re-examine one. No scrolling.

Caveat

These motions require an active diff session — they do nothing in a regular buffer. If you land on a hunk and want to act on it, do (diff obtain) pulls the change from the other window, and dp (diff put) pushes it the other direction. Run :diffupdate if the highlighting goes stale after you make edits.


2) Sorting lines numerically instead of lexicographically

Why it matters

Plain :sort in Vim is alphabetical. That means 10 sorts before 2, 100 sorts before 9, and a list of version numbers or file sizes ends up in nonsensical order. Adding the n flag tells Vim to compare lines by numeric value instead of character code.

:'<,'>sort n    " sort selected lines numerically, ascending
:'<,'>sort n!   " sort numerically, descending
:%sort n        " sort the whole buffer numerically
Enter fullscreen mode Exit fullscreen mode

Real scenario

You have a list of benchmark results:

150ms  parse_config
23ms   load_cache
1200ms run_query
87ms   render_view
Enter fullscreen mode Exit fullscreen mode

Selecting all four lines and running :'<,'>sort n orders them by the leading number: 23ms, 87ms, 150ms, 1200ms. With plain sort, you get 1200ms second because "1" < "8" lexicographically.

Caveat

The n flag extracts the first integer found on each line as the sort key. If a line has no leading number, Vim treats it as zero. Non-numeric lines that happen to mix in will group at the top. For floating-point values or sorting by a middle column, pipe through an external sort instead: :'<,'>!sort -k1 -n.


3) Running normal mode commands without your mappings interfering

Why it matters

:normal {cmd} is a powerful way to replay Normal mode keystrokes over a range of lines — common in macros, autocommands, and shared vimrc snippets. The problem is that it applies your custom mappings. If you have nnoremap w b in your config, then :%normal w moves backward instead of forward. Adding a bang fixes it: :normal! {cmd} always executes the built-in command, regardless of what you have mapped.

:%norm! A;       " append semicolon to every line using the real A, not any remapped A
:'<,'>norm! >>   " indent selected lines using the real >>, not any remapping
:5,10norm! dw    " delete first word on lines 510 using the real dw
Enter fullscreen mode Exit fullscreen mode

Real scenario

You are writing a function in a shared vimrc that bulk-indents a range of lines. Without the bang, the function breaks on any machine where > has been remapped. With :norm!, the function works identically everywhere, regardless of the user's mapping configuration.

Caveat

:norm! does not accept special key notation directly in command-line usage. If your command needs <CR>, <Esc>, or <C-a>, you have to wrap it in :execute "norm! \<CR>" (using execute to interpolate the special key). This applies to any key that requires angle-bracket notation.


4) Reverting to the last saved state in one step

Why it matters

Vim's undo history is deep and time-stamped. The :earlier command navigates it using time units instead of individual undo steps — and f is the unit for file writes. So :earlier 1f jumps the buffer back to exactly the state it was in when you last ran :w. It does not matter if you made 50 changes since then; one command reverts all of them.

:earlier 1f     " revert to state at last file write
:earlier 2f     " revert to two saves ago
:later 1f       " re-apply forward to next write checkpoint
Enter fullscreen mode Exit fullscreen mode

Real scenario

You refactored a function, saved, continued editing, and an hour later realized the refactor introduced a subtle bug. You want to see the version just before that last batch of changes without destroying the ability to redo. :earlier 1f takes you back to the previous save in one command. If you change your mind, :later 1f restores the current state.

Caveat

This uses Vim's in-memory undo tree — it does not reload the file from disk. If you want a true disk reload, use :e! instead. Also, undo history is lost when you close Vim unless you have :set undofile enabled. With undofile, the undo tree persists across sessions, which makes :earlier 1f useful even after restarting Vim.


5) Matching the shortest possible string in a regex

Why it matters

By default, .* in a Vim pattern is greedy — it matches as many characters as possible. When your text has repeated delimiters, that means one .* can consume far more than you intended, from the first delimiter all the way to the last one in the file. Vim's non-greedy quantifier is \{-}, and it works as the lazy counterpart to *, matching as few characters as possible.

/start.\{-}end        " matches shortest span from 'start' to 'end'
:%s/<b>.\{-}<\/b>//g  " removes each <b>...</b> tag pair individually
Enter fullscreen mode Exit fullscreen mode

Real scenario

You have a log file with repeated bracketed fields:

[INFO] Request received [user=alice] [status=200]
[WARN] Timeout reached [user=bob] [status=408]
Enter fullscreen mode Exit fullscreen mode

Running :%s/\[.\{-}\]//g removes every bracket pair individually. Using greedy .* instead would eat from the first [ all the way to the last ] on the line, deleting content you wanted to keep.

Caveat

\{-} is Vim's regex syntax, not PCRE. If you are accustomed to .*? from Python or JavaScript, this is the direct equivalent but the syntax is different. In Vim's very-magic mode (\v), you can write {-} without the backslash. For zero-or-more non-greedy, use .\{-}. For one-or-more, use .\{-1,}.


Wrap-up

None of these are obscure. Diff navigation with ]c/[c replaces scrolling. Numeric sort with sort n prevents the kind of ordering bug that looks like a data problem until you realize it is not. :norm! is essential the moment you start writing any vimrc automation. :earlier 1f is a safety net for the editing session. And non-greedy \{-} is one of those regex tools you reach for constantly once you know it exists.

If you want more practical Vim tricks like these, I publish them regularly at https://vimtricks.wiki.

Which of these do you already use, and which ones sneak up on you?

Top comments (0)