Vim
Edit with operators, motions and text objects, run substitutions over ranges, record macros, manage buffers and windows, and fix paste, swap and startup problems.
On this page
Cheatsheet#
| Task | Keys or command |
|---|---|
| Quit without saving, save and quit | :q!, :wq or ZZ |
| Save as root after opening read-only | :w !sudo tee % > /dev/null |
| Undo, redo | u, C-r |
| Delete a line, a word, to end of line | dd, dw, D |
| Change inside quotes, parentheses, a word | ci", ci(, ciw |
| Yank a line, paste after, paste before | yy, p, P |
| Repeat the last change | . |
| Search, next, previous | /pattern, n, N |
| Search the word under the cursor | *, # |
| Replace in the whole file, confirm each | :%s/old/new/gc |
| Delete every line matching | :g/pattern/d |
| Jump to line, to top, to bottom | :42 or 42G, gg, G |
| Jump back, forward through the jump list | C-o, C-i |
| Open a file, list buffers, switch | :e path, :ls, :b name |
| Split, vertical split, move between | :sp, :vs, C-w h/j/k/l |
Record a macro into q, run it, run it 10 times | qq ... q, @q, 10@q |
| Indent a block | > in visual mode, >> on a line, = to re-indent |
| Run a shell command, filter lines through one | :!cmd, :%!sort |
| Reload the file from disk | :e! |
| Show what a key does | :help ciw, :verbose map <leader>f |
Behaviour below is Vim 9.1 or later and Neovim 0.11 or later; differences are noted where they matter. Reference: Vim help and Neovim docs.
Modes#
Vim is a modal editor: the same keys do different things depending on the mode, and almost every “Vim typed garbage” complaint is a key pressed in the wrong one. The status line shows the mode when showmode is on (the default), and Escape returns to Normal from anywhere.
| Mode | Enter | Purpose |
|---|---|---|
| Normal | Escape | Commands and motions; where you spend most time |
| Insert | i, a, I, A, o, O, c, s | Typing text |
| Visual | v, V, C-v | Character, line and block selection; an operator then acts on it |
| Command-line | :, /, ?, ! | Ex commands, search, filters |
| Replace | R | Overwrite characters |
| Terminal | :terminal | A shell in a buffer; C-\ C-n returns to Normal |
i inserts before the cursor, a after, I at the first non-blank, A at the end of the line, o opens a line below, O above. C-o in Insert mode runs one Normal command and returns, so C-o zz centres the screen without leaving insert. C-r " in Insert or Command-line mode pastes a register; C-r = evaluates an expression.
Operators, motions and text objects#
The grammar is [count] operator [count] motion-or-object. An operator says what to do, a motion or text object says to what, and counts multiply. Learning the pieces separately gives every combination for free.
| Operator | Action |
|---|---|
d | Delete (into a register; see below) |
c | Change: delete and enter Insert |
y | Yank (copy) |
> < | Shift indentation |
= | Re-indent using the filetype’s rules |
gu gU g~ | Lowercase, uppercase, toggle case |
gq | Reformat to textwidth |
! | Filter through a shell command |
Doubling an operator applies it to the current line: dd, cc, yy, >>, gUU. A capital is a shortcut for “to end of line”: D, C, Y (in Vim Y is yy; Neovim maps it to y$ for consistency).
| Motion | Moves |
|---|---|
h j k l | One character or line |
w b e | Word start forward, back; word end. W B E use whitespace-delimited words |
0 ^ $ | Column 0, first non-blank, end of line |
f{c} t{c} | To, till the next c on the line; F T backward; ; , repeat |
% | Matching bracket |
{ } | Paragraph back, forward |
gg G | First line, last line; 42G is line 42 |
H M L | Top, middle, bottom of the window |
C-d C-u C-f C-b | Half page, full page |
'' `` | Line, exact position before the last jump |
/pat ?pat | Search: a motion like any other, so d/foo deletes to the match |
Text objects work only after an operator or in Visual mode, and select a structure around the cursor regardless of where in it the cursor is. i means inside, a means around (including delimiters or trailing whitespace).
| Object | Selects |
|---|---|
iw aw | Word; aw includes the following space |
iW aW | Whitespace-delimited word |
is as | Sentence |
ip ap | Paragraph |
i( a( (or ib, i)) | Parentheses, also [ { < |
i" a" | Quoted string, also ' and ` |
it at | XML or HTML tag |
ci" change the text inside the quotes the cursor is in
da( delete the parenthesised group including the parentheses
yip yank the paragraph
>ap indent the paragraph and its trailing blank line
gUiw uppercase the word
d2w delete two words; 2dw does the same
c3j change this and the three lines below
dt, delete up to but not including the next comma
=i{ re-indent the block inside braces. repeats the last change including its count and the text inserted, so a ciwfoo<Esc> followed by n. fixes the next occurrence. Count-prefixed dots (3.) replace the original count.
Registers#
Every delete, change and yank writes to a register, and p reads from the unnamed register ". Prefix the operator with "x to name one.
| Register | Holds |
|---|---|
"" | Last delete, change or yank; what p uses |
"0 | Last yank only, untouched by deletes: the fix for “I yanked, deleted, and pasted the wrong thing” |
"1-"9 | Last nine deletes or changes of at least a line, shifting down |
"- | Last small (less than a line) delete |
"a-"z | Named; "A appends to a |
"+ "* | System clipboard, and X11 primary selection; need a clipboard-enabled build |
"_ | Black hole: "_dd deletes without touching any register |
"/ ": ". | Last search, last command line, last inserted text (read-only) |
"% "# | Current and alternate file names |
"= | Expression: "=strftime('%F')<CR>p |
"ayy yank the line into a
"Ayy append the next line to a
"ap paste a
"0p paste the last yank even after several deletes
"+y yank a visual selection to the system clipboard
:reg a0" show these registers
:let @a='' clear a:echo has('clipboard') returns 1 when "+ works. On Fedora, vim-enhanced lacks it; vim-X11 (gvim -v) or Neovim with wl-clipboard or xclip installed provide it. Inside tmux over SSH, Neovim can use OSC 52 with vim.g.clipboard set to the osc52 provider.
Macros#
A macro records keystrokes into a register and replays them, which makes it a register like any other: you can paste it, edit it and yank it back.
qa start recording into a
0f=lct;"$HOME/bin"<Esc>j the edit, ending with a move to the next line
q stop
@a replay once
5@a replay five times
@@ repeat the last macro
:'<,'>normal @a run on each line of a visual selection
:g/TODO/normal @a run on each line matching TODOA macro stops when a motion fails, so a recording that ends with j stops at the last line and one that uses f= stops on a line without =. That is a feature: 1000@a runs until the pattern runs out. Record with commands that are position-independent (0, ^, f, /) rather than counting characters. To edit a macro, "ap it into a scratch line, change it, then "ayy it back; <Esc> appears as ^[ and must stay as a literal escape character (type it with C-v Esc).
Search and replace#
Searches are regular expressions in Vim’s own flavour, which is nearer to BRE than to PCRE: +, ?, |, (, ) and { are literal unless escaped. \v (very magic) at the start makes them special, so \v(foo|bar)+ matches without the backslashes. \c anywhere makes one search case-insensitive; ignorecase plus smartcase makes lowercase patterns insensitive and mixed-case ones sensitive.
/\<word\> whole word
/\vfoo(bar)@! foo not followed by bar (lookahead)
/\d\{3} three digits
/^\s*$ blank lines
/foo\_.*bar across lines: \_ prefixes a class to include newline:s substitutes on a range of lines; the default is the current line. :% is the whole file, :'<,'> the visual selection (typed automatically when : is pressed in Visual mode), :.,+5 this line and five more, :10,20, :/start/,/end/ between two matches, and :.,$.
:%s/old/new/g every occurrence on every line; without g only the first per line
:%s/old/new/gc confirm each: y, n, a (all), q, l (this one then quit)
:s/\(\w\+\) \(\w\+\)/\2 \1/ swap two words with groups
:%s/\v(\w+)@(\w+)/\2 at \1/ the same with very magic
:%s/foo/\U&/g & is the match; \U uppercases to the end or \E
:%s/\s\+$//e strip trailing whitespace; e suppresses "pattern not found"
:%s#/usr/local#/opt#g any delimiter works when the pattern contains slashes
:%s/x/\r/g \r inserts a newline in the replacement; \n would insert a NUL
:%s/\n\n\+/\r\r/ collapse runs of blank lines to one
:%s//new/g empty pattern reuses the last search
:%s/\<\(\w\)\(\w*\)\>/\u\1\L\2/g Title Case each word
:%s/pat/\=line('.')/ \= evaluates an expression as the replacement
& or :&& repeat the last substitute on this line, with the same flags
g& repeat it on every line:g/pattern/command runs an Ex command on every line matching; :v or :g! on every line not matching. The command defaults to p (print), which is where the name of grep comes from.
:g/^\s*#/d delete comment lines
:v/error/d keep only lines containing error
:g/^$/,/./-j join each run of blank lines into one
:g/func/normal A; append ; to every line containing func
:g/pat/m0 reverse the order of matching lines (move each to the top)
:g/pat/t$ copy matching lines to the end
:g/^Host /+1s/^/ / indent the line after each Host line
:g/pat/s/a/b/ substitute only on matching linesBuffers, windows and tabs#
A buffer is a file loaded in memory. A window is a viewport onto a buffer. A tab page is a collection of windows. Closing a window does not unload its buffer, and the same buffer can show in several windows, which is why “tabs as files” from other editors maps badly: use buffers for files and windows for views.
:e path edit a file (open a buffer); :e! discards changes and reloads
:ls list buffers; % is current, # alternate, + modified, h hidden
:b 3 :b name switch by number or partial name (Tab completes)
:bn :bp C-^ next, previous, toggle with the alternate buffer
:bd delete (unload) the buffer; :bd! discards changes
:bufdo %s/a/b/ge | update run a command in every buffer and save the changed ones
:sp path :vs path split horizontally, vertically
C-w s C-w v split the current buffer
C-w h j k l move between windows; C-w w cycles
C-w H J K L move the window to the far left, bottom, top, right
C-w o close every other window
C-w = equalise sizes; C-w _ maximise height; C-w | width; 10 C-w + grow by ten
C-w q :q close the window; the buffer stays loaded
:tabnew path new tab; gt gT move between; :tabclose
:windo diffthis run a command in every window of the tab
:find name search the path option; set path+=** for recursive lookup in the project
:args **/*.go set the argument list; :argdo runs a command over ithidden (on by default in Neovim, off in Vim) lets a modified buffer leave the window without being saved; without it :e other on a modified buffer fails with E37, and :e! other discards the changes.
Marks and jumps#
m{a-z} sets a mark in the buffer, m{A-Z} a global one that also records the file. 'a jumps to the line, `a to the exact position, and both are motions, so d'a deletes from here to the mark’s line and ya` yanks to its position.
| Mark | Meaning |
|---|---|
`. | Position of the last change; gi inserts there |
`^ | Where Insert mode was last exited |
`" | Where the cursor was when the buffer was last exited |
`[ `] | Start and end of the last changed or yanked text |
`< `> | Start and end of the last visual selection; gv reselects it |
'' | Position before the last jump |
Jumps (G, gg, %, /, n, '', :42 and anything that moves more than a line) go on the jump list; C-o goes back and C-i (Tab) forward, across files. g; and g, walk the change list instead. :marks, :jumps and :changes show them.
A minimal sane vimrc#
Vim loads ~/.vimrc (or ~/.vim/vimrc), and when neither exists it loads defaults.vim, which turns on syntax highlighting, filetype detection, incsearch, a five-line scrolloff and a short ttimeoutlen. Creating an empty vimrc switches all of that off, so a vimrc should either start with source $VIMRUNTIME/defaults.vim or set the essentials itself. This one sets them itself so it reads the same on any version.
set nocompatible " Vim, not vi; implied when a vimrc exists but harmless
filetype plugin indent on
syntax enable
set encoding=utf-8
set hidden " switch buffers without saving
set backspace=indent,eol,start " backspace over everything in Insert
set incsearch hlsearch ignorecase smartcase
set scrolloff=5 sidescrolloff=5
set number relativenumber " absolute on the cursor line, relative elsewhere: counts for j/k
set wildmenu wildmode=longest:full,full
set laststatus=2 ruler showcmd
set ttimeout ttimeoutlen=50 " Escape is recognised quickly, key codes still work
set nrformats-=octal " C-a on 007 gives 008, not 010
set autoread " reload a file changed outside Vim when it is unmodified
set undofile undodir=~/.vim/undo// " persistent undo; create the directory
set noswapfile " or set directory=~/.vim/swap// to keep them out of the tree
set splitbelow splitright
set list listchars=tab:▸\ ,trail:·,nbsp:␣
set expandtab shiftwidth=4 softtabstop=4 " ftplugins override per filetype
set formatoptions+=j " remove comment leaders when joining lines
set mouse=a
set clipboard=unnamedplus " y and p use the system clipboard when available
let mapleader = ' '
nnoremap <leader>w :update<CR>
nnoremap <silent> <leader>h :nohlsearch<CR>
nnoremap Q gqnoremap variants never expand other mappings, and are what a vimrc should use unless a mapping deliberately builds on another. :verbose set shiftwidth? shows where an option was last set, which is how to find the plugin or ftplugin overriding a vimrc value. :scriptnames lists every file sourced, in order.
Neovim differences#
Neovim keeps Vim’s editing model and most of its Vimscript, and changes defaults, configuration and extension points. Config lives at ~/.config/nvim/init.lua (or init.vim), data under ~/.local/share/nvim, and ~/.local/state/nvim/shada/main.shada replaces .viminfo. Everything the sane vimrc above sets is already the default except number, list, undofile, the indent settings, splitbelow/splitright, clipboard and the mappings: hidden, autoread, incsearch, hlsearch, wildmenu, laststatus=2, ttimeoutlen=50, backspace, nrformats-=octal, mouse=nvi, filetype and syntax are on, and Y yanks to end of line. Bracketed paste is handled automatically, so pastetoggle and :set paste are unnecessary.
Removed: Vim9 script (Neovim runs legacy Vimscript and Lua), cscope, :hardcopy and the GUI-specific commands. Added: a built-in LSP client (vim.lsp), Tree-sitter parsing for highlighting and text objects, a Lua API (vim.api, vim.o, vim.keymap.set), :terminal with a job control API, and default mappings such as gcc to comment a line, K for LSP hover and grn, gra, grr for rename, code action and references (0.10+, [d ]d for diagnostics).
-- ~/.config/nvim/init.lua: the same settings as the vimrc above
vim.o.number = true
vim.o.relativenumber = true
vim.o.undofile = true
vim.o.expandtab = true
vim.o.shiftwidth = 4
vim.o.softtabstop = 4
vim.o.splitbelow = true
vim.o.splitright = true
vim.o.list = true
vim.o.listchars = 'tab:▸ ,trail:·,nbsp:␣'
vim.o.clipboard = 'unnamedplus'
vim.g.mapleader = ' '
vim.keymap.set('n', '<leader>w', '<Cmd>update<CR>')
vim.keymap.set('n', '<leader>h', '<Cmd>nohlsearch<CR>', { silent = true })
vim.cmd.colorscheme('habamax'):checkhealth reports missing providers (clipboard, Python, Node) and misconfiguration; nvim --clean starts without any config, and nvim -u NONE is the Vim equivalent. The vim command on many systems is a symlink to nvim; vim --version | head -1 says which.
vimdiff#
vimdiff a b (or vim -d, nvim -d) opens files side by side with diffthis set on each window, folds unchanged regions and highlights changed lines and the characters within them. It is git mergetool with merge.tool = vimdiff, and git difftool -t vimdiff for reviewing.
]c [c next, previous change
do obtain: pull the other window's hunk into this one (:diffget)
dp put: push this hunk to the other window (:diffput)
:diffget //2 in a three-way merge, take from the left (LOCAL) buffer; //3 is REMOTE
:diffupdate recompute after manual edits
zo zc zR zM open, close a fold; open, close all
:set diffopt+=iwhite ignore whitespace changes
:set diffopt+=algorithm:patience,indent-heuristic diff algorithm; 8.1+ and Neovim
:windo diffthis compare two already-open windows; :diffoff! ends it
:wqa save every buffer and quitWith git mergetool there are four windows: LOCAL, BASE, REMOTE on top and the merged file below. Edit the bottom one, :diffget LO or :diffget RE (buffer name prefixes work), then :wqa; :cq exits non-zero to tell Git the merge failed.
Oneliners#
" Delete trailing whitespace in the whole file
:%s/\s\+$//e
" Convert tabs to spaces per the current settings, or the reverse with noexpandtab
:set expandtab | retab
" Sort the visual selection, removing duplicates
:'<,'>sort u
" Sort by the number in each line
:%sort n
" Reverse every line in the file
:g/^/m0
" Number each line in the selection
:'<,'>s/^/\=line('.') - line("'<") + 1 . '. '/
" Insert the output of a command below the cursor
:r !date -Is
" Replace the buffer with the output of a filter
:%!jq .
" Format a JSON selection in place
:'<,'>!python3 -m json.tool
" Run the current line as a shell command and replace it with the output
!!sh
" Write the selection to a file
:'<,'>w part.txt
" Open every file containing a pattern, one per buffer, and jump through matches
:grep -r pattern . | copen then :cn :cp
" Substitute across the quickfix list (Vim 8+, Neovim): grep, then
:cdo s/old/new/g | update
" Increment a column of numbers, one more each line: select with C-v, then
g C-a
" Join all lines into one
:%j
" Split a line on commas
:s/,/\r/g
" Change the file's line endings to Unix
:set ff=unix | w
" Show the character under the cursor as a code point
ga
" Show the full path of the current file
:echo expand('%:p') or C-g, or 1 C-g
" Change to the directory of the current file
:cd %:h
" Open the file name under the cursor, or the URL with a handler
gf gx
" Diff the buffer against the file on disk
:w !diff % -
" Spell check with the Australian dictionary
:setlocal spell spelllang=en_au then ]s z= zg
" Open the file with the cursor at a line, at a pattern
vim +42 file vim +/pattern file
" Run Ex commands from the shell without opening the editor
vim -es -c '%s/foo/bar/g' -c 'wq' file
" Encrypt a file (Vim only; Neovim removed it)
vim -x secrets.txtScripts#
Batch-edit files with Vim’s engine from Bash, for edits that need Vim’s regex or text objects rather than sed.
#!/usr/bin/env bash
# usage: vim-batch '<ex command>' file...
# Example: vim-batch '%s/\v<colour>/color/g' src/*.md
set -euo pipefail
cmd=${1:?ex command}; shift
for f in "$@"; do
[[ -f $f ]] || { printf 'skip: %s\n' "$f" >&2; continue; }
vim -es -u NONE -i NONE -c "set nomore" -c "$cmd" -c 'update' -c 'qa!' -- "$f" </dev/null \
|| printf 'failed: %s\n' "$f" >&2
doneFind and dispose of stale swap files across a tree; swap files with no owning process are safe to remove once you have confirmed the file on disk is current.
#!/usr/bin/env bash
set -euo pipefail
dir=${1:-$HOME}
find "$dir" -type f \( -name '.*.swp' -o -name '.*.swo' \) -print0 | while IFS= read -r -d '' swp; do
pid=$(vim -r "$swp" 2>&1 | awk '/process ID/ {print $NF}' | tr -dc '0-9' || true)
if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then
printf 'in use by %s: %s\n' "$pid" "$swp"
else
printf 'stale: %s\n' "$swp"
[[ ${DELETE:-0} == 1 ]] && rm -f -- "$swp" # DELETE=1 removes; default is report only
fi
doneProfile startup and print the slowest sourced scripts.
#!/usr/bin/env bash
set -euo pipefail
log=$(mktemp)
trap 'rm -f "$log"' EXIT
vim --startuptime "$log" -c 'qa!' "${1:-}" >/dev/null 2>&1 </dev/null
printf 'total: %s ms\n' "$(tail -1 "$log" | awk '{print $1}')"
grep -E 'sourcing|require' "$log" | sort -k2 -rn | head -15 # column 2 is the elapsed time of that itemTroubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| Pasted code gains indentation on every line | autoindent applied to pasted text as if typed; terminal did not send bracketed paste | Vim 8.0+ and Neovim detect bracketed paste in terminals that support it (:set t_BE?); otherwise :set paste, paste, :set nopaste, or use "+p |
E325: ATTENTION, swap file found | Another Vim has the file open, or one crashed | Read the message: process still running means edit elsewhere; otherwise R recovers, D deletes the swap, and vim -r file recovers from the shell |
| Startup takes seconds | A slow plugin, or a clipboard provider probing X11 over SSH | vim --startuptime log; vim -u NONE to confirm; in Neovim :checkhealth provider |
| Escape delay before Normal mode | ttimeoutlen too high, or esc-time in tmux | set ttimeout ttimeoutlen=50; tmux escape-time |
Arrow keys insert A B C D in Insert mode | TERM wrong, or nocompatible unset with an ancient config | Check echo $TERM; a vimrc that exists but sets nothing leaves Vim in vi-compatible mode |
| Colours wrong or missing in tmux or SSH | TERM lacks 256-colour or RGB capability | :set termguicolors only after tmux’s RGB feature is set; otherwise :set notermguicolors |
E212: Can't open file for writing | Read-only file or directory | :w !sudo tee % > /dev/null then :e!, or open with sudoedit |
E37: No write since last change | hidden off and switching buffers | set hidden, or :w first, or :e! to discard |
| Search finds nothing though the text is there | Special characters unescaped, or \v missing; or ignorecase off and case differs | \V for a literal search, \c for case-insensitive |
| Undo lost after reopening | undofile off, or undodir missing so nothing was written | set undofile undodir=~/.vim/undo// and mkdir -p ~/.vim/undo |
. does not repeat a visual-mode operation as expected | . repeats on the same number of characters or lines, not the same object | Use a text object or macro instead |
| Mapping does not work in a filetype | An ftplugin overrides it with <buffer> | :verbose map <key> shows the winner and where it was set |
File shows ^M at line ends | CRLF file opened with fileformat=unix | :e ++ff=dos to reread, or :%s/\r$// to strip; :set ff=unix before writing |
E492: Not an editor command for a plugin command | Plugin not loaded, or load order | :scriptnames, :packpath?; in Neovim :Lazy or the manager’s status view |
vim -V9/tmp/vimlog traces every sourced line and autocommand into a file, which locates a plugin that changes an option behind your back; :verbose set opt? is the quicker check for one option.
Further reading#
- Vim user manual: the task-oriented chapters;
:help user-manualopens the same thing - Vim reference: motions and text objects: the complete list with edge cases
- Vim reference: pattern syntax: the regex flavour,
\v, and multi-line matching - Neovim: differences from Vim: defaults, removed features and added mappings
- Neovim: Lua guide: configuring options, mappings and autocommands from
init.lua