Skip to content

Searching

Three different jobs, three different tools: / finds the next occurrence in this buffer, :s rewrites occurrences in this buffer, and Ctrl+G finds them in every file in the project.

In this buffer

/thing        search forwards
?thing        search backwards
n             the next match, the way you were going
N             the one before, the other way
:noh          stop highlighting matches

Search wraps at the end of the file, as vi's does. Matches are highlighted while a search is live; :noh turns that off until the next one, and :set nohls turns it off for good.

A search is a jump, so `` brings you back from it.

Substituting

:s/from/to/            the first match on this line
:s/from/to/g           every match on this line
:%s/from/to/g          every match in the file
:1,5s/from/to/         lines 1 to 5
:.,$s/from/to/         from here to the end
:.+1,.+9s/from/to/     relative addresses work too
Flag
g every match on the line, not only the first
i ignore case
n count the matches and change nothing

The delimiter is whichever non-alphanumeric character follows the s, so :s#/usr/local#/opt# needs no backslashes. & in the replacement stands for what was matched. One u undoes the whole substitution, however many lines it touched.

Find in project

Ctrl+G (or :grep) opens a box that searches every file under the project root as you type.

┏ Find in Project — 12 matches ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  tab_width                                                    ┃
┃   ^A case   ^W word   ^R regex                                ┃
┃                                                               ┃
┃ src/turbovi/editorconfig.py                                   ┃
┃    52  tab_width: int = DEFAULT_TAB_WIDTH                     ┃
┃   183  tab_width=_number(properties.get("tab_width")) or si…  ┃
┃ src/turbovi/screen.py                                         ┃
┃    97  used += _screen_lines(state.line_at(row), width, sta…  ┃
┃                                                               ┃
┃  ↑↓ move · Enter opens and fills :copen · Esc cancels         ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Hits are grouped under the file they came from, numbered by line, and the match itself is underlined, windowed into view if it sits past the right edge of a long line.

Key
move through the hits
PgUp PgDn a windowful at a time
Enter open the hit under the cursor, and fill the quickfix list with the rest
Esc close, leaving the old list alone
Ctrl+A ignore case
Ctrl+W whole words only
Ctrl+R treat the pattern as a regular expression (on by default)

The three toggles are drawn in the dialog and lit when they are on. A search box whose rules you cannot see is one you stop trusting the first time it misses something you know is there.

The pattern is a Python regular expression. One that will not compile is searched for literally, because :grep foo(bar is somebody looking for a call and not writing a regex.

:grep PATTERN opens the same dialog with the box already filled.

What it does not read

Nothing under .git, .venv, node_modules, build, dist, htmlcov or the rest of the usual build output. No dotfiles, and no file over a megabyte. Nothing with a NUL byte in its first 8 KB, because strings is a different program. Searching stops at 2,000 hits.

Where it searches

The project root: the nearest directory at or above the current buffer holding a .git, .hg, .svn, pyproject.toml, package.json, Cargo.toml or go.mod. Failing all of those, the directory the editor was started in. Never the buffer's own directory, which would narrow the search every time you opened a result from it.

Ctrl+P, the file picker, uses the same root.

The quickfix list

What :grep found stays in a list after the dialog closes, and that list is the point: you can walk it while you edit.

:copen     show it in a window along the bottom   (:cw, :cope)
:cclose    close that window; the list stays      (:ccl)
:cn        the next entry                         (:cnext)
:cp        the one before                         (:cprev, :cN)
:cc        the current one again
:cfirst    the first                              (:cfir)
:clast     the last                               (:cla)

Every jump says where you are: (2 of 12) src/turbovi/screen.py.

The pane shows each hit with the line above and below it, so you can tell whether this is the match you meant without opening the file. Enter on a marked line goes there; on a file heading or a line of context it does nothing, because those are not entries.

src/turbovi/editorconfig.py
      31  DEFAULT_INDENT = "    "
 →    32  DEFAULT_TAB_WIDTH = 8
 →    52      tab_width: int = DEFAULT_TAB_WIDTH
      53      newline: str | None = None

A jump never opens a file into the pane: that is how you would lose the results you were halfway through reading.

Replacing across files

:cdo s/from/to/g

The substitution is applied to the line of every entry in the list, and the files are written. Running :%s in each of them would take whole files; the list says which lines. Any buffer you have open on a file it rewrote is re-read, so a later :w cannot put the old text back.

The usual shape is: Ctrl+G, narrow the pattern until the dialog shows exactly the lines you meant, Enter, :copen to read them once more, then :cdo.

It writes without asking

:cdo goes straight to disk across every file in the list, and there is no undo across files. u only undoes the buffer you are in. Look at :copen first, and have the work committed.