Getting Started
Modes & Basic Movement
Vim is modal — different modes serve different purposes. Esc returns to Normal mode from any other mode. The hjkl keys keep your hands on the home row. w/b move by word, 0/$ move to line boundaries, gg/G jump to file start/end, :N jumps to line N.
# Vim Modes
# Normal mode (Esc) - default, for navigation
# Insert mode (i) - for typing text
# Visual mode (v) - for selecting text
# Command mode (:) - for Ex commands
# Basic movement (Normal mode)
h # left
j # down
k # up
l # right
w # next word start
b # previous word start
0 # beginning of line
$ # end of line
gg # first line
G # last line
:42 # go to line 42Exiting & Saving
ZZ and ZQ are the fastest ways to exit — they don't require reaching for the colon. :x only writes if the file has changed (preserves mtime), unlike :wq which always writes. :w! overrides read-only flags if filesystem permissions allow. Use :qa! to bail out of multiple modified buffers.
:w # write (save) file
:w! # force write (override read-only)
:q # quit
:q! # force quit (discard changes)
:wq # write and quit
:x # write only if changed, then quit
ZZ # same as :x (write if changed, quit)
ZQ # quit without saving (like :q!)
:w newfile.txt # save as new file
:sav newfile.txt # save as new file and switch to it
:wa # write all buffers
:xa # write all changed buffers and exit
:qa! # force quit all (discard everything)Help System
Vim's help is comprehensive and hyperlinked. Ctrl-] follows a tag (link) and Ctrl-T pops back. Help notation: i_CTRL-N means Ctrl-N in insert mode, 'number' (with quotes) means an option. :helpgrep searches all help files at once — use :cn/:cp to navigate matches. K invokes man for the keyword under the cursor (configurable via 'keywordprg').
:help # main help
:help subject # help on subject (e.g. :help insert)
:help i_CTRL-N # help on Ctrl-N in insert mode
:help 'number' # help on the 'number' option (quotes)
:helpgrep pattern # search all help files for pattern
:cn / :cp # next/prev helpgrep match
K # man page for word under cursor (Normal mode)
Ctrl-] # jump to tag under cursor (follow link)
Ctrl-T # jump back from tag (pop tag stack)
:helptags ~/.vim/doc # regenerate help tags for a doc directoryWord & Character Movement
Lowercase motions (w/b/e) treat punctuation as word separators — useful for code. Uppercase motions (W/B/E) only treat whitespace as separators — useful for prose. e/E move to the end of the next word; ge/gE move to the end of the previous word (rarely used but handy).
# Word motions (punctuation-aware)
w # next word start
b # previous word start
e # end of next word
ge # end of previous word
# WORD motions (whitespace-only delimiters, uppercase)
W # next WORD start
B # previous WORD start
E # end of next WORD
gE # end of previous WORD
# Word boundaries:
# word: sequence of word-characters OR sequence of punctuation
# WORD: anything separated by whitespace
# Examples (cursor on 'a' in "foo.bar baz"):
# w -> 'bar' (punctuation splits words)
# W -> 'baz' (only whitespace splits WORDS)Line Movement
0 and $ go to absolute line start/end (including whitespace). ^ and g_ skip leading/trailing whitespace — usually what you want. When 'wrap' is on, g0/g$ operate on screen lines while 0/$ operate on logical lines. | jumps to a specific column number.
0 # first column (absolute start)
^ # first non-blank character
$ # end of line (last character)
g_ # last non-blank character
g0 # start of screen line (with wrap)
g$ # end of screen line (with wrap)
gm # middle of screen line
gM # middle of text line
| # column N (e.g. 10| = column 10)
+ # first non-blank of next line
- # first non-blank of previous line
_ # first non-blank of current line [count down]File & Screen Movement
H/M/L jump within the visible screen. Ctrl-d/u scroll half a page (less disorienting than full-page Ctrl-f/b). zz/zt/zb scroll the current line to the middle/top/bottom of the screen without moving the cursor — extremely useful for keeping context while editing. N% jumps to a percentage of the file.
gg # go to first line
G # go to last line
Ngg / NG # go to line N (e.g. 50gg or 50G)
:N # go to line N (Ex command)
H # top of screen (High)
M # middle of screen
L # bottom of screen (Low)
N% # jump to N% of the file
Ctrl-f # scroll forward (full page)
Ctrl-b # scroll backward (full page)
Ctrl-d # scroll down (half page)
Ctrl-u # scroll up (half page)
zz # scroll current line to middle
zt # scroll current line to top
zb # scroll current line to bottomInserting Text
Basic Insert (i, I)
i/I insert before the cursor / at line start. a/A insert after the cursor / at line end. gi returns to the last insert position (mark '^) — handy when you Esc to do something then want to keep typing. gI goes to the absolute column 1, ignoring leading whitespace.
i # insert before cursor
I # insert at first non-blank (same as ^i)
gi # insert at last insert position (also sets mark)
gI # insert at column 1 (same as 0i)
a # insert after cursor
A # insert at end of line (same as $a)
# Examples:
# foo|bar + i -> foo|bar (cursor stays, type before)
# foo|bar + I -> |foobar (cursor at first non-blank)
# foo|bar + A -> foobar| (cursor at end)
# gi returns to where you last left insert mode —
# very useful after escaping to run a Normal-mode command.Open New Lines (o, O)
o and O open a new line below/above the cursor and enter insert mode. They're the fastest way to add a line — no need to position the cursor at line end. Indentation is auto-copied from the surrounding lines (controlled by 'autoindent').
o # open new line BELOW cursor, enter insert
O # open new line ABOVE cursor, enter insert
# Before (cursor on middle line):
# line one
# li|ne two
# line three
# After 'o':
# line one
# line two
# | <- new blank line, insert mode
# line three
# After 'O':
# line one
# | <- new blank line, insert mode
# line two
# line threeChange Commands (c, C, s, S)
c{motion} deletes the motion target and enters insert mode — the most flexible edit command. C is c$ (change to end of line), S is cc (change whole line). s deletes one character and enters insert; combine with counts like 3s. ciw/caw use text objects (see dedicated section).
cw / ce # change to end of word
cb # change backward to word start
c$ / C # change to end of line
cc / S # change entire line
c0 # change to beginning of line
c^ # change to first non-blank
ct, # change up to (but not including) next ','
cfx # change up to and including next 'x'
ciw # change inner word (no surrounding whitespace)
caw # change a word (with surrounding whitespace)
cap # change around paragraph
s # substitute: delete char under cursor, insert
S # substitute: delete entire line, insert (like cc)Replace Mode (r, R, gr)
r replaces a single character and returns to Normal mode immediately (no mode change). R enters Replace mode where typing overwrites characters — Backspace restores them, unlike Insert mode which just deletes. Use gr/gR (virtual replace) when working with tabs — it preserves tab alignment instead of shifting text.
r{x} # replace single char under cursor with x
gr{x} # virtual replace (doesn't shift tabs)
R # enter Replace mode (overwrite until Esc)
gR # virtual Replace mode (tabs stay aligned)
3r{x} # replace 3 chars with x (single char only)
# In Replace mode, typing overwrites existing text
# but Backspace restores the original characters.
# Tabs are preserved with virtual replace (gr/gR).
# Examples:
# abc|def + rx -> abc|xef (cursor advances)
# abc|def + R -> enter Replace mode
# type 'XYZ' -> abcXYZ|f
# Backspace -> abcXY|def (original restored)Special Inserts
:r !cmd inserts shell command output into the buffer — great for pasting command output, dates, file lists. In insert mode, Ctrl-r followed by a register name inserts that register; Ctrl-r = evaluates a Vimscript expression and inserts the result. Ctrl-k inserts a digraph (special character) by typing two ASCII chars.
:r file.txt # insert contents of file below cursor
:r !command # insert output of shell command
:0r !date # insert date at top of file
:r !ls -la # insert directory listing
# Insert with special registers
Ctrl-r = # expression register: evaluate, insert result
Ctrl-r " # insert unnamed register
Ctrl-r % # insert current filename
# Insert from current file path
:i! !pwd # insert pwd output (no autoindent)
# Non-ASCII / digraphs (in insert mode)
Ctrl-k {ch1}{ch2} # digraph: e.g. Ctrl-k e' produces é
:digraphs # list available digraphsInsert Mode Shortcuts
Ctrl-h/w/u work like Backspace/Delete-word/Delete-line within insert mode. Ctrl-t/Ctrl-d adjust indentation without leaving insert mode — invaluable for code. Ctrl-n/Ctrl-p do keyword completion from buffers and tags. Ctrl-o runs a single Normal command then returns to insert (e.g. Ctrl-o zz to recenter). Ctrl-g u breaks the undo chain so a long insert can be undone in pieces.
# In insert mode:
Ctrl-h # delete char before cursor (Backspace)
Ctrl-w # delete word before cursor
Ctrl-u # delete to start of line
Ctrl-t # indent current line (one shiftwidth)
Ctrl-d # dedent current line (one shiftwidth)
Ctrl-n # keyword completion (forward)
Ctrl-p # keyword completion (backward)
Ctrl-o # execute one Normal command, return to insert
Ctrl-r {reg} # insert register contents
Ctrl-a # insert text typed in last insert mode
Ctrl-@ # insert last text + Esc (like Ctrl-a then Esc)
Ctrl-g u # break undo sequence (start new undo block)
Ctrl-] # trigger abbreviation without insertingEditing Commands (Delete/Yank/Paste)
Delete Commands
d{motion} is the universal delete operator — combine with any motion. dd deletes the whole line. x is a shortcut for dl (delete char). Deleted text goes into the unnamed register (and register 1). Use d with text objects (ciw, dib) for powerful edits. J joins the next line onto the current one with a single space.
x # delete char under cursor (also 'dl')
X # delete char before cursor (also 'dh')
dd # delete current line
dw # delete to next word start
de # delete to end of word
d$ / D # delete to end of line
d0 # delete to beginning of line
d^ # delete to first non-blank
dgg # delete from cursor to first line
dG # delete from cursor to last line
J # join next line (removes leading whitespace, inserts space)
gJ # join next line without inserting space
3dd # delete 3 lines
dW # delete WORD (whitespace-separated)Yank (Copy)
y{motion} copies text without deleting. yy yanks the line. Unlike d (which has x as a shortcut), y has no single-character shortcut — you must use a motion. Yank always updates register 0, useful for distinguishing from deletes. Use text objects: ya( yanks around parens, yi" yanks inside quotes.
y # yank {motion} (no character shortcut)
yy / Y # yank current line
yw # yank to next word start
ye # yank to end of word
y$ # yank to end of line
y0 # yank to beginning of line
yG # yank to end of file
ygg # yank to beginning of file
5yy # yank 5 lines
ya( # yank around parentheses
yi" # yank inside double quotes
yt, # yank up to (not including) next comma
# Yanked text goes to:
# " (unnamed register)
# 0 (yank register, only updated by y)Put (Paste)
p pastes after the cursor (or below the line for linewise data); P pastes before. gp/gP move the cursor to the end of the pasted text — useful when chaining operations. ]p/[p reindent pasted lines to match surroundings. The clipboard register (+) shares with the system clipboard if 'clipboard' is set properly.
p # paste after cursor (linewise: line below)
P # paste before cursor (linewise: line above)
gp # paste after, leave cursor on last pasted char
gP # paste before, leave cursor on last pasted char
]p # paste after, reindent to match
[p # paste before, reindent to match
"+p # paste from system clipboard
"*p # paste from primary selection (X11)
# Linewise vs charwise:
# yy + p -> pasted as a new line below
# y$ + p -> pasted after cursor on same line
# Counts: 3p pastes the register 3 times.Changing Case
~ toggles a single character and advances. g~/gu/gU are operators that work with motions or text objects. In visual mode, U/u/~ act on the selection. Use gUiw to uppercase the current word, gUU for the whole line. These are non-destructive — only case changes.
~ # toggle case of char under cursor (advances)
3~ # toggle case of next 3 chars
g~{motion} # toggle case of motion
gu{motion} # lowercase motion
gU{motion} # uppercase motion
g~~ # toggle case of current line
guu # lowercase current line
gUU # uppercase current line
g~~gg # toggle case to first line
# Visual mode:
# U # uppercase selection
# u # lowercase selection
# ~ # toggle case of selection
# Examples:
# hello + gUw -> HELLO
# HELLO + guw -> hello
# hello + g~w -> HELLOJoining & Splitting Lines
J joins lines with a single space; gJ joins without inserting anything. :join works on a range. gq is the format operator — it wraps lines to 'textwidth' (commonly 80), respecting 'formatoptions'. Use gqip to reflow a paragraph, gqq for one line. Set 'textwidth=80' to enable auto-wrapping.
J # join next line with current (inserts space)
gJ # join next line without inserting space
3J # join next 3 lines into one
:[range]join # join lines in range (also :j)
:[range]join! # join without inserting space
# Splitting a line:
# In insert mode, press Enter (autoindent applies)
# In normal mode: r<Enter> replaces char with newline
# Break a long line at column 80 (with 'textwidth'):
gq{motion} # format motion text to 'textwidth'
gqq # format current line
gqip # format paragraph
gvgq # format visual selection
# Set text width:
:set textwidth=80Repeat & Undo
. repeats the last change — the single most useful key in Vim. u/Ctrl-r are undo/redo. U undoes all changes on the current line. :earlier/:later can time-travel by time or count, even across branches (Vim keeps an undo tree). Use :undolist to see branches. @: repeats the last Ex command, & repeats the last substitute.
. # repeat last change (most powerful key in Vim)
u # undo
Ctrl-r # redo
U # undo all changes on current line
:Nu # undo N changes
:earlier 10m # revert to state 10 minutes ago
:later 5 # redo 5 changes
:undolist # show undo tree branches
# Repeat with counts:
5. # repeat last change 5 times
# Repeat Ex commands:
@: # repeat last :command
& # repeat last :s (substitute)
# Repeat macros/marks across files:
:& # repeat last substitute on current lineSearch & Substitute
Basic Search
/ and ? search forward/backward; n/N repeat. * searches for the whole word under the cursor (word boundaries), g* searches for any substring match. :noh clears current highlighting until the next search. Set 'hlsearch' to highlight all matches persistently, 'incsearch' for incremental matching as you type.
/pattern # search forward for pattern
?pattern # search backward
n # repeat search (same direction)
N # repeat search (opposite direction)
* # search forward for word under cursor
# # search backward for word under cursor
g* # search forward (loose, matches substring)
g# # search backward (loose)
# Search history:
# Press / then Up/Down to recall previous searches
# Clear search highlighting:
:nohlsearch # or :noh
# Toggle 'hlsearch' permanently:
:set hlsearch!Search Options
'ignorecase' + 'smartcase' is the recommended combo: case-insensitive unless your pattern contains an uppercase letter. \c/\C override case per-search. \v enables 'very magic' mode where many characters become special without escaping — cleaner regex syntax. Search offsets (e, b) place the cursor relative to the match.
:set ignorecase # case-insensitive search (ic)
:set smartcase # case-sensitive if pattern has uppercase
:set hlsearch # highlight all matches (hls)
:set incsearch # show match as you type (is)
:set wrapscan # wrap around end of file (ws)
:set gdefault # add 'g' flag to all :s by default
# Per-search case override:
/foo\c # case-insensitive search for foo
/foo\C # case-sensitive search for foo
# Use very-magic mode (less escaping):
/foo\v(.+)@<=(bar) # very magic regex
# Search offsets:
/foo/e # cursor on last char of match
/foo/e+2 # cursor 2 chars past match
/foo/b-1 # cursor 1 char before matchSearch Patterns (Regex)
Vim regex differs from PCRE — backslashes are needed for +, =, {, (, |. Use \v prefix for 'very magic' mode where these become special without escaping (closer to PCRE). \< \> mark word boundaries. \d/\w/\s are character classes. Use \(...\) for capturing groups, \1/\2 for backreferences.
# Standard Vim regex (default magic mode)
. # any char
* # zero or more (preceding atom)
\+ # one or more
\= # zero or one (optional)
\{n,m} # between n and m
^ $ # start/end of line
\< \> # word boundaries
[abc] # char class (a, b, or c)
[^abc] # negated char class
\( \) # group
\1 \2 # backreference
\| # or
# Predefined classes:
\d \D # digit / non-digit
\w \W # word char / non-word
\s \S # whitespace / non-whitespace
# Very magic (\v prefix) — less escaping:
/\v\w+\s+\w+ # word, spaces, word (no backslashes needed)
/\v(%d+) # capture one-or-more digitsSubstitute Command
:[range]s/pattern/replacement/flags is the substitute command. :% means the whole file. Without 'g' flag, only the first match per line is replaced; 'g' replaces all. 'c' asks for confirmation (y/n/a/q/l). Use \v for very-magic to simplify grouping syntax. Capture groups are referenced as \1, \2 in the replacement.
:[range]s/old/new/[flags]
# Basic:
:s/old/new/ # replace first 'old' on current line
:s/old/new/g # replace all 'old' on current line
:%s/old/new/g # replace all in whole file
:%s/old/new/gc # confirm each replacement
:%s/old/new/gi # case-insensitive
# Range examples:
:5,10s/old/new/g # lines 5-10
:.,$s/old/new/g # current line to end
:'a,'bs/old/new/g # from mark a to mark b
:'<,'>s/old/new/g # visual selection
:1,$s/old/new/g # whole file (same as %)
# Replace with capture groups:
:%s/\(\w\+\),\(\w\+)/\2,\1/ # swap two comma-separated words
:%s/\v(\w+),(\w+)/\2,\1/ # same with very-magicSubstitute Flags & Special Replacement
Common flags: g (all), c (confirm), i (case-insensitive), n (count only). & in the replacement inserts the matched text. \u/\U/\l/\L modify case of the replacement. Use \r to insert a newline (\n is a null byte in replacement). The 'n' flag is great for counting matches without changing anything.
# Flags:
g # all occurrences on each line (not just first)
c # confirm each substitution
i # ignore case
I # don't ignore case
e # don't error if no match
p # print last substituted line
n # count matches, don't substitute
# Special chars in replacement:
& # the matched text
\0 # the matched text (same as &)
\1..\9 # capture groups
\u # next char uppercase
\U # rest of string uppercase (until \e or \E)
\l \L # lowercase (single / until \e)
\r # split into new line
\n # null byte (use \r for newline)
# Examples:
:%s/foo/&bar/g # foo -> foobar
:%s/\v(\w+)/\U\1/g # uppercase every word
:%s/, /,\r/g # split lines on ', '
:%s/old/new/gn # count matches onlyGlobal Command (:g)
:g runs an Ex command on every line matching a pattern — a powerful 'global' editor. :v (or :g!) runs on non-matching lines. Combine with any Ex command: d (delete), m (move), t (copy), s (substitute), normal (run normal-mode command). Useful for batch operations on tagged lines. Order matters: :g processes top-to-bottom by default.
:[range]g/pattern/cmd # run cmd on each matching line
:[range]g!/pattern/cmd # run cmd on NON-matching lines
:v/pattern/cmd # same as :g! (inverse)
# Common uses:
:g/^$/d # delete all blank lines
:g/TODO/d # delete all lines with TODO
:g/pattern/p # print all matching lines
:g/pattern/normal @a # run macro a on each matching line
:g/pattern/m0 # move matching lines to top (reverse order)
:g/pattern/t$ # copy matching lines to end
:g/pattern/s/old/new/g # substitute on matching lines only
# Combined with Ex range:
:g/^#/s/foo/bar/g # in lines starting with #, replace foo with bar
# Multiple commands per match:
:g/pattern/d|t0 # delete then copy (chained with |)Visual Mode
Visual Mode Types
v/V/Ctrl-v select characters/lines/block. gv reselects the last visual selection — handy for re-applying an operation. o moves to the other end of the selection (so you can extend in either direction). For blockwise, O moves to the other corner of the same line.
v # charwise visual (select characters)
V # linewise visual (select whole lines)
Ctrl-v # blockwise visual (select rectangle)
gv # reselect last visual selection
# In visual mode:
o / O # move cursor to other end of selection (corner for block)
Esc / v # exit visual mode (toggle)
# Quick selections from normal mode:
vap # select around paragraph
vip # select inside paragraph
i" # then press v... actually ciw etc. work directly
# Switching between types:
# v then V -> linewise (extends to whole lines)
# V then v -> charwise
# Ctrl-v -> blockwiseVisual Operations
Once you have a selection, d/c/y/x work as expected. = auto-indents (great for code reformatting). >/< indent/dedent one shiftwidth. ! pipes the selection through a shell command and replaces it with the output. Pressing : in visual mode auto-fills '<,'> as the range, so :s/old/new/g runs only on the selection.
# In visual mode, press:
d / x # delete selection
c / s # change selection (delete, enter insert)
y # yank (copy) selection
~ # toggle case of selection
u / U # lowercase / uppercase selection
> # indent selection
< # dedent selection
= # auto-indent selection (reformat)
!cmd # filter through external command
:r !cmd # (after yank) paste command output
# Replace entire selection with a char:
r{x} # replace every char with x
# Print line numbers of selection:
g Ctrl-g # count words/chars/lines in selection
# Search within selection (after selecting, type :):
# '<,'>s/old/new/g -- range auto-filledVisual Block Tricks
Visual block (Ctrl-v) is one of Vim's most powerful features. I and A insert before/after the block — text is replicated to every line after Esc. This is the easiest way to add a comment prefix to multiple lines or append a suffix. c changes the entire block; r{x} replaces every char with x. Block edits only work cleanly when lines are similar length.
# Enter block visual: Ctrl-v
# Select a column rectangle, then:
I # insert BEFORE block (text typed appears in front)
# Type text, then Esc -> inserted on every line of block
A # insert AFTER block (text appears at end of each line)
# Esc propagates the insert to all lines
c # change block (delete, enter insert; Esc propagates)
r{x} # replace every char in block with x
# Common use cases:
# 1. Add a comment to a column of lines:
# Ctrl-v select first column, I, type '# ', Esc
# 2. Add semicolons at end of selected lines:
# Ctrl-v select end column, $A;, Esc
# 3. Delete a column:
# Ctrl-v select, d
# 4. Replace a column with same char:
# Ctrl-v select, r;Visual Line Mode
V selects whole lines. After V, common operations are d/y/>/</= to manipulate them. :m moves the selection (e.g. :'<,'>m0 moves it to the top). :t duplicates. '< and '> are marks at the start/end of the last visual selection — useful in scripts. gv reselects the last visual range.
V # enter linewise visual
5V # select 5 lines (current + 4 below)
ggVG # select entire file
:'<,'> # range referring to last visual selection
# After selecting lines (V), useful commands:
d # delete lines
y # yank lines (linewise)
> # indent lines
< # dedent lines
= # auto-indent lines
:m +2 # move selection down 2 lines
:m -3 # move selection up 3 lines
:t . # duplicate lines (copy below selection)
:g/^/m0 # reverse line order (whole file)
# Reselect last visual:
gv # reselect last visual selection
'< # mark: start of last visual
'> # mark: end of last visualVisual with Text Objects
Text objects work in visual mode too — they extend the current selection. vi( selects inside the parens; va( includes the parens. Repeating a text object (vawaw) extends further. This is often faster than manually positioning the cursor. Combine with operators: ciw changes the inner word regardless of cursor position within it.
# In visual mode, text objects EXTEND the selection:
# After pressing v (charwise), type:
aw # extend selection to include next 'a word'
iw # extend selection to inner word
ab # around brackets ()
ib # inside brackets ()
aB # around braces {}
iB # inside braces {}
a" # around double-quoted string
i" # inside double-quoted string
ap # around paragraph
ip # inside paragraph
# Repeated text objects extend selection:
# vaw -> select word + trailing space
# vawaw -> extend to next word + space
# Switch between inner and around:
# vi( -> select inside parens
# va( -> extend to include the parensVisual Search & Numbering
Pressing : in visual mode pre-fills '<,'> as the range — substitutes only run on the selection. To search for the visual selection, use y/ then Ctrl-r " Enter (paste register into search). g Ctrl-a in visual block creates a running sequence — invaluable for generating numbered lists (1, 2, 3, ...) from a column of identical numbers.
# Search within visual selection:
# 1. Visually select (v/V/Ctrl-v)
# 2. Press : -> :'<,'> appears
# 3. Type s/old/new/g -> substitute only in selection
# Search for current visual selection (forward):
# In visual mode, type:
# \V then / (very-nomagic, escapes regex chars)
# Or use a plugin (e.g. vim-visual-star-search)
# Manual method:
# y/ then Ctrl-r " Enter -> search for selection
# Increment/decrement numbers (visual block):
# Select a column of numbers (Ctrl-v)
Ctrl-a # increment every number in selection by 1
Ctrl-x # decrement every number in selection by 1
g Ctrl-a # increment with running counter (1,2,3,...)
g Ctrl-x # decrement with running counter
# Example: turn 0,0,0,0 into 1,2,3,4:
# Ctrl-v select column, g Ctrl-aBuffers
Buffer Basics
Buffers are in-memory files. :e opens a file into a new buffer. :ls lists all buffers with status flags (% = current, # = alternate, + = modified, h = hidden). :b N switches by number; :b name by unique prefix. Ctrl-6 (or :b#) toggles between current and alternate buffers — extremely common.
:e file.txt # open file in new buffer (current window)
:enew # new empty buffer
:ls / :buffers # list all buffers
:b N # go to buffer N (by number from :ls)
:b name # go to buffer by name (unique prefix)
:bn # next buffer
:bp # previous buffer
:b# / Ctrl-6 # alternate buffer (last edited)
:ball # open all buffers in horizontal splits
:vertical ball # open all buffers in vertical splits
# Buffer list symbols:
# % current window's buffer
# # alternate buffer
# a active (loaded and visible)
# h hidden (loaded but not visible)
# - not modifiable
# = read-only
# + modifiedBuffer Navigation
:bn/:bp cycle through buffers. Ctrl-6 toggles between two most recent — the fastest way to flip back and forth. With 'hidden' set, switching away from a modified buffer doesn't force a save — Vim hides it instead. Use :ls then :b N if you forget buffer numbers. Plugins like fzf.vim make fuzzy buffer switching much faster.
:bn / :bnext # next buffer (wraps around)
:bp / :bprev # previous buffer
:bf / :bfirst # first buffer
:bl / :blast # last buffer
:b# # alternate buffer (Ctrl-6)
:N b # go to buffer number N (e.g. 3b)
:b name<tab> # tab-complete buffer name
:ls then :b N # two-step navigation
# Faster navigation (set in vimrc):
:nmap <C-n> :bn<CR>
:nmap <C-p> :bp<CR>
# Jump to a buffer with fuzzy matching (with fzf.vim):
:Buffers # fuzzy-select from buffer list
# Hide current buffer without saving (if 'hidden' is set):
:hide # or :bn with 'set hidden'Buffer Operations
:bd removes a buffer from the list (file is closed). :bw also wipes marks and buffer-local variables. :%bd | e# deletes all but the current buffer — a common cleanup pattern. :bufdo runs an Ex command in every buffer — powerful for global substitutions, but watch for errors (use ! to ignore, or add 'e' flag to :s).
:bd / :bdelete # delete current buffer
:bd N # delete buffer N
:bd name # delete buffer by name
:bw / :bwipeout # wipe buffer (also clears marks, vars)
:N,N bd # delete buffers in range
:%bd # delete all buffers
:%bd | e# # delete all but current
:bn! # force switch (don't save current)
:wall # write all buffers
:bufdo cmd # run cmd in every buffer
:bufdo! cmd # same but ignore errors
# Useful patterns:
:bufdo %s/old/new/ge # substitute in all buffers
:bufdo setlocal nowrap # apply setting to all buffers
# Save and quit current buffer:
:wq # write current and quit (closes window)Hidden Buffers
With 'set hidden' (highly recommended), you can switch away from modified buffers without saving — Vim keeps them in memory. Without 'hidden', Vim blocks switching until you save or use ! to discard. :ls shows 'h' for hidden buffers and '+' if modified. This setting is essential for productive multi-file editing.
:set hidden # allow switching from modified buffers
:set nohidden # force save/abandon when switching
# With 'hidden' set:
# :bn # switch even if current is modified (no save)
# :q # if buffer is modified, prompts to save
# :qa! # quit all without saving
# :wa # write all (hidden + visible)
# Buffer with changes still in memory:
:ls # shows 'h' flag for hidden, '+' for modified
# Recover list of hidden buffers:
:ls h # only hidden buffers
# Save a hidden buffer:
:w # save current (if visible)
:N w # save buffer N
:wall # save all buffers
# Common workflow with hidden:
# :e file1 -> make changes -> :e file2 -> :e file1 (changes preserved)Argument List
The arglist is a separate list from the buffer list — it's the set of files passed on the command line or set with :args. :n/:prev navigate it. :argdo runs an Ex command in every arglist file — perfect for batch processing like :argdo %s/foo/bar/ge | update. The arglist is often more focused than the buffer list.
# Open files in arglist:
vim file1.txt file2.txt # initial args
:args file1 file2 file3 # replace arglist
:argadd file4 # add to arglist
:arga *.py # add all Python files
:argdelete file2 # remove from arglist
# Navigate arglist:
:n / :next # next file in arglist
:prev / :Next # previous file
:first # first file
:last # last file
:rewind # rewind to first file
# Process all files:
:argdo %s/old/new/ge # substitute in every arg file
:argdo set ff=unix | w # convert line endings in all
# Show current arglist:
:args # show files (current marked with [ ])Buffer List Management
:ls lists buffers, :ls! includes unlisted (deleted but remembered). Use :sb to open a buffer in a split without changing the current window's buffer. There's no built-in 'recently closed buffer' undo, but plugins exist. :bufdo vimgrepadd followed by :copen lets you search across all open buffers via the quickfix list.
:ls # list all buffers
:ls! # list including unlisted buffers
:files # alias for :ls
# Move current buffer to a specific position:
:N b N # buffer number N
# Quickly inspect related files:
:b <tab> # tab-complete buffer names
:b #<tab> # buffers matching '#'
# Find buffer containing a pattern:
:vimgrep /pattern/ % # current buffer only
:bufdo vimgrepadd /pattern/ % # all buffers (then :copen)
# Open buffer in a specific window:
:sb N # open buffer N in horizontal split
:vertical sb N # open buffer N in vertical split
:tab split +b N # open buffer N in new tab
# Restore closed buffers:
# (No built-in, but plugins like vim-molten or
# vim-restore-buffers can help)Marks & Jumps
Setting Marks
Marks are named positions in files. Lowercase (a-z) are buffer-local; uppercase (A-Z) are file marks that persist across Vim sessions. Numbered marks (0-9) are set automatically — 0 is the position when Vim last closed, 1-9 are recent closed files. :marks lists them, :delm deletes.
m{a-zA-Z} # set mark at cursor position
ma # set mark 'a' (lowercase, buffer-local)
mA # set mark 'A' (uppercase, file mark)
m' # set the unnamed mark (jump before/after)
# Lowercase marks (a-z):
# Local to current buffer
# Lost when buffer is unloaded
# Uppercase marks (A-Z):
# File marks — preserved across sessions
# Work even after the file is closed
# Numbered marks (0-9):
# Set automatically by Vim
# 0 = position when Vim was last closed
# 1-9 = positions of last 9 closed files
# View marks:
:marks # list all marks
:marks a b c # specific marks
:delm! a # delete mark 'a' (force)Jumping to Marks
` (backtick) jumps to the exact position (line + column); ' (apostrophe) jumps to the first non-blank of the marked line. `A opens the file containing mark A even if it was closed. `. jumps to where you last changed text, `[ and `] bound the last yanked/deleted region, `< and `> bound the last visual selection.
\`{a-zA-Z} # jump to mark (line AND column)
'{a-zA-Z} # jump to mark (line only, column 1)
\`a # jump to mark 'a' (exact position)
'a # jump to line of mark 'a'
\`A # jump to file mark 'A' (opens file if closed)
\`. # jump to position of last change in this buffer
\`" # jump to position when file was last closed
\`[ # jump to start of last yanked/changed text
\`] # jump to end of last yanked/changed text
\`< # jump to start of last visual selection
\`> # jump to end of last visual selection
\`^ # jump to last insert position (also gi)
# Jump to next/prev mark in alphabetical order:
:[count]next # (no built-in, requires plugin)Jumplist
Vim keeps a jumplist of significant cursor movements. Ctrl-o/Ctrl-i move back/forward through it. 'Significant' means jumps (G, gg, search, mark jumps, tag jumps) — not regular motions like j/k/w. :jumps shows the list. The jumplist is per-window and persists across sessions in viminfo/shada.
Ctrl-o # jump BACK in jumplist (older positions)
Ctrl-i # jump FORWARD in jumplist (newer positions)
Tab # same as Ctrl-i
:ju # show jumplist (:jumps)
# What counts as a jump?
# - Movement commands: G, gg, H, M, L, %
# - Search: /, ?, n, N, *, #
# - Mark jumps: 'x, `x (NOT mm)
# - Buffer switch: Ctrl-6, :b, :e
# - Tag jumps: Ctrl-]
# NOT counted: j, k, w, b, h, l (regular motions)
# Max jumplist size: 100 entries by default
:set jumpoptions=stack # treat jumplist like a stack
# Clear jumplist:
:clearjumps # or :cljChangelist
The changelist tracks edit positions (not navigation). g; jumps to the previous edit, g, to the next. Useful for walking back through your changes: 'where was I just working?'. :changes shows the list. Unlike the jumplist, the changelist is per-buffer. Combine with `.` for the very last change in the current buffer.
g; # go to OLDER position of last change
g, # go to NEWER position of last change
:changes # list changelist
# Each entry shows:
# change number, line, column, text of change
# What counts as a change?
# - Any insertion, deletion, or substitution
# - Records the position BEFORE the change
# Differences from jumplist:
# - Changelist tracks EDITS, not movements
# - Per-buffer (not per-window)
# - Limited to 100 entries
# Combine with marks:
\`. # last change position in current buffer
g; # previous change position
# Common pattern: g;g;g; to walk back through editsFile Marks (A-Z)
Uppercase marks (A-Z) are file marks — they remember the file AND position, persisting across Vim sessions (stored in viminfo). Setting mA in foo.txt lets you return to that position from anywhere with `A, even after restarting Vim. Use them as bookmarks for important files/positions in a project.
# Setting a file mark:
mA # set mark 'A' at cursor (in current file)
mB # set mark 'B' (in current file)
# Jumping to a file mark:
\`A # if file is open: jump to position
\`A # if file is closed: open it, then jump
'A # jump to first non-blank of mark's line
# File marks persist across Vim sessions:
# Stored in viminfo (or shada in Neovim)
# Restored on next Vim launch
# Use cases:
# 1. Bookmark a function: mA in function definition
# 2. Quick jump between files: `A, `B
# 3. Project navigation: set marks in important files
# Listing file marks:
:marks A-Z # show only file marks
# Deleting:
:delm A # delete file mark A (must be in its file)Built-in Special Marks
Vim sets special marks automatically. `.` is the last change, `[ and `] bound the last yanked/deleted region, `< and `> bound the last visual selection, `^ is where insert mode last ended (gi jumps there and re-enters insert). `" is the position when the file was last closed — useful for resuming work.
\`. # position of last change in current buffer
\`" # position when file was last exited
\`[ # start of last yanked/changed text
\`] # end of last yanked/changed text
\`< # start of last visual selection
\`> # end of last visual selection
\`^ # position where insert mode was last stopped
\`' # position before latest jump (where you came FROM)
# Common uses:
\`[\`] # operate on last yanked range: \`[v\`]U (uppercase it)
\`<\`> # re-select last visual: \`<v\`> or gv
# Re-select last visual selection:
gv # easier than \`<v\`>
# Re-select last pasted text:
\`]v\`[ # or use a mapping: nnoremap gp \`]v\`[
# Return to last insert position:
gi # jump to `^ and enter insert modeText Objects
Word & Sentence Objects
Text objects let you operate on semantic units. 'aw' (around word) includes trailing whitespace — daw closes the gap. 'iw' (inner word) leaves whitespace intact — diw leaves a blank. as/ap are sentence/paragraph versions. Repeating an object (dawaw) extends the selection. Most useful with d/c/y/v operators.
# Operators (d/c/y/v) + text object:
aw # 'a word' - word + trailing whitespace
iw # 'inner word' - word only (no whitespace)
aW # 'a WORD' - WORD + trailing whitespace
iW # 'inner WORD' - WORD only
as # 'a sentence' - sentence + trailing space
is # 'inner sentence' - sentence only
ap # 'a paragraph' - paragraph + trailing blank line
ip # 'inner paragraph' - paragraph only
# Examples:
daw # delete word + trailing space (closes gap)
diw # delete word only (leaves whitespace)
cas # change sentence (with trailing space)
yap # yank paragraph (with trailing blank line)
gqip # format (wrap) inner paragraph
# Repeated: dawaw extends to next wordBracket Objects
Bracket objects work on (), [], {}, <>. a (around) includes the brackets; i (inner) excludes them. ci( changes the contents of the parentheses without removing them — perfect for editing function args. Works on the innermost pair when nested. at/it operate on HTML/XML tags (e.g. cit changes inner tag content).
# Brackets (any of ( [ { <):
ab / ib # around / inside ()
aB / iB # around / inside {} (also a{ / i{)
a( / i( # same as ab / ib
a[ / i[ # around / inside []
a{ / i{ # same as aB / iB
a< / i< # around / inside <>
# Examples:
ci( # change text inside ( ... )
daB # delete {...} block including braces
yi[ # yank text inside [...]
vaB # visually select around {...}
# Special: 'tag' objects (HTML/XML):
at / it # around / inside <tag>...</tag>
# Tip: works with mismatched brackets too:
# ci) on (foo bar) changes 'foo bar'
# Nested: ci( in ((a)(b)) changes innermostQuote Objects
Quote objects (a"/i", a'/i', a`/i`) operate on quoted strings. The cursor doesn't need to be inside the quotes — Vim finds the nearest pair. ci" changes the contents of a string without removing the quotes — invaluable for editing string literals. Includes the quotes with 'a', excludes them with 'i'.
a" # around double-quoted string (includes quotes)
i" # inside double-quoted string (no quotes)
a' # around single-quoted string
i' # inside single-quoted string
a\` # around backtick-quoted string
i\` # inside backtick-quoted string
# Examples:
ci" # change text inside "..."
da" # delete "..." (including quotes)
yi' # yank text inside '...'
va\` # visually select around \`...\`
# Cursor can be ANYWHERE on the line — Vim finds the
# nearest quote pair containing or following the cursor.
# Special: with counts
3a" # around 3 consecutive quoted strings
# Tip: ci" when cursor is on the quote itself works tooSentence & Paragraph Objects
Sentences end with . ! ? followed by whitespace (or paragraph break). Paragraphs are blocks separated by blank lines. 'a' includes trailing whitespace, 'i' excludes it. Useful with operators: gqap reformats a paragraph, >ip indents it, das deletes a sentence. The ( and ) motions jump between sentences.
# Sentence: ends with . ! ? followed by whitespace
as / is # around / inner sentence
# Paragraph: blocks of text separated by blank lines
ap / ip # around / inner paragraph
# Examples:
das # delete a sentence
cip # change inside paragraph
vap # select around paragraph (with trailing blank)
# Differences:
# 'a' (around): includes trailing whitespace/blank line
# 'i' (inner): no trailing whitespace
# Use cases:
# gqap # reformat (wrap) the paragraph
# >ap # indent the paragraph
# vip # select inside paragraph
# :'<,'>s/old/new/g # substitute in selected paragraph
# Sentence motion commands (not objects):
( # previous sentence start
) # next sentence startTag & URL Objects (with plugins)
Built-in at/it operate on HTML/XML tags. cit changes inner tag content, dat deletes the whole tag including the open/close. For nested tags, it selects the innermost, then vat extends outward. Plugins add more objects: vim-textobj-url (au/iu), vim-indent-object (ai/ii for same-indent blocks — excellent for code).
# Built-in tag object (HTML/XML):
at / it # around / inside <tag>...</tag>
# Examples:
cit # change inner tag text: <p>text</p> -> <p>|</p>
dat # delete entire tag: <p>text</p> -> (gone)
vit # visually select inner tag text
yat # yank entire tag including <tag>...</tag>
# For nested tags, 'it' selects the innermost:
# <div><p>text</p></div>
# cit on 'text' changes 'text' only
# vat extends selection outward
# URL objects (require a plugin like vim-textobj-url):
# au / iu # around / inside URL
# Indent objects (require vim-indent-object):
# ai / ii # around / inside same-indent block
# aI / iI # entire indentation level
# Common plugin combos:
# vai # select around indent block (great for code)Using Text Objects (Recipes)
Text objects combine with any operator (d/c/y/v/>/</=) for powerful edits. ciw changes a word without you needing to position the cursor exactly. ci"/ci(/ci[ change inside quotes/brackets. gqip reformats paragraphs. Mastering these is the key to editing at the speed of thought in Vim.
# Common editing recipes using text objects:
# Word:
ciw # change word (regardless of cursor position in it)
daw # delete word + space (closes gap)
yiw # yank inner word (no whitespace)
# Quotes:
ci" # change inside "..."
ci'\` # change inside '...' or \`...\`
da" # delete the entire string (with quotes)
# Brackets:
ci( # change inside (...) -- great for args
daB # delete {...} block (function body)
ci[ # change inside [...]
# Sentences & paragraphs:
cas # change a sentence
dap # delete a paragraph
gqip # reformat paragraph
# HTML tags:
cit # change tag content
vat # select tag, then vat again to extend
# Combos with counts:
2daw # delete two words
c2aw # change two wordsRegisters
Register Basics
Registers are Vim's clipboards. "ayy yanks a line into register a, "ap pastes from a. There are many registers: named (a-z), append (A-Z), numbered (1-9), and special (unnamed ", yank 0, clipboard +, etc.). :reg lists them. In insert mode, Ctrl-r {reg} inserts a register. Uppercase letter APPENDS to a register.
# Registers store text. 10 named (a-z), 10 append (A-Z),
# 9 numbered (1-9), and several special ones.
"ay # yank into register 'a' (then motion: "ayy, "ayw)
"ap # paste from register 'a'
"ad # delete into register 'a' (e.g. "add)
"Ay # APPEND to register 'a' (uppercase = append)
# View all registers:
:reg # list all registers
:reg a b c # list specific registers
:reg + # show clipboard register
# Use a register with operators:
"aD # delete to end of line into 'a'
"byy # yank line into 'b'
"cp # paste register 'c'
# Insert mode: Ctrl-r + register
Ctrl-r a # insert contents of register 'a'
Ctrl-r = # expression register (compute, insert)
# Search from a register:
/ then Ctrl-r a # search register 'a' contentsNamed & Numbered Registers
Named registers (a-z) are user-controlled; A-Z append to a-z. Numbered registers (1-9) auto-track recent deletes — 1 is most recent, each new delete shifts them down. Register 0 holds only the last YANK (deletes don't overwrite it) — useful for yank-then-delete workflows. "- holds small (sub-line) deletes.
# Named registers (a-z, A-Z):
"ayy # yank line into 'a'
"byiw # yank inner word into 'b'
"Ayy # append yanked line to 'a' (uppercase)
# Numbered registers (1-9) — auto-filled by deletes:
"1 # last delete (largest)
"2 # second-to-last delete
... # shift older with each new delete
"9 # oldest tracked delete
# Small delete register:
"- # stores deletes < 1 line (e.g. dw)
# Yank register (separate from deletes):
"0 # last YANK only (not affected by deletes)
# Examples:
"0p # paste last yank (won't paste a delete)
"1p # paste last delete
". # last inserted text (read-only)
"% # current file name (read-only)Special Registers
Special registers: ". = last inserted text, "% = current file, "/ = last search, ": = last command. "_ is the black hole — writing to it discards text (useful for deletes you don't want polluting other registers). "+ is the system clipboard, "* is X11 primary selection. "= evaluates a Vimscript expression and inserts the result.
# Read-only registers:
". # last inserted text
"% # current file name
"# # alternate file name
": # last Ex command
"/ # last search pattern
# Read/write registers:
"0 # last yank (not overwritten by deletes)
"- # last small delete (less than one line)
"_ # black hole register (discard, like /dev/null)
"= # expression register (evaluate Vimscript)
# Clipboard / selection:
"+ # system clipboard (Ctrl-C / Ctrl-V)
"* # primary selection (X11 middle-click)
# Examples:
"_dd # delete line without saving to register
"+y # yank to system clipboard (then Ctrl-V elsewhere)
"*p # paste from X11 primary selection
# Expression register (in insert mode):
Ctrl-r =strftime('%Y-%m-%d')<CR>
# inserts today's date
# Black hole: "_ prevents updating other registers.Macros in Registers
Macros live in registers — recording with qa writes keystrokes to register a, @a plays it back. Because they're registers, you can inspect them (:reg a), edit them by pasting/modifying/yanking back, or save them to your vimrc with :let @a = '...'. Escape special keys with \<Esc> in string literals.
# Macros are stored in registers, just like yanked text.
# Record a macro into register 'a':
qa # start recording into 'a'
... # perform actions
q # stop recording
# Play back the macro:
@a # play macro 'a' once
@@ # play last macro again (the one just played)
5@a # play macro 'a' 5 times
:reg a # view the recorded keystrokes
# Edit a macro:
:let @a = "..." # set register 'a' directly
# Or: paste the register, edit, yank back:
"ap # paste macro keystrokes
# ... edit the text ...
"ayy # yank edited version back into 'a'
# Save a macro to vimrc:
:let @a = "0iHello \<Esc>" # escaped special keys
# Apply macro to a range:
:%normal @a # run macro 'a' on every lineAppend & Black Hole
Uppercase register letters APPEND: "Ayy appends to register a (very useful for building up text or multi-step macros). The black hole register "_ discards anything written to it — deletes don't update the numbered/unnamed/yank registers. Use it when you want to delete text without clobbering your last yank.
# Append to a register (use UPPERCASE letter):
"ayy # yank line into 'a'
"Ayy # APPEND another line to 'a'
"Ayiw # APPEND a word to 'a'
# After: register 'a' contains both.
# Black hole register "_:
"_d # delete without affecting other registers
"_dd # delete line, doesn't update "1, "0, "-
"_c # change without saving deleted text
"_x # delete char, nothing saved
# Why use black hole?
# - Keep your yank register ("0) intact while deleting
# - Avoid polluting numbered registers
# Example: yank a line, then delete another (without
# losing the yank):
"ayy # yank into 'a'
"_dd # delete line WITHOUT touching 'a' or '0
"ap # paste the original yank
# When clipboard=unnamed, "_d still bypasses clipboard.Clipboard & Selection
"+ is the system clipboard (Ctrl-C/Ctrl-V in GUI apps), "* is the X11 primary selection (middle-click paste). With :set clipboard=unnamedplus, yanks and pastes go directly to the system clipboard — no need for the "+ prefix. On Linux, you may need xclip/xsel (X11) or wl-clipboard (Wayland) installed.
# System clipboard register:
"+y # yank to system clipboard
"+p # paste from system clipboard
"+dd # delete line to clipboard
"+P # paste before cursor from clipboard
# X11 primary selection (Linux, middle-click):
"*y # yank to primary selection
"*p # paste from primary selection
# Make the system clipboard the default:
:set clipboard=unnamedplus # use '+ for unnamed register
:set clipboard=unnamed # use '* for unnamed register
# Then yank/paste go to clipboard automatically:
yy # yanks to system clipboard (with above setting)
p # pastes from system clipboard
# Check what's in the clipboard:
:reg + # show clipboard contents
# On Linux, install a clipboard tool if missing:
# xclip or xsel for X11
# wl-clipboard for WaylandMacros
Recording Macros
q{letter} starts recording into a register; press q again to stop. The recorded keystrokes can be replayed with @{letter}. When recording, use motions that work consistently — prefer 0, ^, $, w, e over j/k which depend on column. Test on a few lines before bulk-applying.
q{a-z} # start recording into register {a-z}
... # perform actions (keystrokes recorded)
q # stop recording
# Example: wrap each word in quotes on a line:
qa # start recording into 'a'
0 # go to first column
i" # insert opening quote
Esc
e # go to end of word
a" # append closing quote
Esc
w # move to next word
q # stop recording
# Now @a will wrap one more word.
# Recording tips:
# - Use 0/^-positioned motions (not j/k)
# - Use commands that work regardless of line length
# - Test on a few lines before applying broadly
# View the recorded macro:
:reg a # see keystrokes in register 'a'Playing Macros
@a plays the macro once, @@ replays the last macro, N@a plays N times. To apply a macro to many lines, visually select them and run :normal @a, or use :%normal @a for the whole file. Macros stop on error (e.g. when a search fails), so 999@a safely processes to end of file. :g/pattern/normal @a runs on matching lines only.
@a # play macro 'a' once (in current buffer)
@@ # replay the LAST macro played
5@a # play macro 'a' 5 times
100@a # play 'a' up to 100 times (stops on error)
# Play across multiple lines:
# 1. Visually select lines (V)
# 2. :normal @a # play 'a' on each selected line
# Or:
:%normal @a # play on every line in file
:5,20normal @a # play on lines 5-20
:g/pattern/normal @a # play on lines matching pattern
# Recursive macros:
# If macro 'a' contains @a, it'll recurse.
# Add a 'j' at the end to move to next line.
# Common pattern: process all matching lines:
# qa0f:x...jq # record macro moving down 1 line
# :%s/pattern//gn # count matches
# 100@a # play up to 100 timesEditing Macros
Because macros are stored in registers, you can edit them. Paste the register ("ap), modify the text, then yank it back ("ay$ or "ayy). Or use :let @a = '...' with key escapes (\<Esc>, \<CR>). This is much easier than re-recording a complex macro from scratch when you make a small mistake.
# Macros are just text in registers — you can edit them.
# Method 1: paste, edit, yank back:
"ap # paste register 'a' into buffer
# ... edit the pasted text (modify keystrokes) ...
0"ay$ # yank the line back into 'a'
# Method 2: set the register directly with :let
:let @a = "0iHello \<Esc>"
# Note: special keys need \<Esc> notation in strings
# Method 3: edit in a scratch buffer:
:edit
"ap # paste macro
# edit
"ayy # yank back to 'a'
:bdelete
# Common edits:
# - Add 'j' at the end to advance to next line
# - Remove a step that you don't need
# - Replace a hard-coded search with a generic one
# Show the macro as keystrokes:
:reg a # view current 'a' contentsRecursive Macros
Recursive macros call themselves until they error out — typically when a search fails. CRITICAL: clear the register first (let @a = '') before recording, otherwise the first @a call inside the macro plays the OLD contents. The macro self-terminates when the failing search stops the recursion. Great for global find-replace patterns.
# A macro can call itself, enabling recursion.
# Example: change every 'foo' to 'bar' in a column:
# (search-based macro)
# Steps to set up recursive macro 'a':
# 1. Clear register 'a':
:let @a = ''
# 2. Start recording:
qa
# 3. Search for 'foo':
/foo<CR>
# 4. Replace with 'bar' (ciwbar<Esc>):
ciwbar<Esc>
# 5. Call itself:
@a
# 6. Stop recording:
q
# Now @a runs until search fails (no more 'foo').
# The recursion stops automatically on the first error
# (when /foo finds nothing).
# Tip: clear the register first, otherwise the FIRST
# call also replays the old contents of 'a'.
# Run:
@a # processes all 'foo' in fileMacros with Counts
N@a plays the macro N times — but it stops early on any error, so 999@a safely processes to end-of-file. :bufdo normal @a runs the macro in every buffer, :argdo in every arglist file. :g/pattern/normal @a runs it on matching lines only. Counts INSIDE a macro (e.g. 5j) repeat per-play, not per-macro.
# Apply a count to a macro:
5@a # play macro 'a' 5 times
N@a # play up to N times (stops on error)
# Useful: large count to process until end-of-file
999@a # play 'a' up to 999 times (errors stop it)
# Macros with counts INSIDE:
# If your macro starts with '5j', each play jumps 5 lines
# Combine with visual selection (block):
# Ctrl-v to select, then :normal @a
# plays 'a' on each line in the block
# Process every Nth line:
:let i = 0
:g/^/if i % 3 == 0 | normal @a | endif
# (plays 'a' on every 3rd line)
# Apply with a delay (avoid blocking):
# No built-in, but plugins can defer
# Repeat across buffers:
:bufdo normal @a # play 'a' in every buffer
:argdo normal @a # play 'a' in every arg fileMacros across Files & Lines
:[range]normal @a runs a macro on each line in the range — the most common way to apply a macro to many lines. End your macro with 'j' so each iteration advances to the next line. :bufdo/:argdo run it across buffers/files. Add | update to save each modified file. Use ! to continue past errors (e.g. :bufdo!).
# Apply a macro to every line in a range:
:[range]normal @a
:%normal @a # whole file
:5,20normal @a # lines 5-20
:'<,'>normal @a # visual selection
:g/pattern/normal @a # only matching lines
# Apply across buffers:
:bufdo normal @a # every buffer
:argdo normal @a # every arglist file
# Save modified buffers after macro run:
:argdo normal @a | update # save each that changed
# Best practice: include 'j' (move down) at end of macro
# so it advances to the next line for :normal usage.
# Example macro for processing CSV rows:
# qa 0f,dt,j q # cut field at comma, move down
# :%normal @a # apply to every line
# Abort on error vs. continue:
:bufdo! normal @a # continue even if a buffer errors
# Debug: play macro step-by-step (no built-in, but
# :set lazyredraw off + slow key timing helps).Folding
Fold Methods
foldmethod controls how folds are created. 'manual' lets you create folds with zf. 'indent' auto-folds by indentation (great for code). 'syntax' uses syntax rules (language-aware). 'marker' uses {{{ }}} in the file text. 'expr' for custom logic. Manual folds are lost when you close the file unless you :mkview to save them.
:set foldmethod=manual # fold by hand (zf)
:set foldmethod=indent # fold by indent level
:set foldmethod=expr # fold by 'foldexpr'
:set foldmethod=syntax # fold by syntax rules
:set foldmethod=marker # fold by {{{ }}} markers
:set foldmethod=diff # fold unchanged text in diff
# Recommended defaults:
:set foldmethod=indent # great for code
:set foldlevel=2 # show first 2 levels
# Per-method notes:
# manual - zf to create, zd to delete
# indent - automatic, based on shiftwidth
# syntax - automatic, requires syntax file support
# marker - persistent (saved in file as {{{ }}})
# expr - custom logic via 'foldexpr'
# Persistent manual folds across sessions:
:mkview # save folds (and other view state)
:loadview # restore them
# Or set:
:set viewoptions=folds # what :mkview savesManual Folding
In manual mode, zf creates a fold (e.g. zf3j folds the next 3 lines). zd deletes one fold, zD deletes recursively, zE eliminates all folds in the window. Visual mode + zf folds the selection. With foldmethod=marker, you put {{{ and }}} in comments to create persistent folds stored in the file — useful for sharing fold structure in version control.
# Manual fold commands (foldmethod=manual):
zf{motion} # create fold over motion (e.g. zf3j)
zF # create fold over current line(s)
zd # delete fold at cursor
zD # delete fold recursively (nested)
zE # eliminate ALL folds in window
zj / zk # move to next/prev fold start
[z / ]z # move to fold start/end
# Select then fold (visual):
V zf # visually select lines, then fold
# Create fold for a range:
:5,20fold # fold lines 5-20
# Fold using a marker (manual mode):
# Place markers in the file (with foldmethod=marker):
function Foo() " {{{
echo "hi"
endfunction " }}}
# When foldmethod=marker, the {{{ }}} create persistent
# folds stored in the file text itself.Fold Open/Close Commands
zo/zc open/close a fold; za toggles. zO/zC/zA act recursively on nested folds. zr/zm adjust the fold level globally (one level at a time). zR opens everything, zM closes everything — quick ways to reset. zj/zk jump between folds, [z/]z jump to the start/end of the current fold.
zo # open fold (one level)
zc # close fold (one level)
za # toggle fold (open <-> closed)
zO # open fold recursively (all levels)
zC # close fold recursively (all levels)
zA # toggle fold recursively
zr # reduce fold level by 1 (open one level everywhere)
zm # increase fold level by 1 (close one level everywhere)
zR # open ALL folds (reduce to level 0)
zM # close ALL folds (fold everything)
# Navigation in/around folds:
zj # next fold start (down)
zk # previous fold end (up)
[z # start of current open fold
]z # end of current open fold
# Move with folds intact (don't open):
# Use j/k normally — they may skip closed folds
# Set 'foldopen' to control what opens foldsFold Navigation & Editing
j/k may skip over closed folds depending on 'foldopen'. zj/zk navigate between fold boundaries. yy/dd on a folded line operate on ALL the folded lines as a unit (linewise). Typing inside a closed fold opens it. Configure 'foldopen' to control which motions open folds (e.g. remove 'search' to keep folds closed when searching).
# Fold-related motions:
zj # next fold start (move down)
zk # previous fold end (move up)
[z # start of current open fold
]z # end of current open fold
# What opens a fold when triggered?
:set foldopen=block,hor,mark,percent,quickfix,
search,tag,undo
# Defaults open folds on most motions.
# To make j/k NOT open folds:
:set foldopen-=block
# Editing a closed fold:
# Typing inside a closed fold opens it.
# 'o' or 'O' on a fold line opens a new line.
# Search inside closed folds:
# Default behavior depends on 'foldopen'.
# :set foldopen-=search # don't open on search
# Yank/paste on folded lines:
# yy yanks ALL lines in the fold (linewise)
# dd deletes ALL lines in the fold
# Paste over a fold:
# p with a linewise register replaces the folded regionFold Options
foldlevel controls how deep folds stay open (higher = more open). foldlevelstart=99 starts with everything open. foldcolumn shows a sidebar with fold state indicators. foldminlines prevents tiny folds. To persist folds across sessions, :mkview on BufWinLeave and :loadview on BufWinEnter — common autocmd pattern.
:set foldmethod=indent # folding method
:set foldlevel=2 # show folds deeper than this
:set foldlevelstart=99 # start with all folds open
:set foldminlines=3 # min lines to form a fold
:set foldcolumn=4 # 4-char column showing fold state
:set foldenable # enable folding (default on)
:set foldclose=all # auto-close folds when cursor leaves
:set foldopen=block,hor,mark,percent,quickfix,search,tag,undo
:set foldtext=foldtext() # text to show on a closed fold
:set fillchars=fold:\ , # chars for fold display
# Common combo for code:
:set foldmethod=indent
:set foldlevelstart=10
:set foldnestmax=10 # max nesting depth
# Custom fold text:
:set foldtext=MyFoldText()
function MyFoldText()
return '+++ ' . (v:foldend - v:foldstart + 1) . ' lines +++'
endfunction
# Persistent folds across sessions:
:set viewoptions=folds,cursor
:autocmd BufWinLeave * mkview
:autocmd BufWinEnter * silent loadviewNested Folds & Fold Level
Folds nest hierarchically. 'foldlevel' determines the deepest level that stays open — foldlevel=0 closes everything, 99 opens everything. zr/zm adjust by 1, zR/zM are extremes. 'foldnestmax' caps nesting depth to avoid runaway folds in deeply-nested code. Custom foldtext functions let you control what's shown on a closed fold.
# Folds can nest. 'foldlevel' controls which are open:
:set foldlevel=0 # all folds closed
:set foldlevel=1 # only top-level open
:set foldlevel=99 # all open
# Display current fold level:
:echo &foldlevel
# Increase/decrease by 1:
zr # reduce (open one level everywhere)
zm # more (close one level everywhere)
zR # open ALL folds (set foldlevel to 99)
zM # close ALL folds (set foldlevel to 0)
# Limit nesting:
:set foldnestmax=10
# Each indent level adds a fold level.
# Useful for deeply-nested code:
# function outer() {
# function inner() { <- foldlevel 2
# function deep() { <- foldlevel 3
# }
# }
# }
# Custom foldtext for nested folds:
:set foldtext=NeatFoldText()
function! NeatFoldText()
let line = getline(v:foldstart)
let n = v:foldend - v:foldstart + 1
return line . ' (' . n . ' lines)'
endfunctionSplit Windows
Splitting Windows
:split/:vsplit divide the current window — both show the same buffer by default (changes sync). :split file opens a file in a new split. :new/:vnew create empty buffers. Ctrl-w s/v are the keyboard shortcuts. Adding a number prefix sets the new window's size (:10split creates a 10-row window).
:split / :sp # horizontal split (current file)
:vsplit / :vs # vertical split (current file)
:split file.txt # split + open file
:vsplit file.txt # vsplit + open file
:new # horizontal split with empty buffer
:vnew # vertical split with empty buffer
:5split / :5vsplit # split with 5 rows/cols
# Quick splits (Normal mode):
Ctrl-w s # split horizontally (same as :split)
Ctrl-w v # split vertically (same as :vsplit)
Ctrl-w n # new horizontal split (empty)
Ctrl-w q # close window (same as :q)
# Split with size hints:
:10split file.txt # split with height 10
:vert 30split file # vertical split with width 30
# Reopen current file in a new split:
Ctrl-w s # same buffer, two views (sync scroll optional)Window Navigation
Ctrl-w followed by h/j/k/l navigates between windows (mnemonic: hjkl like movement). Ctrl-w w cycles to the next window. For speed, many users map Ctrl-h/j/k/l directly to window navigation. Ctrl-w p returns to the previously-used window — useful when bouncing between two windows.
Ctrl-w h # left window
Ctrl-w j # below window
Ctrl-w k # above window
Ctrl-w l # right window
Ctrl-w w # cycle to next window (wraps)
Ctrl-w W # cycle to previous window
Ctrl-w t # top-left window
Ctrl-w b # bottom-right window
Ctrl-w p # previous (last accessed) window
# Faster (with mapping):
:nmap <C-h> <C-w>h
:nmap <C-j> <C-w>j
:nmap <C-k> <C-w>k
:nmap <C-l> <C-w>l
# In terminal mode:
Ctrl-w N # exit terminal mode (to Normal)
Ctrl-w h/j/k/l # navigate (after exiting terminal mode)
# List windows in current tab:
:ls # also shows windows' buffersWindow Resizing
Ctrl-w +/-/</> adjust size by 1; prefix with a count (10 Ctrl-w +) for bigger steps. Ctrl-w _ maximizes height, Ctrl-w | maximizes width. Ctrl-w = equalizes all windows. For precision, use :resize N (height) and :vertical resize N (width). With 'mouse=a', you can drag status lines and separators directly.
Ctrl-w = # equalize all window sizes
Ctrl-w + # increase height by 1
Ctrl-w - # decrease height by 1
Ctrl-w > # increase width by 1
Ctrl-w < # decrease width by 1
Ctrl-w _ # maximize height (current window)
Ctrl-w | # maximize width (current window)
10 Ctrl-w + # increase height by 10
# Ex commands for precise resize:
:resize 20 # set height to 20 (horizontal split)
:resize +5 # increase height by 5
:resize -5 # decrease height by 5
:vertical resize 80 # set width to 80 (vertical split)
:vertical resize +10 # increase width by 10
# Mouse: drag the status line / vertical separator
# (with :set mouse=a)
# Auto-resize on split:
:set equalalways # default: auto-equalize on split/close
:set noequalalways # don't auto-resizeWindow Movement
Ctrl-w H/J/K/L move the current window to the far edges (capital = full-screen on that side). Ctrl-w r/R rotate windows within the layout. Ctrl-w x swaps with the next window. Ctrl-w T moves a window to a new tab page. A neat trick: Ctrl-w K/H also converts between horizontal and vertical orientation.
Ctrl-w H # move current window to far LEFT (full height)
Ctrl-w J # move current window to BOTTOM (full width)
Ctrl-w K # move current window to TOP (full width)
Ctrl-w L # move current window to far RIGHT (full height)
Ctrl-w r # rotate windows DOWN/RIGHT
Ctrl-w R # rotate windows UP/LEFT
Ctrl-w x # exchange current window with next
Ctrl-w T # move current window to a NEW tab page
# Move split orientation:
Ctrl-w K # if vertical, become horizontal (and move to top)
Ctrl-w H # if horizontal, become vertical (and move to left)
# Example: convert a vertical split to horizontal:
:vsplit # creates vertical split
Ctrl-w H # moves it to the left, becoming horizontal
# Reopen current buffer in a new tab:
Ctrl-w T # current window moves to a new tabWindow Arrangement
Ctrl-w = equalizes window sizes. :ball opens one window per buffer — useful for seeing everything at once. Ctrl-w r/R rotate windows within the layout. To save and restore a complete window layout (including buffers, folds, etc.), use :mksession and :source. Each tab page maintains its own window layout.
# Equalize window sizes:
Ctrl-w = # equalize height and width
# Stack layouts:
# Vertical splits side by side, horizontal splits stacked
# Example setup:
# :vsplit -> | win1 | win2 |
# :split -> | win1 | win3 |
# | | win2 |
# Cycle through layouts (with plugins like golden-ratio,
# or use built-in rotation):
Ctrl-w r # rotate down/right
Ctrl-w R # rotate up/left
# Distribute buffers across windows:
:ball # one window per buffer (horizontal)
:vertical ball # one window per buffer (vertical)
# Layouts persist within a tab page.
# Save current layout to a session:
:mksession mylayout.vim
# Restore:
:source mylayout.vim
# Tab page = a collection of windows
:tabnew # new tab with one windowClosing Windows
:q closes a window (and exits Vim if it's the last). :close (or Ctrl-w c) closes a window but won't close the very last one. :only (Ctrl-w o) closes all other windows. With 'hidden' set, closing a window with a modified buffer hides it instead of forcing a save decision. :only! force-closes others even if modified.
:q / :quit # close current window (errors if last)
:q! # close without saving
:wq # save and close
:close / Ctrl-w c # close window (won't close last)
:only / Ctrl-w o # close all OTHER windows
:hide # hide current buffer in window (close window)
# Save and close splits:
:xa # save all and exit (close all windows)
:wqa # same as :xa
# Closing a window with modified buffer:
# - :q prompts: "Save changes?"
# - :q! discards
# - :wq saves and closes
# - :hide (with 'hidden' set) keeps buffer in memory
# Close every window except the current:
:only # or Ctrl-w o
# Errors if other windows have unsaved changes:
:only! # force-close other windows
# When you close the last window, Vim exits.
# To prevent accidental exit:
:q! on last window still exits Vim.Tabs
Tab Creation
:tabnew/:tabedit create a new tab page. vim -p file1 file2 opens each file in its own tab at startup. Each tab page contains one or more windows — a tab is a layout, not a single file. Ctrl-w T moves the current window into a new tab page. Tabs are great for separating unrelated work contexts (e.g. one tab per project area).
:tabnew # new tab with empty buffer
:tabedit file.txt # new tab opening file
:tab split # new tab with current buffer in a split
:tabfind file.txt # new tab, find file in 'path'
# Open multiple files in tabs at startup:
vim -p file1.txt file2.txt file3.txt
# From inside Vim, open files in tabs:
:args *.py | argdo tabe %
# Or one-by-one:
:tabe file1.txt
:tabe file2.txt
# Keyboard shortcut (common mapping):
:nmap <C-t> :tabnew<CR>
# Open current buffer in a new tab (move window):
# Ctrl-w T (move current window to a new tab page)
# Each tab page has its own window layout
# (multiple splits/windows per tab allowed)Tab Navigation
gt/gT switch tabs (Normal mode). Ngt jumps directly to tab N (1-indexed). :tabs lists all tab pages and their windows. Common to map Ctrl-Tab/Ctrl-S-Tab or <leader>1/2/3 for fast tab switching. Tabs wrap around by default. Each tab maintains its own window layout (splits, etc.).
:tabnext / :tabn # next tab
:tabprevious / :tabp # previous tab
:tabfirst / :tabr # first tab
:tablast / :tabl # last tab
gt # next tab (Normal mode)
gT # previous tab
3gt # go to tab 3
:tabn 3 # go to tab 3 (Ex command)
# Common mappings (Vim default):
:nmap <C-Tab> :tabnext<CR>
:nmap <C-S-Tab> :tabprevious<CR>
# Or use leader keys:
:nmap <leader>1 1gt
:nmap <leader>2 2gt
:nmap <leader>3 3gt
# Cycle behavior:
# gt at last tab wraps to first
# gT at first tab wraps to last
# To disable wrap:
:set tabpagemax=50 # max tabs (not wrap-related)
# Show all tabs:
:tabs # list tabs with their windowsTab Operations
:tabc closes the current tab; :tabo closes all others. :tabm N moves the current tab to position N (0 = first). :tabdo runs an Ex command in every tab — useful for global operations across tab-separated workspaces. Closing a tab closes all its windows; modified buffers are preserved if 'hidden' is set.
:tabclose / :tabc # close current tab
:tabclose! / :tabc! # force close (discard changes)
:tabonly / :tabo # close all OTHER tabs
:tabonly! / :tabo! # force close others
# Reorder tabs:
:tabmove / :tabm 0 # move current tab to position 0 (first)
:tabm # move to last position
:tabm 2 # move to position 2
:tabm +1 # move right by 1
:tabm -1 # move left by 1
# Quick keyboard reordering (no default, common mapping):
:nmap <leader>tm :tabmove<Space>
# Run a command in every tab:
:tabdo cmd # run cmd in every tab page
# Example:
:tabdo %s/old/new/ge # substitute in every tab's first window
# Each tab can hold multiple windows/splits.
# 'Tabclose' closes all windows in the tab.Tab Layout & Multi-Window
Each tab page maintains an independent window layout (splits, sizes, positions). You can have multiple windows per tab. Ctrl-w T moves a window into its own new tab. :set showtabline=2 always shows the tab bar. Tabs are best used as workspace separators (e.g. one tab per project area), each with its own layout.
# Each tab page has its own window layout.
# Create a tab with a 3-window layout:
:tabnew # new tab
:vsplit # split vertically (2 windows)
:split # split horizontally (3 windows)
# Now tab has: [win1] [win2]
# [win3]
# Add a buffer to current tab:
:e file.txt # replaces current window's buffer
# Move a window to its own tab:
Ctrl-w T # window becomes a new tab page
# Show tab line (always):
:set showtabline=2 # 0=never, 1=multi-tab, 2=always
# Tab page-local options:
:setlocal ... # option applies only to current tab's windows
:tabdo setlocal ... # apply across all tabs
# Tab label customization (status line):
:set guitablabel=%t # show filename in GUI tab label
# Common use case: one tab per project area:
# Tab 1: source files
# Tab 2: tests
# Tab 3: docs
# Each with its own split layout.Tab Options & Labels
showtabline controls the tab bar (0=never, 1=only with multiple tabs, 2=always). You can fully customize the tabline with a function returning the format string — useful to show modified status, buffer names, tab numbers. guitablabel/guitabtooltip customize GUI Vim. tabs are numbered from 1.
:set showtabline=2 # always show tab bar (0=never, 1=auto)
:set tabpagemax=50 # max tabs creatable
:set hidden # allow switching tabs with unsaved changes
# Custom tab labels (terminal Vim):
:set tabline=%!MyTabLine()
function! MyTabLine()
let s = ''
for i in range(tabpagenr('$'))
let n = i + 1
let s .= '%' . n . 'T'
let s .= (n == tabpagenr() ? '%#TabLineSel#' : '%#TabLine#')
let s .= ' [' . n . '] '
let buflist = tabpagebuflist(n)
let bufname = bufname(buflist[0])
let s .= empty(bufname) ? '[No Name]' : fnamemodify(bufname, ':t')
let s .= ' '
endfor
let s .= '%T%#TabLineFill#'
return s
endfunction
# GUI tab labels:
:set guitablabel=%t # show filename
:set guitabtooltip=%f # show full path on hover
# Quickly switch with number:
:nmap <leader>t :tabs<CR>Tab Page Commands
:tabs lists tabs and their windows. :tabdo runs an Ex command in every tab. :tab ball opens each buffer in its own tab. <C-w>gf opens the file under the cursor in a new tab. Use tabs to group related files (one tab per feature/area). For cross-tab operations, :tabdo is the workhorse — pair with windo for multi-window tabs.
:tabs # list all tabs and their windows
:tabdo cmd # run cmd in every tab
:tab ball # open each buffer in its own tab
:tab help subject # open help in a new tab
:tab split # split current window into new tab
# Quickly duplicate a tab's layout:
# (no built-in, but possible with sessions)
# Or manually: create a new tab, re-open buffers
# Open file under cursor in a new tab:
:nmap gf :tabe <cfile><CR>
# (or use the built-in <C-w>gf)
# Common workflow:
# 1. Group related files in tabs
# 2. Use gt/gT to switch
# 3. Use :tabdo for cross-tab operations
# Save all tabs' buffers:
:tabdo windo w # write every window in every tab
# (or simpler)
:wall # write all buffers (across all tabs)
# Close all tabs but keep buffers:
# (no single command — use :tabo + :b then :bd)Configuration (.vimrc)
vimrc Basics
$MYVIMRC is the path to your vimrc file — :edit $MYVIMRC opens it, :source $MYVIMRC reloads after edits. The runtimepath controls where Vim looks for syntax files, plugins, etc. Use :verbose set option? to see where an option was last set — invaluable for debugging conflicting plugins.
# vimrc location:
# Unix: ~/.vimrc (or ~/.vim/vimrc)
# Windows: ~/_vimrc (or ~/vimfiles/vimrc)
# Neovim: ~/.config/nvim/init.vim
# Reload vimrc after editing:
:source $MYVIMRC # or :so %
# Edit vimrc quickly:
:edit $MYVIMRC # $MYVIMRC = path to your vimrc
# Runtime path (where Vim looks for scripts):
:set runtimepath?
# Add a custom path:
:set runtimepath+=~/.vim/custom
# Conditional config (only in terminal Vim):
if !has('gui_running')
set mouse=a
endif
# Per-filetype settings (in vimrc or ftplugin/):
:autocmd FileType python setlocal expandtab shiftwidth=4
# Check where an option was set:
:verbose set tabstop?Basic Settings
These are common quality-of-life settings. relativenumber + number shows hybrid numbering — line numbers relative to cursor for easy jumps, with absolute on the current line. showmatch flashes matching brackets. wildmenu enhances :command completion with a menu. scrolloff keeps context visible around the cursor — never edit at the screen edge.
set nocompatible # Vim mode (not Vi)
set number # show line numbers
set relativenumber # relative line numbers (great for jumps)
set cursorline # highlight current line
set cursorcolumn # highlight current column
set showcmd # show partial commands in status
set showmode # show current mode (insert, visual...)
set showmatch # highlight matching brackets
set wildmenu # better command-line completion
set wildmode=longest:full,full # completion style
set laststatus=2 # always show status line
set ruler # show cursor position
set backspace=indent,eol,start # backspace through everything
set encoding=utf-8 # default encoding
set scrolloff=5 # keep 5 lines above/below cursor
set sidescrolloff=5 # keep 5 cols left/right of cursor
set mouse=a # enable mouse (all modes)Indentation
tabstop controls how many columns a tab character displays as. shiftwidth is the indent amount used by <<, >>, and autoindent. softtabstop makes Backspace remove the right number of columns even when expandtab is off. expandtab converts tabs to spaces — recommended for consistency. :retab converts existing tabs/spaces after changing settings.
set autoindent # copy indent from previous line
set smartindent # smart auto-indent for code (basic)
set cindent # C-style indenting (more language-aware)
set expandtab # use spaces instead of tabs
set tabstop=4 # tab key inserts 4-column tabs visually
set shiftwidth=4 # indent/outdent amount (<<, >>, autoindent)
set softtabstop=4 # backspace removes 4 cols even with tabs
set shiftround # round indent to multiple of shiftwidth
set smarttab # tab key respects shiftwidth at line start
# Toggle tabs vs spaces:
:set expandtab! # toggle
:retab # convert existing tabs<->spaces
# Filetype-specific indent:
:filetype plugin indent on
# Then in ~/.vim/ftplugin/python.vim:
# setlocal expandtab shiftwidth=4
# Quick indent/dedent in Normal mode:
>> # indent current line
<< # dedent current line
5>> # indent 5 linesSearch Settings
ignorecase + smartcase is the recommended combo: case-insensitive unless your pattern has an uppercase letter. hlsearch highlights all matches; incsearch shows matches incrementally as you type. Common mapping: <leader>h toggles hlsearch, or :noh clears the current highlight. Vim 8.2+ can show search count via shortmess.
set ignorecase # case-insensitive search
set smartcase # case-sensitive if pattern has uppercase
set hlsearch # highlight all matches (hls)
set incsearch # show match as you type (is)
set wrapscan # wrap search around end of file (ws)
# Toggle search highlight:
:nmap <leader>h :set hlsearch!<CR>
# Or clear current highlight:
:nmap <leader>nh :nohlsearch<CR>
# Show search count (Vim 8.2+):
:set shortmess-=S
:set shortmess+=S # search stats in command line
# Search in visual selection:
:vmap * y/\V<C-R>"<CR>
# Use very-magic mode by default (advanced):
# No built-in, but a search mapping can prepend \v
# Improved '*' (don't jump on first press):
:nmap * *N
# Disable highlighting until next search:
:nohlsearchDisplay Settings
wrap + linebreak wrap long lines at word boundaries (not mid-word) for readability. list + listchars reveal tabs, trailing whitespace, and non-breaking spaces. colorcolumn marks a column (e.g. 80 for line length limits). termguicolors enables true 24-bit color in modern terminals. splitright/splitbelow control where new splits appear.
set wrap # wrap long lines
set linebreak # wrap at word boundaries (not mid-word)
set textwidth=80 # auto-wrap at 80 chars when typing
set showbreak=+++ # marker for wrapped lines
set list # show invisible chars
set listchars=tab:>-,trail:.,extends:>,precedes:<
set fillchars=fold:-,vert:\| # chars for folds/vsplits
set colorcolumn=80 # highlight column 80
set signcolumn=yes # always show sign column (LSP)
set conceallevel=0 # don't conceal chars
set splitright # new vsplit on the right
set splitbelow # new split below
set termguicolors # 24-bit color (if terminal supports)
set background=dark # or 'light' — adjusts color scheme
# Color scheme:
:colorscheme desert
# Or with a plugin (e.g. vim-colors-solarized):
:colorscheme solarizedMapping Keys
ALWAYS use the *noremap variants (nnoremap, vnoremap, etc.) for personal mappings — they don't recurse, avoiding unexpected behavior. <leader> is a customizable prefix key (default backslash; many set it to comma or space). <CR> is Enter, <Esc> is escape, <C-x> is Ctrl-X. Map leader shortcuts for common commands like save/quit.
# Map families:
:map # normal, visual, select, operator-pending
:nmap # normal mode only
:imap # insert mode only
:vmap # visual + select mode
:xmap # visual mode only
:smap # select mode only
:omap # operator-pending mode only
:cmap # command-line mode only
:tmap # terminal mode only
# Non-recursive (RECOMMENDED):
:nnoremap # normal mode, non-recursive
:vnoremap # visual mode, non-recursive
:inoremap # insert mode, non-recursive
:cnoremap # command-line mode, non-recursive
# Examples:
nnoremap <leader>w :w<CR> # save with leader-w
nnoremap <leader>q :q<CR> # quit
inoremap jk <Esc> # Esc with jk
nnoremap <C-h> <C-w>h # window nav
# Leader key (default is \):
:let mapleader = ','
:let maplocalleader = '\\'
# Special keys:
# <CR> <Esc> <Tab> <Space> <C-x> <A-x> <M-x> <F1>-<F12>
# <Up> <Down> <Left> <Right> <leader> <localleader>Plugins (vim-plug)
vim-plug Basics
vim-plug is the most popular modern plugin manager. Declare plugins between call plug#begin and plug#end. Each Plug line specifies a GitHub repo (user/repo). After adding plugins, :source $MYVIMRC then :PlugInstall downloads them. You can pin to a tag, branch, or commit for stability. Plugins install to the 'plugged' directory.
# Installation (Unix):
# curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
# https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
# In vimrc:
call plug#begin('~/.vim/plugged')
Plug 'junegunn/fzf.vim' " GitHub repo: user/repo
Plug 'tpope/vim-surround' " another plugin
Plug 'morhetz/gruvbox' " colorscheme
Plug 'preservim/nerdtree' " file explorer
call plug#end()
# After editing vimrc:
:source $MYVIMRC # or restart Vim
:PlugInstall # install all declared plugins
# Plugin directory structure:
# ~/.vim/plugged/<plugin-name>/
# Pin to a specific commit/tag/branch:
Plug 'tpope/vim-fugitive', { 'tag': 'v3.0' }
Plug 'user/repo', { 'branch': 'dev' }
Plug 'user/repo', { 'commit': 'abc123' }Plugin Management Commands
:PlugInstall installs new plugins, :PlugUpdate updates all, :PlugClean removes unlisted ones, :PlugStatus shows their state. The 'do' hook runs a command after install — useful for plugins that need to build or install binaries. The 'on'/'for' options enable lazy loading (load only when a command/filetype triggers it) — improves startup time.
:PlugInstall # install newly added plugins
:PlugUpdate # update all plugins
:PlugClean # remove plugins no longer in vimrc
:PlugStatus # show plugin status (loaded, errored)
:PlugUpgrade # update vim-plug itself
:PlugDiff # review changes from last update
:PlugSnapshot # generate a snapshot of plugin versions
# Post-install hooks:
Plug 'user/repo', { 'do': ':GoInstallBinaries' }
Plug 'user/repo', { 'do': './install.sh' }
Plug 'user/repo', { 'do': 'npm install' }
# Lazy loading (load only when needed):
Plug 'user/repo', { 'on': 'CommandName' } " on command
Plug 'user/repo', { 'for': 'python' } " for filetype
Plug 'user/repo', { 'on': ['Cmd1', 'Cmd2'] }
# Frozen plugin (don't update):
Plug 'user/repo', { 'frozen': 1 }
# Disable a plugin temporarily:
# (no built-in, but comment out the Plug line and :PlugClean)Common Essential Plugins
Common essentials: fzf for fuzzy finding files/buffers/text (much faster than :find), vim-surround for editing quotes/brackets/tags around text, vim-fugitive for Git integration, vim-commentary for fast commenting (gc operator), NERDTree for a file-explorer sidebar, vim-airline for a nicer status line, and a good colorscheme like gruvbox.
" Fuzzy finder (requires fzf binary):
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'
" :Files :GFiles :Buffers :Rg
" Surround (cs/ds/ys for surroundings):
Plug 'tpope/vim-surround'
" cs"' change " to '
" ds" delete surrounding "
" ysiw) surround word with ()
" Fugitive (Git integration):
Plug 'tpope/vim-fugitive'
" :G :Gstatus :Gcommit :Gpush
" Commentary (gc to toggle comments):
Plug 'tpope/vim-commentary'
" gcc comment line
" gc{motion} comment motion
" NERDTree (file explorer sidebar):
Plug 'preservim/nerdtree'
" :NERDTree :NERDTreeToggle
" Airline (status line):
Plug 'vim-airline/vim-airline'
" Gruvbox (colorscheme):
Plug 'morhetz/gruvbox'Plugin Configuration
Use 'on'/'for' to lazy-load plugins — they only load when triggered, improving startup time. Plugin-specific config typically uses g:pluginname_var variables set in vimrc, OR files in ~/.vim/after/plugin/. The 'do' hook runs after install/update for build steps. Setting g:loaded_pluginname=1 in vimrc prevents a plugin from loading at all.
" Lazy load on command:
Plug 'tpope/vim-fugitive', { 'on': 'Git' }
" Lazy load on filetype:
Plug 'fatih/vim-go', { 'for': 'go' }
" Multiple triggers:
Plug 'junegunn/fzf.vim', { 'on': ['Files', 'Buffers', 'Rg'] }
" Post-install hook:
Plug 'dense-analysis/ale', { 'do': 'npm install' }
" Configuration AFTER plugin loads:
Plug 'preservim/nerdtree'
let g:NERDTreeShowHidden = 1
let g:NERDTreeWinSize = 30
nmap <leader>n :NERDTreeToggle<CR>
" Configuration in AFTER/plugin-name.vim:
" (alternative to setting g: vars in vimrc)
" ~/.vim/after/plugin/nerdtree.vim:
" let g:NERDTreeShowHidden = 1
" Disable plugin entirely:
let g:loaded_nerdtree = 1 " prevent loadingPlugin Loading & Runtime
Vim sources plugin/*.vim files at startup in directory order, then after/plugin/*.vim last (for overrides). :scriptnames lists everything sourced — invaluable for debugging. :verbose map <key> shows where a mapping was defined; :verbose set option? does the same for options. autoload/ files load lazily when their functions are first called.
" Plugin loading sequence:
" 1. Vim starts, reads vimrc
" 2. plug#begin / plug#end registers plugins
" 3. Runtime path is updated to include plugin dirs
" 4. plugin/*.vim files are sourced (in directory order)
" 5. after/plugin/*.vim files are sourced last
" Check if a plugin is loaded:
:echo exists('g:loaded_fugitive')
:scriptnames # list all sourced scripts in order
" Manually source a plugin:
:source ~/.vim/plugged/foo/plugin/foo.vim
" Find where a mapping was defined:
:verbose map <leader>w
" Find where an option was set:
:verbose set formatprg?
" Plugin runtime files:
" plugin/ - loaded at startup
" ftplugin/ - loaded for specific filetypes
" syntax/ - syntax highlighting
" indent/ - indentation rules
" autoload/ - lazily-loaded functions
" after/ - sourced last (overrides)
" Helptags for plugin docs:
:Helptags (vim-plug generates these on install)Alternative Plugin Managers
Beyond vim-plug, options include Vundle (similar API, less feature-rich), Pathogen (simplest — just adds bundle dirs to runtimepath), Dein.vim (fast, lazy-loading focused), and Vim 8+'s built-in packages (no plugin manager needed — drop plugins in pack/<name>/start/). For Neovim users, Packer (Lua-based) is popular. Built-in packages are great for minimalists.
" 1. Vundle (older, similar to vim-plug):
" set rtp+=~/.vim/bundle/Vundle.vim
" call vundle#begin()
" Plugin 'tpope/vim-surround'
" call vundle#end()
" 2. Pathogen (just adds dirs to runtimepath):
" Execute:
" git clone https://github.com/tpope/vim-pathogen ~/.vim/bundle
" In vimrc:
" execute pathogen#infect()
" Then clone plugins into ~/.vim/bundle/
" 3. Dein.vim (modern, lazy-loading focused):
" set runtimepath+=~/.cache/dein/repos/github.com/Shougo/dein.vim
" if dein#load_state('~/.cache/dein')
" call dein#begin('~/.cache/dein')
" call dein#add('tpope/vim-surround')
" call dein#end()
" call dein#save_state()
" endif
" 4. Vim 8+ built-in packages (NO plugin manager needed):
" Place plugins in:
" ~/.vim/pack/<name>/start/<plugin>/ " loaded at startup
" ~/.vim/pack/<name>/opt/<plugin>/ " loaded on demand
" Load an opt plugin: :packadd <plugin>
" 5. Packer (Neovim Lua):
" require('packer').startup(function()
" use 'tpope/vim-surround'
" end)Advanced Movement
Character Search (f/F/t/T)
f/F find the next/previous character on the current line (cursor lands on it); t/T move until just before/after it. ; repeats the last f/F/t/T in the same direction, , in the opposite. Combine with operators: dt, deletes up to (not including) the next comma, df. deletes through the next period. Fast and precise for in-line navigation.
f{x} # find next char 'x' on line (cursor ON x)
F{x} # find previous char 'x' (cursor ON x)
t{x} # till next char 'x' (cursor BEFORE x)
T{x} # till previous char 'x' (cursor AFTER x)
; # repeat last f/F/t/T (same direction)
, # repeat last f/F/t/T (opposite direction)
# Examples (cursor on 'a' in "hello world abc"):
# fw -> cursor on 'w' (world)
# tw -> cursor on space before 'w'
# Fo -> cursor on 'o' (hello) - backwards
# ; -> next 'w'... or next match of last
# Combined with operators:
dt, # delete up to (not including) next ','
df. # delete up to AND including next '.'
ct; # change up to next ';'
yt: # yank up to next ':'
# Find on a long line (no built-in multi-line f):
# Use :s or search for multi-line patterns.
# Set 'hlsearch' to highlight f/F/t/T targets:
:set hlsearch " doesn't highlight f, but / does.Sentence & Paragraph Movement
( and ) move by sentence (sentences end with . ! ? plus whitespace); { and } move by paragraph (blank-line separated). For wrapped lines, gj/gk/g0/g$ operate on visual screen lines while j/k/0/$ operate on logical lines — essential when 'wrap' is on. Combine ( ) { } with operators: das deletes a sentence, yap yanks a paragraph.
( # previous sentence start
) # next sentence start
{ # previous paragraph start (blank-line delimited)
} # next paragraph start
# Sentence boundaries: ends with . ! ? followed by
# whitespace or end of paragraph.
# Paragraph: block of lines separated by blank lines.
# Examples:
# Hello world. This is a sentence.
# ^ ^ ^
# ( ) ( moves to sentence starts
# Combine with operators:
das # delete a sentence
cis # change inside sentence
yap # yank a paragraph
dip # delete inside paragraph
# Move by visual line (with 'wrap' on):
gj # down one screen line (wrap-aware)
gk # up one screen line
g0 # start of screen line
g$ # end of screen line
g_ # last non-blank of screen line
# These respect wrapping where j/k don't.Match Pairs (%)
% jumps between matching brackets (default: (), [], {}). It also works on #if/#endif and /* */ if 'matchpairs' is set appropriately. Combine with operators: d% deletes from cursor to the match, v% selects it. [( [{ ]) ]} jump to unmatched brackets — useful for finding the enclosing function/class block.
% # jump to matching bracket: ( ) [ ] { }
# also works on #if/#endif, /* */ with matching pairs
# In Normal mode:
% # if cursor on (, jumps to matching )
% # if cursor on ), jumps to matching (
# Combine with operators:
d% # delete from cursor to matching bracket
c% # change from cursor to matching bracket
y% # yank from cursor to matching bracket
v% # visually select from cursor to matching bracket
# Extend matching to other pairs:
:set matchpairs+=<:>
:set matchpairs+=«:»
# Default: (:),[:],{:}
# Move to enclosing bracket:
# [( # back to unmatched (
# ]) # forward to unmatched )
# [{ # back to unmatched {
# ]} # forward to unmatched }
# [} / ]{ similar for braces
# Example: jump to outer function's opening brace:
[{ " previous unmatched {Screen Movement
H/M/L jump to the top/middle/bottom of the visible screen. Ctrl-f/b scroll full pages; Ctrl-d/u half pages (less disorienting); Ctrl-e/y scroll one line without moving the cursor. zz/zt/zb reposition the cursor line to the middle/top/bottom of the screen — essential for keeping context. 'scrolloff' ensures the cursor never reaches the screen edge.
H # top of screen (High) — first non-blank
M # middle of screen
L # bottom of screen (Low) — first non-blank
N H # Nth line from top (e.g. 5H = 5 lines from top)
N L # Nth line from bottom
# Scrolling:
Ctrl-f # forward (down) full screen
Ctrl-b # backward (up) full screen
Ctrl-d # down half screen
Ctrl-u # up half screen
Ctrl-e # scroll down 1 line (cursor stays)
Ctrl-y # scroll up 1 line (cursor stays)
# Reposition cursor on screen:
zz # cursor line to middle of screen
zt # cursor line to top of screen
zb # cursor line to bottom of screen
z. # cursor to middle, first non-blank
z+ # cursor to bottom, scroll up (like zb but accepts count)
# Keep cursor off edges:
:set scrolloff=5 # always 5 lines above/below cursor
:set sidescrolloff=5 # 5 cols left/right of cursorScroll Bind & Sync
:set scrollbind synchronizes scrolling across windows — set it in each window you want to sync. :vimdiff opens two files in diff mode with scrollbind on, syntax-highlighted differences, and [c/]c to navigate hunks. do (diff obtain) pulls changes from the other window, dp (diff put) pushes them. Great for comparing versions of a file.
# Bind windows to scroll together:
:set scrollbind # current window syncs scroll
:set syncbind # force immediate sync
:scrollbind # same as :set scrollbind
# In each window you want synced:
# :set scrollbind
# Unbind:
:set noscrollbind
# Common use: diff two files side by side
:vimdiff file1 file2 # opens both, scrollbound, diff highlighted
:diffthis # mark current window as a diff
:diffoff # turn off diff mode for current window
:diffupdate # refresh diff highlighting
# Vertical diff split:
:vert diffsplit file2.txt
# In diff mode:
[c # previous diff hunk
]c # next diff hunk
do # diff obtain (pull from other window)
dp # diff put (push to other window)
# Bind but at different starting positions:
# Set scrollbind AFTER scrolling to desired positions.Going to Positions
Many ways to position the cursor: :N for line N, N% for percentage, `.`/`"`/[`]/`</`> for special marks (last change, last exit, last yank, last visual). Ctrl-o/Ctrl-i navigate the jumplist. `" restores the position when the file was last closed — combined with a viminfo setting, this persists across sessions.
:N # go to line N (e.g. :42)
Ng / N G # go to line N (e.g. 42G or 42gg)
gg # first line
G # last line
N% # go to N% of file (e.g. 50% = middle)
:-N # N lines before current (e.g. :-5)
:+N # N lines after current (e.g. :+10)
# Marks and jumps:
\`. # position of last change in this buffer
\`" # position when file was last closed
\`[ # start of last yanked/changed region
\`] # end of last yanked/changed region
\`< # start of last visual selection
\`> # end of last visual selection
\`^ # last insert position (gi goes here + insert)
# Recent jumps:
Ctrl-o # back in jumplist
Ctrl-i # forward in jumplist
\`' # position before last jump (where you came from)
# File position on save:
:set viminfo^=% # remember buffer positions across sessions
# Cursor hold (no movement for 'updatetime' ms):
# CursorHold event triggers (advanced)Command-Line Mode
Range Commands
Ranges specify which lines a command affects. :% is the whole file (1,$). . is current line, $ is last line. '<,'> is the last visual selection (auto-filled when you press : in visual mode). 'a,'b is from mark a to mark b. You can also use search patterns as range boundaries: :/foo/,/bar/d deletes from line matching foo to line matching bar.
# Range syntax: :{start},{end}command
# Range forms:
:% # whole file (1,$)
:1,10 # lines 1-10
:.,$ # current line to end of file
:1,. # first line to current
:'a,'b # from mark a to mark b
:'<,'> # last visual selection
:. # current line only
:N # line N only
:.,+5 # current line + 5 below
:.,-2 # current line - 2 above
# Examples:
:1,10d # delete lines 1-10
:%s/old/new/g # substitute whole file
:'<,'>s/old/new/g # substitute visual selection
:.,$j # join from current to end into one line
:5,10y a # yank lines 5-10 into register a
:g/pattern/m$ # move matching lines to end of file
# Search-based range:
:/start/,/end/d # delete from 'start' to 'end' patternsFile Commands
:w saves, :e opens a new file, :r inserts another file's content (or shell command output via :r !cmd). :saveas saves and switches to the new file. :e! reloads the file discarding changes. ++enc/++ff specify encoding/line-endings explicitly — useful for converting files. :browse e opens a GUI file dialog (in GUI Vim).
:w / :write # save current file
:w file.txt # save as new file
:w! file.txt # overwrite existing file
:w >> file.txt # append to file
:saveas / :sav file # save as new file and switch to it
:e / :edit file # open file (replace current buffer)
:e! # reload current file (discard changes)
:e # # edit alternate file
:r / :read file # insert file below cursor
:r !command # insert command output below cursor
:0r !date # insert date at top of file
# Multiple files:
:wa / :wall # write all buffers
:xa / :xall # write all and quit
:qa! # quit all without saving
# Browse file system:
:browse e # GUI file open dialog
:Explore # Netrw file browser
# File encoding:
:w ++enc=utf-8 # save as UTF-8
:e ++enc=latin1 file # open with specific encoding
# File format (line endings):
:w ++ff=unix # save as Unix line endingsBuffer & Window Commands
All multi-buffer/window/tab operations have Ex-command forms. :b switches buffers, :sp/:vs split windows, :tabnew creates tabs. The :*do family (bufdo, argdo, tabdo, windo, cdo) runs a command across many targets — extremely powerful for batch operations. :ls shows buffers; :tabs shows tabs; :args shows the arglist.
# Buffer commands:
:b N / :buffer N # switch to buffer N
:b name # switch by name (tab-completes)
:bd / :bdelete # delete buffer
:bw / :bwipeout # wipe buffer (clears marks)
:ls / :buffers # list buffers
:bn / :bp # next / previous buffer
:b# # alternate buffer
# Window commands:
:sp / :split # horizontal split
:vs / :vsplit # vertical split
:new # split with empty buffer
:close # close window
:only # close all other windows
:qa / :qa! # quit all windows
# Tab page commands:
:tabnew # new tab
:tabe file # new tab with file
:tabc # close tab
:tabo # close other tabs
:tabs # list tabs
:tabn / :tabp # next / previous tab
# Argument list commands:
:args # show arg list
:n / :next # next arg file
:prev # previous arg file
:argdo cmd # run cmd on each arg fileSetting Options
:set toggles boolean options with !, sets values with =, appends/removes with +=/-=, and resets to defaults with &. :set option? shows the current value. :setlocal sets a buffer/window-local value (doesn't affect other buffers). :set with no args lists all changed options; :set all shows everything. Tab completes option names.
:set option # turn boolean option on
:set nooption # turn boolean option off
:set option! # toggle boolean
:set option? # show current value
:set option=value # set value
:set option+=value # append to value
:set option-=value # remove from value
:set option& # reset to default
:set all # show all options
:set # show changed options
# Local vs global:
:setlocal option # buffer/window-local only
:setglobal option # global value
# Examples:
:set number # show line numbers
:set nonumber # hide line numbers
:set number! # toggle
:set tabstop=4 # set tabstop to 4
:set formatoptions+=r # add 'r' to formatoptions
:set ignorecase& # reset ignorecase to default
# View all set options:
:set # shows only non-default values
# Find option with substring:
:set ic<tab> # tab-complete (ignorecase)Map Commands
Use the *noremap variants for personal mappings — they don't recurse, preventing unexpected behavior. <leader> is a customizable prefix (default backslash; many set it to space or comma). <buffer> makes a mapping local to the current buffer. Special keys use <notation> like <CR>, <Esc>, <C-x> (Ctrl-X), <A-x> (Alt-X).
:map <key> <action> # generic map (most modes)
:nmap <key> <action> # normal mode
:imap <key> <action> # insert mode
:vmap <key> <action> # visual mode
:xmap <key> <action> # visual mode only
:smap <key> <action> # select mode
:omap <key> <action> # operator-pending mode
:cmap <key> <action> # command-line mode
:tmap <key> <action> # terminal mode
# Non-recursive (RECOMMENDED for personal mappings):
:nnoremap <key> <action> # normal mode
:vnoremap <key> <action> # visual mode
:inoremap <key> <action> # insert mode
:onoremap <key> <action> # operator-pending
:cnoremap <key> <action> # command-line
:tnoremap <key> <action> # terminal
# Buffer-local mappings:
:nnoremap <buffer> <key> <action>
# Clear a mapping:
:nunmap <key> # or :nmapclear (clears all normal maps)
# Special keys in mappings:
# <CR> <Esc> <Tab> <Space> <BS> <C-x> <A-x> <M-x>
# <F1>-<F12> <Up> <Down> <Left> <Right>
# <leader> <localleader> <C-r> <C-w>
:let mapleader = ' '
:let maplocalleader = '\\'Useful Ex Commands
:!cmd runs a shell command; :r !cmd inserts its output into the buffer. :sort sorts lines (u=unique, n=numeric, !=reverse). :retab converts between tabs and spaces. :normal runs normal-mode keystrokes as Ex — add ! to ignore custom mappings. :%!cmd pipes the whole file through an external command and replaces it with the output.
:!command # run shell command
:r !command # insert command output below cursor
:!! # repeat last :! command
:sh # open a shell (exit to return)
:terminal / :term # open terminal in a window
:read file # insert file contents below cursor
:sort # sort selected lines (visual mode)
:sort u # sort and remove duplicates
:sort n # numeric sort
:sort! # reverse sort
:retab # convert tabs to spaces (and vice versa)
:retab! # also convert in strings
:normal {cmds} # run normal-mode commands
:normal! {cmds} # ignore mappings (use raw keys)
# Execute Ex commands from a register:
:@a # run Ex commands in register 'a'
:@@ # repeat last :@
# Range + command examples:
:5,20s/old/new/g # substitute in lines 5-20
:%!sort # pipe whole file through sort
:'<,'>!column -t # format visual selection as columns
# Help:
:help :command # help on a specific commandVimscript Basics
Variables
Vimscript uses let for assignment and unlet to delete. Variables have scopes indicated by prefixes: g: (global), b: (buffer), w: (window), t: (tab), s: (script), l: (function-local), v: (built-in). const creates immutable values (Vim 8+). exists() checks if a variable is defined. Vim 8.2+ supports string interpolation with $"...{expr}...".
let var = value # assignment
let var = "hello" # string
let var = 42 # number
let var = 3.14 # float
let var = [1, 2, 3] # list
let var = {"key": "val"} # dictionary
unlet var # delete a variable
# Variable scopes (prefix with letter + colon):
let g:global_var = 1 # global (everywhere)
let b:buffer_var = 1 # buffer-local
let w:window_var = 1 # window-local
let t:tab_var = 1 # tab-local
let s:script_var = 1 # script-local (this file only)
let l:local_var = 1 # function-local
let v:count # built-in Vim variable
# Constants (Vim 8+):
const PI = 3.14159
let g:CONFIG = #{name: "vim"} # locked dictionary
# Check if a variable exists:
if exists('var')
if exists('g:plugin_var')
# String interpolation (Vim 8.2+):
let name = "World"
echo $"Hello, {name}!"Functions
function! Name(args) ... endfunction defines a function — the ! allows redefinition. a: accesses arguments; a:000 is the list of variadic args. Default arguments (Vim 8+) use =value. Lambdas (Vim 8+) are {args -> expr}. function('Name') returns a funcref you can pass around. Use :call to invoke a function for its side effects.
# Define a function:
function! MyFunc(arg1, arg2)
echo a:arg1 . ' ' . a:arg2
return a:arg1 + a:arg2
endfunction
# The ! in function! overrides an existing function
# (without ! Vim errors if function already exists).
# Call a function:
call MyFunc("hello", "world")
let result = MyFunc(1, 2)
# Variadic arguments (...):
function! Sum(...)
let total = 0
for i in a:000
let total += i
endfor
return total
endfunction
# Default arguments (Vim 8+):
function! Greet(name, greeting="Hello")
echo a:greeting . ', ' . a:name
endfunction
# Lambda / anonymous functions (Vim 8+):
let Square = {x -> x * x}
echo Square(5) " 25
# Function as a value:
let Fn = function('MyFunc')
echo Fn(1, 2)Conditionals & Loops
if/elseif/else/endif for conditionals. for loops over a list (range(N) generates 0..N-1), and can unpack [key, val] pairs from items(). while/endwhile for condition loops. break/continue work as expected. Common condition checks: has() for features, exists() for variables/commands, filereadable() for files, empty() for empty values, type() for type comparison.
# Conditionals:
if x > 10
echo "big"
elseif x > 5
echo "medium"
else
echo "small"
endif
# Ternary:
let result = (x > 0) ? "positive" : "non-positive"
# Loops:
for i in range(5) # 0,1,2,3,4
echo i
endfor
for [key, val] in items({'a': 1, 'b': 2})
echo key . '=' . val
endfor
while x > 0
let x -= 1
endwhile
# Loop control:
break # exit loop
continue # skip to next iteration
# Match-related conditionals:
if has('python3') # feature check
if exists(':Command') # command exists
if filereadable('file') # file is readable
if empty(var) # variable is empty
if type(var) == type([]) # type checkStrings
. concatenates strings. Vim 8.2+ adds $"...{expr}..." interpolation. len/strlen give byte length; strcharlen gives character count (important for multi-byte). split/join convert between strings and lists. substitute() is the programmatic :s command. matchstr() extracts a regex match. =~ is the regex match operator.
# String concatenation:
let s = "Hello" . " " . "World"
# String interpolation (Vim 8.2+):
let name = "Vim"
echo $"Welcome to {name}!"
# Common functions:
len("hello") # 5
strlen("hello") # 5 (byte length)
strcharlen("hello") # 5 (char length)
tolower("HELLO") # "hello"
toupper("hello") # "HELLO"
strpart("hello", 1, 3) # "ell" (substring)
matchstr("foo123bar", '\d\+') # "123"
substitute("foo", 'o', '0', 'g') # "f00"
split("a,b,c", ',') # ['a', 'b', 'c']
join(['a', 'b', 'c'], '-') # "a-b-c"
trim(" hello ") # "hello"
# printf-style formatting:
printf("%d + %d = %d", 1, 2, 3) # "1 + 2 = 3"
printf("%05.2f", 3.14159) # "03.14"
# Regex matching:
if "hello" =~ '^h'
echo "starts with h"
endifLists & Dictionaries
Lists are ordered (like arrays); dictionaries are key-value maps. List slicing uses [start:end]. add/insert/remove modify lists in place. sort/reverse also mutate. For dicts, both .key and ["key"] access work; keys()/values()/items() return lists. has_key() checks membership. #{...} (Vim 8+) is a cleaner dict literal that doesn't require quoting keys.
# Lists (like arrays):
let nums = [1, 2, 3, 4, 5]
echo nums[0] # 1 (first)
echo nums[-1] # 5 (last)
echo nums[1:3] # [2, 3, 4] (slice)
# List operations:
call add(nums, 6) # append 6
call insert(nums, 0, 0) # insert 0 at index 0
call remove(nums, 0) # remove index 0
echo len(nums) # length
call sort(nums) # sort in place
call reverse(nums) # reverse in place
call extend(nums, [7, 8]) # concatenate
# Iterate:
for n in nums
echo n
endfor
# Dictionaries (like hash maps / objects):
let d = {"name": "Vim", "version": 9}
echo d.name # "Vim"
echo d["version"] # 9
let d.lang = "Vimscript" # add a key
call remove(d, "version") # remove a key
echo keys(d) # ['name', 'lang']
echo values(d) # ['Vim', 'Vimscript']
echo items(d) # [['name', 'Vim'], ['lang', 'Vimscript']]
echo has_key(d, "name") # 1 (true)
# Dictionary literal (Vim 8+, cleaner):
let d2 = #{key: "value", count: 42}Autocmds & Custom Commands
autocmd runs code on Vim events (BufRead, BufWritePre, FileType, VimEnter, CursorMoved, etc.). Always wrap autocmds in augroup with autocmd! to clear old definitions on reload - prevents duplication. Custom commands use :command! -nargs=N -range -bang Name action. <args>, <line1>, <line2>, <bang> access the user's input in the command definition.
# Autocmds: run code on events
:autocmd BufWritePre *.md :%s/\s\+$//e " trim trailing whitespace
# Group autocmds (recommended - prevents duplicates on reload):
augroup markdown_settings
autocmd!
autocmd BufRead,BufNewFile *.md setlocal spell
autocmd FileType markdown setlocal textwidth=80
autocmd BufWritePre *.md :%s/\s\+$//e
augroup END
# Common events:
# BufReadPost - after loading a file
# BufWritePre - before saving
# BufWritePost - after saving
# FileType - when filetype is set
# VimEnter - after Vim startup
# CursorMoved - cursor moved in normal mode
# InsertEnter - entering insert mode
# Custom Ex commands:
command! -nargs=1 Grep !grep <args> %
command! -range Sum echo <line1> + <line2>
command! -bang Q quit<bang>
# Use them:
:Grep pattern
:Sum
# Show all autocmds:
:autocmd
# Show autocmds for a specific event:
:autocmd BufWritePreRelated Vim snippets
Copy-paste ready code for common tasks.
Buffers, Windows & Tabs
Edit multiple files with buffers, split windows, and tab pages.
Search & Substitute
Find text with / and replace with :s, leveraging regex and ranges.
Registers (Multi-Clipboard)
Store yanks/deletes in named registers and paste from them.
Marks (Bookmarks)
Jump back to positions in a file or across files with marks.
Macros (Recorded Keystrokes)
Record a sequence of keys and replay it to automate repetitive edits.
Folding (Collapse Code)
Hide regions of code to focus on structure with fold methods.
Plugin Managers & Ecosystem
Install, configure and discover plugins with vim-plug / packer / lazy.
LSP, Completion & Diagnostics
Get IDE features (autocomplete, go-to-definition, diagnostics) via LSP.
Was this helpful?