There's a point in most editing sessions where a simple substitution or a quick dd stops being enough. You're mid-refactor, your search history is getting polluted, your macro keeps running past where it should, or you just want to get back to the line you were editing five minutes ago. These are the commands I keep reaching for in those moments.
1) Matching only part of a pattern with \zs and \ze
Why it matters
The \zs and \ze atoms let you pinpoint which part of a match gets highlighted or replaced — without writing lookaheads or making the regex more complex. The surrounding context still needs to match, but only the portion between \zs and \ze counts as the actual match.
" Match the function name only, not the 'function ' keyword before it
/\(function\s\+\)\zs\w\+
" Substitute: rename only the value after 'color: '
:%s/color: \zsred/blue/g
Real scenario
You're grepping through a codebase for function definitions and want to rename all functions that follow the pattern process_* — but only the name part, not the keyword. Without \zs, you'd need a capture group and a \1 backreference. With it, you write:
:%s/\(function\s\+\)\zs\w\+/handle_data/g
Only the function name gets replaced. function stays intact.
Caveat
\zs and \ze are Vim-specific — they don't exist in POSIX regex or most other languages' regex engines. If you export a pattern for use in a script or shell command, you'll need to convert it to a capture group approach.
2) Substituting without losing your current search with :keeppattern
Why it matters
Every time you run :%s/foo/bar/g, Vim quietly replaces your active search pattern with foo. That kills your highlighted matches and sends n/N chasing the substitution target instead of what you were originally searching for. :keeppattern is a modifier that prevents this side effect entirely.
:keeppattern %s/old/new/g
Real scenario
You're stepping through a file looking at all TODO comments with /TODO and n. Mid-review, you realize a variable name needs a quick rename. Running a bare :%s/myVar/myVariable/g nukes your search context. Prepend :keeppattern and n still jumps to the next TODO after the substitute finishes:
:keeppattern %s/myVar/myVariable/g
" n still navigates to the next TODO — untouched
Caveat
:keeppattern works on any Ex command that normally updates @/, not just :s. But it only preserves the search register — if your substitution also changes the cursor position, that still happens. Worth knowing if you have mappings that depend on cursor state after a substitute.
3) Making a macro stop itself automatically with a recursive call
Why it matters
If you've ever guessed 99@q hoping a macro stops at the right line, this is the alternative. A recursive macro calls itself at the end of its recording. When any motion inside it fails — usually j hitting the last line — Vim aborts the macro chain. No guessing required.
" Step 1: clear register q so @q does nothing during recording
qqq
" Step 2: record the macro with a self-call at the end
qqA;<Esc>j@qq
" Step 3: run it
@q
Real scenario
You have a 300-line file of JavaScript variable declarations and need a semicolon appended to every line. You don't want to count the lines and you don't want to use :%normal A; (maybe the formatting logic is more complex than that, and you want the macro). Record once, run once with @q, and it processes every line and stops cleanly at the end of file.
Caveat
The qqq clear step is non-negotiable. If register q already contains something, @q during recording will execute that old macro mid-recording, and the result is unpredictable. Also make sure wrapscan is off (:set nowrapscan) if your macro includes a search — otherwise the macro wraps back to the top and runs forever.
4) Inserting computed values inline with the expression register
Why it matters
<C-r>= opens an expression prompt while in Insert mode. Type any Vimscript expression, press <CR>, and the result gets inserted at the cursor. It's a small feature with a surprising surface area: inline math, timestamps, current line numbers, shell output — all available without leaving Insert mode.
" Insert today's date
<C-r>=strftime('%Y-%m-%d')<CR>
" Insert the current line number
<C-r>=line('.')<CR>
" Quick math
<C-r>=1024 * 4<CR>
Real scenario
You're writing a config file and need to insert a timestamp for a created_at field. You could :r !date, but that inserts on a new line. With <C-r>=, you stay in Insert mode and drop the result exactly where the cursor is:
created_at: <C-r>=strftime('%Y-%m-%dT%H:%M:%S')<CR>
One keystroke sequence, no mode switching, no cleanup.
Caveat
system() works inside expressions, but it inserts a trailing newline. Use system('date')[:-2] or trim(system('date')) to strip it. Also, <C-r>= evaluates one expression — you can't run multi-statement Vimscript here. For that you'd use :call or a function.
5) Jumping back through your own edit history with g; and g,
Why it matters
Vim maintains a changelist for every buffer — a record of every position where text was modified. g; jumps backward to the previous change location; g, jumps forward. This is distinct from the jump list (<C-o>/<C-i>), which tracks cursor movement. The changelist tracks only actual edits.
g; " go to older change
g, " go to newer change
:changes " see the full list
Real scenario
You edited a function at line 50, scrolled to line 300 to look at something, then jumped around reading context. Now you want to get back to where you were working. <C-o> takes you through every jump including all the reading you just did. g; takes you directly to line 50 — your last edit — in one or two keystrokes:
g; " → line 50 (where you last edited)
g; " → the edit before that, if any
g, " → back to line 50
Caveat
The changelist is per-buffer and lives only as long as the buffer is open — unless you have :set undofile configured, in which case it's rebuilt from persistent undo history on next open. If you close a buffer and reopen without undofile, the changelist resets.
Wrap-up
These five commands don't get much screen time in tutorials, but they show up constantly in real editing sessions. \zs/\ze handle targeted substitutions without complex capture groups. :keeppattern keeps your search context intact while you do unrelated replacements. Recursive macros let you automate repetitive edits across a whole file without counting lines. The expression register turns Insert mode into a small calculator and templating engine. And g;/g, give you a direct path back to where you were actually working.
If you want more practical Vim tricks like these, I publish them at https://vimtricks.wiki.
What Vim action still feels slower than it should in your workflow?
Top comments (0)