# Vim

> Edit with operators, motions and text objects, run substitutions over ranges, record macros, manage buffers and windows, and fix paste, swap and startup problems.

Canonical: https://www.wiki.jodisand.me/vim/
Reviewed: 2026-09-24
Related: [Git](https://www.wiki.jodisand.me/git/index.md), [Bash](https://www.wiki.jodisand.me/bash/index.md), [tmux](https://www.wiki.jodisand.me/tmux/index.md)


## Cheatsheet

| Task | Keys or command |
| --- | --- |
| Quit without saving, save and quit | `:q!`, `:wq` or `ZZ` |
| Save as root after opening read-only | `:w !sudo tee % > /dev/null` |
| Undo, redo | `u`, `C-r` |
| Delete a line, a word, to end of line | `dd`, `dw`, `D` |
| Change inside quotes, parentheses, a word | `ci"`, `ci(`, `ciw` |
| Yank a line, paste after, paste before | `yy`, `p`, `P` |
| Repeat the last change | `.` |
| Search, next, previous | `/pattern`, `n`, `N` |
| Search the word under the cursor | `*`, `#` |
| Replace in the whole file, confirm each | `:%s/old/new/gc` |
| Delete every line matching | `:g/pattern/d` |
| Jump to line, to top, to bottom | `:42` or `42G`, `gg`, `G` |
| Jump back, forward through the jump list | `C-o`, `C-i` |
| Open a file, list buffers, switch | `:e path`, `:ls`, `:b name` |
| Split, vertical split, move between | `:sp`, `:vs`, `C-w h/j/k/l` |
| Record a macro into `q`, run it, run it 10 times | `qq ... q`, `@q`, `10@q` |
| Indent a block | `>` in visual mode, `>>` on a line, `=` to re-indent |
| Run a shell command, filter lines through one | `:!cmd`, `:%!sort` |
| Reload the file from disk | `:e!` |
| Show what a key does | `:help ciw`, `:verbose map <leader>f` |

Behaviour below is Vim 9.1 or later and Neovim 0.11 or later; differences are noted where they matter. Reference: [Vim help](https://vimhelp.org/) and [Neovim docs](https://neovim.io/doc/user/).

## Modes

Vim is a modal editor: the same keys do different things depending on the mode, and almost every "Vim typed garbage" complaint is a key pressed in the wrong one. The status line shows the mode when `showmode` is on (the default), and `Escape` returns to Normal from anywhere.

| Mode | Enter | Purpose |
| --- | --- | --- |
| Normal | `Escape` | Commands and motions; where you spend most time |
| Insert | `i`, `a`, `I`, `A`, `o`, `O`, `c`, `s` | Typing text |
| Visual | `v`, `V`, `C-v` | Character, line and block selection; an operator then acts on it |
| Command-line | `:`, `/`, `?`, `!` | Ex commands, search, filters |
| Replace | `R` | Overwrite characters |
| Terminal | `:terminal` | A shell in a buffer; `C-\ C-n` returns to Normal |

`i` inserts before the cursor, `a` after, `I` at the first non-blank, `A` at the end of the line, `o` opens a line below, `O` above. `C-o` in Insert mode runs one Normal command and returns, so `C-o zz` centres the screen without leaving insert. `C-r "` in Insert or Command-line mode pastes a register; `C-r =` evaluates an expression.

## Operators, motions and text objects

The grammar is `[count] operator [count] motion-or-object`. An operator says what to do, a motion or text object says to what, and counts multiply. Learning the pieces separately gives every combination for free.

| Operator | Action |
| --- | --- |
| `d` | Delete (into a register; see below) |
| `c` | Change: delete and enter Insert |
| `y` | Yank (copy) |
| `>` `<` | Shift indentation |
| `=` | Re-indent using the filetype's rules |
| `gu` `gU` `g~` | Lowercase, uppercase, toggle case |
| `gq` | Reformat to `textwidth` |
| `!` | Filter through a shell command |

Doubling an operator applies it to the current line: `dd`, `cc`, `yy`, `>>`, `gUU`. A capital is a shortcut for "to end of line": `D`, `C`, `Y` (in Vim `Y` is `yy`; Neovim maps it to `y$` for consistency).

| Motion | Moves |
| --- | --- |
| `h j k l` | One character or line |
| `w` `b` `e` | Word start forward, back; word end. `W B E` use whitespace-delimited words |
| `0` `^` `$` | Column 0, first non-blank, end of line |
| `f{c}` `t{c}` | To, till the next `c` on the line; `F T` backward; `;` `,` repeat |
| `%` | Matching bracket |
| `{` `}` | Paragraph back, forward |
| `gg` `G` | First line, last line; `42G` is line 42 |
| `H M L` | Top, middle, bottom of the window |
| `C-d` `C-u` `C-f` `C-b` | Half page, full page |
| `''` `` `` `` | Line, exact position before the last jump |
| `/pat` `?pat` | Search: a motion like any other, so `d/foo` deletes to the match |

Text objects work only after an operator or in Visual mode, and select a structure around the cursor regardless of where in it the cursor is. `i` means inside, `a` means around (including delimiters or trailing whitespace).

| Object | Selects |
| --- | --- |
| `iw` `aw` | Word; `aw` includes the following space |
| `iW` `aW` | Whitespace-delimited word |
| `is` `as` | Sentence |
| `ip` `ap` | Paragraph |
| `i(` `a(` (or `ib`, `i)`) | Parentheses, also `[` `{` `<` |
| `i"` `a"` | Quoted string, also `'` and `` ` `` |
| `it` `at` | XML or HTML tag |

```text
ci"      change the text inside the quotes the cursor is in
da(      delete the parenthesised group including the parentheses
yip      yank the paragraph
>ap      indent the paragraph and its trailing blank line
gUiw     uppercase the word
d2w      delete two words; 2dw does the same
c3j      change this and the three lines below
dt,      delete up to but not including the next comma
=i{      re-indent the block inside braces
```

`.` repeats the last change including its count and the text inserted, so a `ciwfoo<Esc>` followed by `n.` fixes the next occurrence. Count-prefixed dots (`3.`) replace the original count.

## Registers

Every delete, change and yank writes to a register, and `p` reads from the unnamed register `"`. Prefix the operator with `"x` to name one.

| Register | Holds |
| --- | --- |
| `""` | Last delete, change or yank; what `p` uses |
| `"0` | Last yank only, untouched by deletes: the fix for "I yanked, deleted, and pasted the wrong thing" |
| `"1`-`"9` | Last nine deletes or changes of at least a line, shifting down |
| `"-` | Last small (less than a line) delete |
| `"a`-`"z` | Named; `"A` appends to `a` |
| `"+` `"*` | System clipboard, and X11 primary selection; need a clipboard-enabled build |
| `"_` | Black hole: `"_dd` deletes without touching any register |
| `"/` `":` `".` | Last search, last command line, last inserted text (read-only) |
| `"%` `"#` | Current and alternate file names |
| `"=` | Expression: `"=strftime('%F')<CR>p` |

```text
"ayy      yank the line into a
"Ayy      append the next line to a
"ap       paste a
"0p       paste the last yank even after several deletes
"+y       yank a visual selection to the system clipboard
:reg a0"  show these registers
:let @a=''  clear a
```

`:echo has('clipboard')` returns 1 when `"+` works. On Fedora, `vim-enhanced` lacks it; `vim-X11` (`gvim -v`) or Neovim with `wl-clipboard` or `xclip` installed provide it. Inside [tmux](https://www.wiki.jodisand.me/tmux/#clipboard) over SSH, Neovim can use OSC 52 with `vim.g.clipboard` set to the `osc52` provider.

## Macros

A macro records keystrokes into a register and replays them, which makes it a register like any other: you can paste it, edit it and yank it back.

```text
qa        start recording into a
0f=lct;"$HOME/bin"<Esc>j    the edit, ending with a move to the next line
q         stop
@a        replay once
5@a       replay five times
@@        repeat the last macro
:'<,'>normal @a    run on each line of a visual selection
:g/TODO/normal @a  run on each line matching TODO
```

A macro stops when a motion fails, so a recording that ends with `j` stops at the last line and one that uses `f=` stops on a line without `=`. That is a feature: `1000@a` runs until the pattern runs out. Record with commands that are position-independent (`0`, `^`, `f`, `/`) rather than counting characters. To edit a macro, `"ap` it into a scratch line, change it, then `"ayy` it back; `<Esc>` appears as `^[` and must stay as a literal escape character (type it with `C-v Esc`).

## Search and replace

Searches are regular expressions in Vim's own flavour, which is nearer to BRE than to PCRE: `+`, `?`, `|`, `(`, `)` and `{` are literal unless escaped. `\v` (very magic) at the start makes them special, so `\v(foo|bar)+` matches without the backslashes. `\c` anywhere makes one search case-insensitive; `ignorecase` plus `smartcase` makes lowercase patterns insensitive and mixed-case ones sensitive.

```text
/\<word\>       whole word
/\vfoo(bar)@!   foo not followed by bar (lookahead)
/\d\{3}         three digits
/^\s*$          blank lines
/foo\_.*bar     across lines: \_ prefixes a class to include newline
```

`:s` substitutes on a range of lines; the default is the current line. `:%` is the whole file, `:'<,'>` the visual selection (typed automatically when `:` is pressed in Visual mode), `:.,+5` this line and five more, `:10,20`, `:/start/,/end/` between two matches, and `:.,$`.

```text
:%s/old/new/g        every occurrence on every line; without g only the first per line
:%s/old/new/gc       confirm each: y, n, a (all), q, l (this one then quit)
:s/\(\w\+\) \(\w\+\)/\2 \1/     swap two words with groups
:%s/\v(\w+)@(\w+)/\2 at \1/     the same with very magic
:%s/foo/\U&/g        & is the match; \U uppercases to the end or \E
:%s/\s\+$//e         strip trailing whitespace; e suppresses "pattern not found"
:%s#/usr/local#/opt#g    any delimiter works when the pattern contains slashes
:%s/x/\r/g           \r inserts a newline in the replacement; \n would insert a NUL
:%s/\n\n\+/\r\r/     collapse runs of blank lines to one
:%s//new/g           empty pattern reuses the last search
:%s/\<\(\w\)\(\w*\)\>/\u\1\L\2/g   Title Case each word
:%s/pat/\=line('.')/   \= evaluates an expression as the replacement
&  or :&&            repeat the last substitute on this line, with the same flags
g&                   repeat it on every line
```

`:g/pattern/command` runs an Ex command on every line matching; `:v` or `:g!` on every line not matching. The command defaults to `p` (print), which is where the name of `grep` comes from.

```text
:g/^\s*#/d           delete comment lines
:v/error/d           keep only lines containing error
:g/^$/,/./-j         join each run of blank lines into one
:g/func/normal A;    append ; to every line containing func
:g/pat/m0            reverse the order of matching lines (move each to the top)
:g/pat/t$            copy matching lines to the end
:g/^Host /+1s/^/    /    indent the line after each Host line
:g/pat/s/a/b/        substitute only on matching lines
```

## Buffers, windows and tabs

A buffer is a file loaded in memory. A window is a viewport onto a buffer. A tab page is a collection of windows. Closing a window does not unload its buffer, and the same buffer can show in several windows, which is why "tabs as files" from other editors maps badly: use buffers for files and windows for views.

```text
:e path           edit a file (open a buffer); :e! discards changes and reloads
:ls               list buffers; % is current, # alternate, + modified, h hidden
:b 3  :b name     switch by number or partial name (Tab completes)
:bn :bp  C-^      next, previous, toggle with the alternate buffer
:bd               delete (unload) the buffer; :bd! discards changes
:bufdo %s/a/b/ge | update    run a command in every buffer and save the changed ones
:sp path  :vs path    split horizontally, vertically
C-w s  C-w v      split the current buffer
C-w h j k l       move between windows; C-w w cycles
C-w H J K L       move the window to the far left, bottom, top, right
C-w o             close every other window
C-w =             equalise sizes; C-w _ maximise height; C-w | width; 10 C-w + grow by ten
C-w q  :q         close the window; the buffer stays loaded
:tabnew path      new tab; gt gT move between; :tabclose
:windo diffthis   run a command in every window of the tab
:find name        search the path option; set path+=** for recursive lookup in the project
:args **/*.go     set the argument list; :argdo runs a command over it
```

`hidden` (on by default in Neovim, off in Vim) lets a modified buffer leave the window without being saved; without it `:e other` on a modified buffer fails with `E37`, and `:e! other` discards the changes.

## Marks and jumps

`m{a-z}` sets a mark in the buffer, `m{A-Z}` a global one that also records the file. `'a` jumps to the line, `` `a `` to the exact position, and both are motions, so `d'a` deletes from here to the mark's line and `y`a` yanks to its position.

| Mark | Meaning |
| --- | --- |
| `` `. `` | Position of the last change; `gi` inserts there |
| `` `^ `` | Where Insert mode was last exited |
| `` `" `` | Where the cursor was when the buffer was last exited |
| `` `[ `` `` `] `` | Start and end of the last changed or yanked text |
| `` `< `` `` `> `` | Start and end of the last visual selection; `gv` reselects it |
| `''` | Position before the last jump |

Jumps (`G`, `gg`, `%`, `/`, `n`, `''`, `:42` and anything that moves more than a line) go on the jump list; `C-o` goes back and `C-i` (Tab) forward, across files. `g;` and `g,` walk the change list instead. `:marks`, `:jumps` and `:changes` show them.

## A minimal sane vimrc

Vim loads `~/.vimrc` (or `~/.vim/vimrc`), and when neither exists it loads `defaults.vim`, which turns on syntax highlighting, filetype detection, `incsearch`, a five-line `scrolloff` and a short `ttimeoutlen`. Creating an empty vimrc switches all of that off, so a vimrc should either start with `source $VIMRUNTIME/defaults.vim` or set the essentials itself. This one sets them itself so it reads the same on any version.

```vim
set nocompatible                 " Vim, not vi; implied when a vimrc exists but harmless
filetype plugin indent on
syntax enable
set encoding=utf-8
set hidden                       " switch buffers without saving
set backspace=indent,eol,start   " backspace over everything in Insert
set incsearch hlsearch ignorecase smartcase
set scrolloff=5 sidescrolloff=5
set number relativenumber        " absolute on the cursor line, relative elsewhere: counts for j/k
set wildmenu wildmode=longest:full,full
set laststatus=2 ruler showcmd
set ttimeout ttimeoutlen=50      " Escape is recognised quickly, key codes still work
set nrformats-=octal             " C-a on 007 gives 008, not 010
set autoread                     " reload a file changed outside Vim when it is unmodified
set undofile undodir=~/.vim/undo//   " persistent undo; create the directory
set noswapfile                   " or set directory=~/.vim/swap// to keep them out of the tree
set splitbelow splitright
set list listchars=tab:▸\ ,trail:·,nbsp:␣
set expandtab shiftwidth=4 softtabstop=4   " ftplugins override per filetype
set formatoptions+=j             " remove comment leaders when joining lines
set mouse=a
set clipboard=unnamedplus        " y and p use the system clipboard when available
let mapleader = ' '
nnoremap <leader>w :update<CR>
nnoremap <silent> <leader>h :nohlsearch<CR>
nnoremap Q gq
```

`noremap` variants never expand other mappings, and are what a vimrc should use unless a mapping deliberately builds on another. `:verbose set shiftwidth?` shows where an option was last set, which is how to find the plugin or ftplugin overriding a vimrc value. `:scriptnames` lists every file sourced, in order.

## Neovim differences

Neovim keeps Vim's editing model and most of its Vimscript, and changes defaults, configuration and extension points. Config lives at `~/.config/nvim/init.lua` (or `init.vim`), data under `~/.local/share/nvim`, and `~/.local/state/nvim/shada/main.shada` replaces `.viminfo`. Everything the sane vimrc above sets is already the default except `number`, `list`, `undofile`, the indent settings, `splitbelow`/`splitright`, `clipboard` and the mappings: `hidden`, `autoread`, `incsearch`, `hlsearch`, `wildmenu`, `laststatus=2`, `ttimeoutlen=50`, `backspace`, `nrformats-=octal`, `mouse=nvi`, filetype and syntax are on, and `Y` yanks to end of line. Bracketed paste is handled automatically, so `pastetoggle` and `:set paste` are unnecessary.

Removed: Vim9 script (Neovim runs legacy Vimscript and Lua), cscope, `:hardcopy` and the GUI-specific commands. Added: a built-in LSP client (`vim.lsp`), Tree-sitter parsing for highlighting and text objects, a Lua API (`vim.api`, `vim.o`, `vim.keymap.set`), `:terminal` with a job control API, and default mappings such as `gcc` to comment a line, `K` for LSP hover and `grn`, `gra`, `grr` for rename, code action and references (0.10+, `[d` `]d` for diagnostics).

```lua
-- ~/.config/nvim/init.lua: the same settings as the vimrc above
vim.o.number = true
vim.o.relativenumber = true
vim.o.undofile = true
vim.o.expandtab = true
vim.o.shiftwidth = 4
vim.o.softtabstop = 4
vim.o.splitbelow = true
vim.o.splitright = true
vim.o.list = true
vim.o.listchars = 'tab:▸ ,trail:·,nbsp:␣'
vim.o.clipboard = 'unnamedplus'
vim.g.mapleader = ' '
vim.keymap.set('n', '<leader>w', '<Cmd>update<CR>')
vim.keymap.set('n', '<leader>h', '<Cmd>nohlsearch<CR>', { silent = true })
vim.cmd.colorscheme('habamax')
```

`:checkhealth` reports missing providers (clipboard, Python, Node) and misconfiguration; `nvim --clean` starts without any config, and `nvim -u NONE` is the Vim equivalent. The `vim` command on many systems is a symlink to `nvim`; `vim --version | head -1` says which.

## vimdiff

`vimdiff a b` (or `vim -d`, `nvim -d`) opens files side by side with `diffthis` set on each window, folds unchanged regions and highlights changed lines and the characters within them. It is `git mergetool` with `merge.tool = vimdiff`, and `git difftool -t vimdiff` for reviewing.

```text
]c  [c            next, previous change
do                obtain: pull the other window's hunk into this one (:diffget)
dp                put: push this hunk to the other window (:diffput)
:diffget //2      in a three-way merge, take from the left (LOCAL) buffer; //3 is REMOTE
:diffupdate       recompute after manual edits
zo zc zR zM       open, close a fold; open, close all
:set diffopt+=iwhite          ignore whitespace changes
:set diffopt+=algorithm:patience,indent-heuristic   diff algorithm; 8.1+ and Neovim
:windo diffthis   compare two already-open windows; :diffoff! ends it
:wqa              save every buffer and quit
```

With `git mergetool` there are four windows: LOCAL, BASE, REMOTE on top and the merged file below. Edit the bottom one, `:diffget LO` or `:diffget RE` (buffer name prefixes work), then `:wqa`; `:cq` exits non-zero to tell Git the merge failed.

## Oneliners

```text
" Delete trailing whitespace in the whole file
:%s/\s\+$//e

" Convert tabs to spaces per the current settings, or the reverse with noexpandtab
:set expandtab | retab

" Sort the visual selection, removing duplicates
:'<,'>sort u

" Sort by the number in each line
:%sort n

" Reverse every line in the file
:g/^/m0

" Number each line in the selection
:'<,'>s/^/\=line('.') - line("'<") + 1 . '. '/

" Insert the output of a command below the cursor
:r !date -Is

" Replace the buffer with the output of a filter
:%!jq .

" Format a JSON selection in place
:'<,'>!python3 -m json.tool

" Run the current line as a shell command and replace it with the output
!!sh

" Write the selection to a file
:'<,'>w part.txt

" Open every file containing a pattern, one per buffer, and jump through matches
:grep -r pattern . | copen      then :cn :cp

" Substitute across the quickfix list (Vim 8+, Neovim): grep, then
:cdo s/old/new/g | update

" Increment a column of numbers, one more each line: select with C-v, then
g C-a

" Join all lines into one
:%j

" Split a line on commas
:s/,/\r/g

" Change the file's line endings to Unix
:set ff=unix | w

" Show the character under the cursor as a code point
ga

" Show the full path of the current file
:echo expand('%:p')      or C-g, or 1 C-g

" Change to the directory of the current file
:cd %:h

" Open the file name under the cursor, or the URL with a handler
gf    gx

" Diff the buffer against the file on disk
:w !diff % -

" Spell check with the Australian dictionary
:setlocal spell spelllang=en_au     then ]s z= zg

" Open the file with the cursor at a line, at a pattern
vim +42 file      vim +/pattern file

" Run Ex commands from the shell without opening the editor
vim -es -c '%s/foo/bar/g' -c 'wq' file

" Encrypt a file (Vim only; Neovim removed it)
vim -x secrets.txt
```

## Scripts

Batch-edit files with Vim's engine from Bash, for edits that need Vim's regex or text objects rather than sed.

```sh
#!/usr/bin/env bash
# usage: vim-batch '<ex command>' file...
# Example: vim-batch '%s/\v<colour>/color/g' src/*.md
set -euo pipefail
cmd=${1:?ex command}; shift
for f in "$@"; do
  [[ -f $f ]] || { printf 'skip: %s\n' "$f" >&2; continue; }
  vim -es -u NONE -i NONE -c "set nomore" -c "$cmd" -c 'update' -c 'qa!' -- "$f" </dev/null \
    || printf 'failed: %s\n' "$f" >&2
done
```

Find and dispose of stale swap files across a tree; swap files with no owning process are safe to remove once you have confirmed the file on disk is current.

```sh
#!/usr/bin/env bash
set -euo pipefail
dir=${1:-$HOME}
find "$dir" -type f \( -name '.*.swp' -o -name '.*.swo' \) -print0 | while IFS= read -r -d '' swp; do
  pid=$(vim -r "$swp" 2>&1 | awk '/process ID/ {print $NF}' | tr -dc '0-9' || true)
  if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then
    printf 'in use by %s: %s\n' "$pid" "$swp"
  else
    printf 'stale: %s\n' "$swp"
    [[ ${DELETE:-0} == 1 ]] && rm -f -- "$swp"     # DELETE=1 removes; default is report only
  fi
done
```

Profile startup and print the slowest sourced scripts.

```sh
#!/usr/bin/env bash
set -euo pipefail
log=$(mktemp)
trap 'rm -f "$log"' EXIT
vim --startuptime "$log" -c 'qa!' "${1:-}" >/dev/null 2>&1 </dev/null
printf 'total: %s ms\n' "$(tail -1 "$log" | awk '{print $1}')"
grep -E 'sourcing|require' "$log" | sort -k2 -rn | head -15    # column 2 is the elapsed time of that item
```

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Pasted code gains indentation on every line | `autoindent` applied to pasted text as if typed; terminal did not send bracketed paste | Vim 8.0+ and Neovim detect bracketed paste in terminals that support it (`:set t_BE?`); otherwise `:set paste`, paste, `:set nopaste`, or use `"+p` |
| `E325: ATTENTION`, swap file found | Another Vim has the file open, or one crashed | Read the message: process still running means edit elsewhere; otherwise `R` recovers, `D` deletes the swap, and `vim -r file` recovers from the shell |
| Startup takes seconds | A slow plugin, or a clipboard provider probing X11 over SSH | `vim --startuptime log`; `vim -u NONE` to confirm; in Neovim `:checkhealth provider` |
| Escape delay before Normal mode | `ttimeoutlen` too high, or `esc-time` in tmux | `set ttimeout ttimeoutlen=50`; [tmux](https://www.wiki.jodisand.me/tmux/#troubleshooting) `escape-time` |
| Arrow keys insert `A` `B` `C` `D` in Insert mode | `TERM` wrong, or `nocompatible` unset with an ancient config | Check `echo $TERM`; a vimrc that exists but sets nothing leaves Vim in vi-compatible mode |
| Colours wrong or missing in tmux or SSH | `TERM` lacks 256-colour or RGB capability | `:set termguicolors` only after tmux's `RGB` feature is set; otherwise `:set notermguicolors` |
| `E212: Can't open file for writing` | Read-only file or directory | `:w !sudo tee % > /dev/null` then `:e!`, or open with `sudoedit` |
| `E37: No write since last change` | `hidden` off and switching buffers | `set hidden`, or `:w` first, or `:e!` to discard |
| Search finds nothing though the text is there | Special characters unescaped, or `\v` missing; or `ignorecase` off and case differs | `\V` for a literal search, `\c` for case-insensitive |
| Undo lost after reopening | `undofile` off, or `undodir` missing so nothing was written | `set undofile undodir=~/.vim/undo//` and `mkdir -p ~/.vim/undo` |
| `.` does not repeat a visual-mode operation as expected | `.` repeats on the same number of characters or lines, not the same object | Use a text object or macro instead |
| Mapping does not work in a filetype | An ftplugin overrides it with `<buffer>` | `:verbose map <key>` shows the winner and where it was set |
| File shows `^M` at line ends | CRLF file opened with `fileformat=unix` | `:e ++ff=dos` to reread, or `:%s/\r$//` to strip; `:set ff=unix` before writing |
| `E492: Not an editor command` for a plugin command | Plugin not loaded, or load order | `:scriptnames`, `:packpath?`; in Neovim `:Lazy` or the manager's status view |

`vim -V9/tmp/vimlog` traces every sourced line and autocommand into a file, which locates a plugin that changes an option behind your back; `:verbose set opt?` is the quicker check for one option.

## Further reading

- [Vim user manual](https://vimhelp.org/usr_toc.txt.html): the task-oriented chapters; `:help user-manual` opens the same thing
- [Vim reference: motions and text objects](https://vimhelp.org/motion.txt.html): the complete list with edge cases
- [Vim reference: pattern syntax](https://vimhelp.org/pattern.txt.html): the regex flavour, `\v`, and multi-line matching
- [Neovim: differences from Vim](https://neovim.io/doc/user/vim_diff.html): defaults, removed features and added mappings
- [Neovim: Lua guide](https://neovim.io/doc/user/lua-guide.html): configuring options, mappings and autocommands from `init.lua`


