Lesson 06 workflow

Search and replace

:s for a range of lines, :g for every line that matches, :find and :replace for the whole project — with a preview you prune before anything is written.

/ finds something in the file and s jumps to something on screen. This lesson is about changing text in bulk: in a few lines, on every matching line, and across every file in the project.

note
Search, :s and :g take literal patterns for now — \d is a backslash and a d. Regular expressions arrive under all of them at once. :find~ and :replace~ already read a regex.

:s — substitute

:[range]s/{pattern}/{replacement}/[flags]
lstring.c :s over a range 1 / 5
:s over a range: step 0 :s over a range: step 1 :s over a range: step 2 :s over a range: step 3 :s over a range: step 4

No range means the cursor’s line, which is why % is the most typed character in the command. The whole command is one undo step, the cursor lands on the last line changed, and the pattern becomes the last search, so n walks what you just replaced. Nothing matched is an error, not a silent no-op.

CommandDoes
:s/a/b/the first a on the cursor’s line
:s/a/b/gevery a on it
:%s/a/b/gevery a in the file
:2,5s/a/b/gevery a in lines 2 to 5
:'v s/a/b/ginside the selection — a rectangle’s columns included
:s#/usr#/opt#any delimiter, so a path needs no escaping
:%s//new/gan empty pattern is the last search
FlagDoes
gevery match on a line, not just the first
imatch without case
Imatch with case
ncount the matches, change nothing

Without i or I it is smartcase, exactly like /: all lowercase ignores case, one capital makes it exact. Inside the pattern and the replacement, \ before the delimiter is the delimiter and \\ is a backslash — nothing else is an escape. The closing delimiter is optional: :%s/old/new works.

Doing it again

KeyDoes
&the last substitute again, on the cursor’s line — flags included
g&the same over the whole file
:[range]& :[range]&&over a range; both spellings mean the same
vs vim
Vim’s :& drops the flags and :&& keeps them. In bi both keep them, so there is one thing to remember. And g& does not swap in the last search pattern — it repeats exactly what ran.

Ranges

Every command that takes lines — :s, :g, :d, :m, :sort, :normal — reads the same small address language.

AddressMeans
12line 12
.the cursor’s line
$the last line
%the whole file
'< '>first and last row of the selection
'vthe selection itself, whatever its shape
.+3 $-1 +2any of the above with an offset

Two addresses make a range: :10,20s/a/b/g, :.,$d, :.,.+5normal A;. Pressing : in visual mode prefills 'v. A line that does not exist is refused (no line 99), not clamped.

:g — a command on every matching line

lstring.c :g and :normal 1 / 5
:g and :normal: step 0 :g and :normal: step 1 :g and :normal: step 2 :g and :normal: step 3 :g and :normal: step 4

:g/pattern/cmd scans first — every line containing the pattern is marked — then runs the command on each, top to bottom. Nothing chases its own output, and the whole run is one undo step. :v (or :g!) takes the lines that do not match. With no range, :g scans the whole file.

CommandDoes
:g/TODO/ddelete every line with a TODO
:v/save/dkeep only the lines mentioning save
:g/TODO/s/old/new/gsubstitute, only on TODO lines
:g/foo/s//bar/gthe inner s borrows :g’s pattern
:g/^use /m 0herd matching lines to the top
:g/fixme/normal A !append ! to each
:%normal I//prefix every line — no pattern needed
:2,5ddelete lines 2 to 5

The command after :g is one of d, s, &, m, case, retab or normal — anything else is refused by name. That whitelist is what keeps :g/x/q from closing the editor on the first match.

:normal {keys} replays keys as if typed, once per line under a range, and presses Esc for you at the end. It uses bi’s own keymap, not your remaps (vim’s :normal!), and takes raw characters — there is no <Esc> notation inside it yet.

tip
To comment lines, reach for gc instead of :normal I// — it knows each language’s comment syntax.

:find — search the project

lstring.c :find across the project 1 / 6
:find across the project: step 0 :find across the project: step 1 :find across the project: step 2 :find across the project: step 3 :find across the project: step 4 :find across the project: step 5

:find walks every file under the project root — skipping what .gitignore ignores and binary files — and puts the matches in a results pane: one heading per file, one row per matching line.

:find needle           literal, smart case
:find src/ needle      only under src/
:find~ \bfn \w+        the pattern as a regular expression
:results               bring the last results pane back

The whole argument is the pattern, so :find fn main() searches for exactly that. A first word ending in / that names a real directory is a scope.

KeyIn a results pane
j k gg Gmove between rows
Ctrl-D Ctrl-Uten rows at a time
Enter oopen the file at the match’s column — a heading opens its top
Ctrl-Vopen the match in a vertical split beside the results
xdrop a row — a hit, or a file with everything under it
q Escput back the file the pane displaced
Ctrl-^after Enter, swap the pane back

The pane is not an overlay: it stays open beside what you are editing, survives Enter, and :results brings back the last list with its prunes intact.

:replace — preview, prune, apply

lstring.c :replace with a preview 1 / 5
:replace with a preview: step 0 :replace with a preview: step 1 :replace with a preview: step 2 :replace with a preview: step 3 :replace with a preview: step 4
:replace /old/new/     find old, preview replacing it with new
:replace //new/        preview rewriting what the focused pane shows
:replace~ /f(\w+)/$1/  regex, with $1 for the first group

:replace never rewrites on Enter. It arms the results pane: the title reads replace: old → new and every row shows its line as it will read. Then:

KeyIn an armed pane
aapply the selected row — on a heading, the whole file
Aapply every row still pending
xdrop a row from the offer
Enteropen the file to look first; Ctrl-^ comes back, decisions intact
qwalk away — nothing you did not press a for was applied

Applied rewrites go into buffers, never straight to disk. Each file is opened, edited as one undo step and left modified: :wa writes the lot, u in any one file takes that file back. A line that changed since the search is skipped and counted rather than rewritten.

A row is a line, so a line with the pattern twice is rewritten twice.

tip
gr lists every reference to a symbol in the same kind of pane. gr, then :replace //new_name/, is a project-wide rename you can review row by row.