Software Engineering WikiSE Wiki

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.

Reviewed MarkdownEdit

On this page

Cheatsheet#

TaskKeys 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, redou, C-r
Delete a line, a word, to end of linedd, dw, D
Change inside quotes, parentheses, a wordci", ci(, ciw
Yank a line, paste after, paste beforeyy, 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 listC-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 timesqq ... 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.

ModeEnterPurpose
NormalEscapeCommands and motions; where you spend most time
Inserti, a, I, A, o, O, c, sTyping text
Visualv, V, C-vCharacter, line and block selection; an operator then acts on it
Command-line:, /, ?, !Ex commands, search, filters
ReplaceROverwrite characters
Terminal:terminalA 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.

OperatorAction
dDelete (into a register; see below)
cChange: delete and enter Insert
yYank (copy)
> <Shift indentation
=Re-indent using the filetype’s rules
gu gU g~Lowercase, uppercase, toggle case
gqReformat 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).

MotionMoves
h j k lOne character or line
w b eWord 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 GFirst line, last line; 42G is line 42
H M LTop, middle, bottom of the window
C-d C-u C-f C-bHalf page, full page
'' ``Line, exact position before the last jump
/pat ?patSearch: 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).

ObjectSelects
iw awWord; aw includes the following space
iW aWWhitespace-delimited word
is asSentence
ip apParagraph
i( a( (or ib, i))Parentheses, also [ { <
i" a"Quoted string, also ' and `
it atXML 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.

RegisterHolds
""Last delete, change or yank; what p uses
"0Last yank only, untouched by deletes: the fix for “I yanked, deleted, and pasted the wrong thing”
"1-"9Last nine deletes or changes of at least a line, shifting down
"-Last small (less than a line) delete
"a-"zNamed; "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 TODO

A 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 lines

Buffers, 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 it

hidden (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.

MarkMeaning
`.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 gq

noremap 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 quit

With 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.txt

Scripts#

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
done

Find 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
done

Profile 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 item

Troubleshooting#

SymptomCauseFix
Pasted code gains indentation on every lineautoindent applied to pasted text as if typed; terminal did not send bracketed pasteVim 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 foundAnother Vim has the file open, or one crashedRead 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 secondsA slow plugin, or a clipboard provider probing X11 over SSHvim --startuptime log; vim -u NONE to confirm; in Neovim :checkhealth provider
Escape delay before Normal modettimeoutlen too high, or esc-time in tmuxset ttimeout ttimeoutlen=50; tmux escape-time
Arrow keys insert A B C D in Insert modeTERM wrong, or nocompatible unset with an ancient configCheck echo $TERM; a vimrc that exists but sets nothing leaves Vim in vi-compatible mode
Colours wrong or missing in tmux or SSHTERM 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 writingRead-only file or directory:w !sudo tee % > /dev/null then :e!, or open with sudoedit
E37: No write since last changehidden off and switching buffersset hidden, or :w first, or :e! to discard
Search finds nothing though the text is thereSpecial characters unescaped, or \v missing; or ignorecase off and case differs\V for a literal search, \c for case-insensitive
Undo lost after reopeningundofile off, or undodir missing so nothing was writtenset 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 objectUse a text object or macro instead
Mapping does not work in a filetypeAn ftplugin overrides it with <buffer>:verbose map <key> shows the winner and where it was set
File shows ^M at line endsCRLF 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 commandPlugin 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#