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¶
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.