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 BufWritePreVerwandte Vim-Snippets
Copy-paste ready code for common tasks.
Buffer, Windows & Tabs
Mehrere Dateien mit Buffern, geteilten Windows und Tabs bearbeiten.
Suchen & Ersetzen
Text mit / finden und mit :s ersetzen, mit Regex und Ranges.
Register (Multi-Clipboard)
Yanks/Deletes in benannten Registern speichern und daraus einfügen.
Marks (Lesezeichen)
Zu Positionen in einer Datei oder zwischen Dateien mit Marks zurückspringen.
Makros (Tastenaufzeichnungen)
Eine Tastensequenz aufzeichnen und abspielen, um wiederholte Edits zu automatisieren.
Folding (Code einklappen)
Code-Regionen ausblenden, um sich auf die Struktur zu konzentrieren, mit Fold-Methoden.
Plugin-Manager & Ökosystem
Plugins mit vim-plug / packer / lazy installieren, konfigurieren und entdecken.
LSP, Completion & Diagnosen
IDE-Features (Autovervollständigung, Go-to-Definition, Diagnosen) via LSP erhalten.
Was this helpful?