Most of the time when editing gets tedious, it's not one hard change — it's fifty copies of the same small change, or a block of generated content that shouldn't be there, or inconsistent casing across identifiers. Vim has dedicated commands for each of these situations, but they're easy to miss if you learned the basics and stopped there.
Here are five commands I reach for when text cleanup turns mechanical.
1) Pass the whole buffer through an external tool
Why it matters
Vim's ! operator sends text through a shell command and replaces it with the output. :%! does this for the entire buffer. You don't need a plugin or a formatter wired into your editor — if the tool runs from a shell, it works here.
:%!jq .
Real scenario
You're looking at a one-line JSON response pasted from a terminal. Instead of copying it to an online formatter, run :%!jq . and the buffer is instantly readable. Same idea for :%!sort -u to deduplicate a list, or :%!column -t to align a table of values.
For a subset of lines, use a range: :5,20!indent formats only those lines. Visual selection followed by ! auto-fills :'<,'> for you.
Caveat
If the command exits nonzero or writes nothing to stdout, Vim restores the original buffer — so it's safe to experiment. But if the tool crashes midway and writes partial output, you'll need u to undo. Check the result before saving.
2) Transform case directly inside a substitute
Why it matters
Vim's replacement string supports case-transform atoms: \U uppercases everything that follows, \L lowercases it, \u and \l affect only the next character. Combined with capture groups, this handles identifier normalization that would otherwise need a separate script.
:%s/\v<(\w+)>/\u\L\1/g
Real scenario
You have a config file where environment variable names are inconsistently cased — some SCREAMING_SNAKE, some screaming_snake, some mixed. A single :%s/\v(\w+)/\U\1/g uppercases all of them in one pass. Or you're converting a list of snake_case keys to Title Case for a document: :%s/_\(\w\)/\u\1/g capitalizes after every underscore.
Caveat
\U and \L affect the rest of the replacement string, not just the next character. If you write \U\1-\2, both capture groups get uppercased. Use \E to end the transform explicitly: \U\1\E-\2 uppercases only \1 and leaves \2 unchanged.
3) Navigate through matches while still typing the search
Why it matters
With incsearch active (the default in Vim 8+ and Neovim), <C-g> and <C-t> jump forward and backward through matches while you're still in the search prompt. You can see exactly which occurrence you'll land on before pressing Enter — no committing to a search and then recounting with n.
/config<C-g><C-g>
Real scenario
A file has thirty references to user and you want the third occurrence in a specific function. Type /user, press <C-g> twice to skip past the first two matches, then confirm with <CR>. It beats a blind search followed by n presses, especially when matches are spread across hundreds of lines.
Caveat
This only works with :set incsearch, which is on by default in modern Vim/Neovim but may be absent in minimal or legacy configs. Also note that <C-g> in normal mode shows file info — this shortcut is search-prompt-only, so don't confuse the two.
4) Delete structured blocks between two pattern markers
Why it matters
:g (global) accepts a range, not just individual line targets. :g/start/,/end/d matches every line containing start, extends the range to the next line containing end, and deletes the whole block — both markers and everything between them.
:g/<!-- BEGIN GENERATED -->/,/<!-- END GENERATED -->/d
Real scenario
A template generates HTML with clearly marked scaffolding sections. Before committing, you want those blocks stripped from the output. One command removes every matching block in the file, no matter how many there are. The same pattern cleans up log files: :g/DEBUG START/,/DEBUG END/d removes every debug dump in a crash log you're trying to read.
Before previewing what would be removed, run:
:g/<!-- BEGIN GENERATED -->/,/<!-- END GENERATED -->/p
Caveat
:g processes matches top to bottom and adjusts line numbers as it goes, so back-to-back blocks are handled correctly. However, if an end marker appears before its corresponding start marker (malformed blocks), the range will expand into unintended territory. Preview with :p before running d.
5) Run a command at every quickfix entry
Why it matters
:cdo {cmd} executes a command at each entry in the quickfix list. Unlike :cfdo (once per file), :cdo visits each individual match location — useful when you want per-occurrence precision rather than a blanket file-wide substitution.
:vimgrep /deprecatedCall/ **/*.go
:cdo s/deprecatedCall/replacementCall/ | update
Real scenario
You're retiring a function. Grep populates the quickfix list with every call site across the project. :cdo substitutes at each location and saves the file. Two commands, no manual file-opening. You can also use :cdo norm @q to replay a recorded macro at each entry — handy when the change is structural and context-dependent enough that a regex won't cut it.
Caveat
:cdo runs at every quickfix entry, including duplicate lines if the same line appears in multiple results. Use :cfdo instead if you'd rather operate once per file. Also, :cdo doesn't save files by default — always append | update unless you want to review diffs before writing.
Wrap-up
These five commands cover a lot of ground: piping through external formatters, case transforms inside substitutions, navigating matches interactively, deleting structured blocks, and project-wide refactoring from the quickfix list. None require plugins and all work identically in Neovim.
If you want more practical Vim tricks like these, I publish them at https://vimtricks.wiki.
What bulk editing task in Vim still feels slower than it should?
Top comments (0)