为什么重新整理这份笔记
以前的 Vim 笔记是边学边记的:寄存器在一篇、宏在另一篇,搜索、分屏和格式化又散落在几十条技巧里。单条命令虽然能查到,真正编辑时却很难回答两个问题:这个命令适合什么场景,以及它为什么能和别的命令组合?
这次把 2019、2020、2024 年的通用笔记和寄存器专文合并为一条学习主线,并纠正其中已经过时或不准确的内容。重点不是背快捷键,而是建立一套可以迁移到代码、配置文件和普通文本的编辑方法。
先记住一句话:
Vim 的核心不是“快捷键很多”,而是用“操作符 + 范围 + 重复”描述一次编辑。
如果只想快速开始,可依次阅读“编辑语言 → 寄存器 → 批量修改 → 工程导航”;每章末尾都可以直接在临时文件里练习。
| 想解决的问题 | 首选工具 | 典型命令 |
|---|---|---|
| 改一个有边界的对象 | 操作符 + 文本对象 | ciw、di"、yap |
| 重复刚才的修改 | 点命令 | . |
| 保护已经复制的内容 | 专用寄存器 | "0p、"_d |
| 对很多行做同一种动作 | :normal |
:%normal! A; |
| 重复一串依赖上下文的动作 | 宏 | qa…q、@a、@@ |
| 跨文件查找并逐项处理 | quickfix | :vimgrep、:cnext |
| 把文本交给系统命令 | filter / Shell | :%!sort -u |
1. 模式不是门槛,而是在选择“接下来要表达什么”
Vim 最常用的四种模式分别承担不同角色:
| 模式 | 用途 | 常用入口 | 返回普通模式 |
|---|---|---|---|
| 普通模式 Normal | 移动、删除、复制、组合命令 | 启动 Vim 后默认进入 | 已在其中 |
| 插入模式 Insert | 输入新文字 | i、a、o、O |
<Esc> 或 <C-[> |
| 可视模式 Visual | 先选择范围,再操作 | v、V、<C-v> |
<Esc> |
| 命令行模式 Command-line | 保存、替换、搜索、批处理 | :、/、? |
<Esc> |
不要长时间停留在插入模式里寻找方向键。完成一小段输入后回到普通模式,下一步便可以继续用“动作”表达意图。
几个入口的区别很实用:
i在光标前插入,a在光标后插入;o在下方新建一行,O在上方新建一行;gi回到上次离开插入模式的位置继续输入;- 插入模式中的
<C-o>可以临时执行一次普通模式命令,执行后自动回到插入模式。
保存和退出只需先掌握这一组:
| 命令 | 含义 |
|---|---|
:w |
保存当前缓冲区 |
:q |
关闭当前窗口;有未保存修改时会拒绝 |
:wq 或 ZZ |
保存并退出 |
:q! 或 ZQ |
放弃当前修改并退出 |
这里的“窗口”和“文件”不是同一个概念,后文会专门说明。
插入模式也有不依赖插件的补全
Vim 自带的补全足以处理许多轻量场景。进入插入模式后:
| 命令 | 补全来源 |
|---|---|
<C-n> / <C-p> |
当前补全来源中的下一个 / 上一个候选 |
<C-x><C-f> |
文件名 |
<C-x><C-l> |
整行 |
<C-x><C-]> |
tags |
<C-x><C-o> |
当前文件类型的 omni completion;需要相应 omnifunc |
补全菜单出现后可用 <C-n>、<C-p> 选择,<C-y> 接受,<C-e> 取消。LSP 或补全插件可以提供更丰富的数据源,但这些内置按键仍有助于理解补全菜单的基本行为。
2. 把命令看成一门编辑语言
普通模式下最有价值的结构是:
[次数] + 操作符 + [次数] + 动作或文本对象 |
常用操作符只有几个:
| 操作符 | 含义 | 示例 |
|---|---|---|
d |
删除并写入寄存器 | dw 删除到下一个单词 |
c |
修改:先删除,再进入插入模式 | ciw 修改当前单词 |
y |
复制(yank) | yap 复制当前段落 |
> / < |
增加 / 减少缩进 | >ip 缩进当前段落 |
= |
按文件类型规则重排缩进 | =ap 重排当前段落 |
gq |
按 textwidth 格式化文本 |
gqap 格式化当前段落 |
动作描述“走到哪里”,例如 w 到下一个词、$ 到行尾、} 到下一段。文本对象描述“这个结构是什么”:
| 文本对象 | 范围 | 常见组合 |
|---|---|---|
iw / aw |
单词内部 / 单词及周围空白 | ciw、daw |
i" / a" |
双引号内部 / 连同双引号 | di"、ya" |
i( / a( |
圆括号内部 / 连同圆括号 | ci(、ya( |
it / at |
HTML/XML 标签内部 / 连同标签 | cit、dat |
ip / ap |
段落内部 / 连同周围空行 | gqip、yap |
i 可以理解成 inside,a 可以理解成 around。光标只要位于对象内部,通常不必先移动到对象开头。

动态图 1:ciw 把“修改当前单词”作为一个完整动作;. 在下一个目标上复用这次修改。
点命令为什么重要
. 重复的是“最近一次可重复的修改”,不是最近按下的单个键。例如:
ciwrequest_timeout<Esc> |
整段会成为一次修改。之后移动到另一个同类单词按 .,即可再次执行相同修改。它适合“动作相同、目标由自己逐个选择”的任务。
需要特别区分:. 不等于重复宏;重复最近执行的宏应使用 @@。
一分钟练习
新建一个临时文件,输入:
timeout = read_timeout + write_timeout |
把光标放到第一个 timeout 内,执行 ciwlimit<Esc>;再用 w 移动到后面的单词,尝试 .。即使这次不需要真的把三个变量改成同名,也要观察“一次修改”的边界。
3. 移动、搜索和跳转:按目标尺度选择命令
不要把所有移动都压在 hjkl 上。文本越大,越应该使用更大尺度的动作。
行内和结构移动
| 目标 | 命令 |
|---|---|
| 行首 / 第一个非空字符 / 行尾 | 0 / ^ / $ |
| 下一个词首 / 上一个词首 / 当前或下一个词尾 | w / b / e |
本行下一个字符 x 的位置 / 前一格 |
fx / tx |
重复 / 反向重复最近的 f、t |
; / , |
| 上一个 / 下一个段落 | { / } |
| 配对的括号、方括号或花括号 | % |
| 文件开头 / 文件末尾 / 第 42 行 | gg / G / 42G |
G 只是跳到文件末尾,不会增加缩进。若要从当前位置处理到文件末尾,应把它作为范围,例如 >G 增加一级缩进,或 =G 重排缩进。
搜索不是最后一步,也可以成为下一条命令的范围
| 命令 | 含义 |
|---|---|
/pattern / ?pattern |
向后 / 向前搜索 |
n / N |
同方向 / 反方向重复搜索 |
* / # |
向后 / 向前搜索光标下的完整单词 |
:nohlsearch |
暂时清除搜索高亮 |
搜索后可以直接结合点命令:先用 * 找到同名词,再完成一次修改,之后用 n 选目标、. 执行修改。
完整单词在 Vim 正则中可写为:
/\<timeout\> |
当表达式中反斜杠太多时,可以用 very magic 模式降低阅读成本:
/\v(user|admin)_[0-9]+ |
跳回“刚才的位置”
Vim 会记录比浏览器历史更细的导航信息:
| 需求 | 命令 |
|---|---|
| 沿跳转列表后退 / 前进 | <C-o> / <C-i> |
| 跳到更早 / 更新的修改位置 | g; / g, |
在当前位置建立标记 a |
ma |
跳到标记 a 所在行 / 精确位置 |
'a / `a |
| 打开光标下的文件名 | gf |
执行 gf 进入另一个文件后,通常用 <C-o> 回来。df 是“删除到字符 f”,不是文件跳转命令。
4. 寄存器:解决“刚复制的内容为什么被覆盖了”
Vim 的复制、删除和修改都会与寄存器交互。p 默认读取无名寄存器,因此下面的操作经常带来意外:
yy复制一行;dd删除另一行;p粘贴时得到刚删除的行,而不是先前复制的行。
这是寄存器设计的结果,不是复制失败。yy 同时更新无名寄存器和复制专用寄存器 0,随后的 dd 会覆盖无名寄存器,但不会覆盖 0,所以此时用 "0p 即可取回最近一次未显式指定寄存器的 yank。

动态图 2:p 读取无名寄存器;"0p 明确读取最近一次 yank。
实用寄存器速查表
| 寄存器 | 用途 | 示例 |
|---|---|---|
" |
无名寄存器,默认被复制、删除和修改使用 | p 等价于 ""p |
0 |
最近一次未显式指定其他寄存器的 yank | "0p |
1 至 9 |
最近几次行删除或较大删除的历史队列 | "2p |
- |
最近一次小于一行的删除 | "-p |
a 至 z |
有名寄存器;写入时用大写名称表示追加 | "ayiw、"Ayiw |
_ |
黑洞寄存器,丢弃删除内容 | "_dd |
+ / * |
系统剪贴板 / 选择缓冲区,具体映射因平台而异 | "+y、"+p |
: / . / % |
最近 Ex 命令 / 最近插入文本 / 当前文件名 | 插入模式中 <C-r>% |
= |
表达式寄存器 | 插入模式中 <C-r>=2+3<CR> |
/ |
最近一次搜索模式 | :let @/ = '' 清空它 |
用下面的命令观察真实状态,比猜测更可靠:
:registers |
若 "+y 无效,先检查当前 Vim 是否带剪贴板支持:
:echo has('clipboard') |
macOS 的 Cmd-V、Linux 的主选择区以及终端自身的粘贴并不总是等同于 Vim 的 + 寄存器;跨应用复制时明确使用 "+y / "+p,行为更容易推理。
5. 从一次修改扩展到一百次
批量编辑前先判断任务属于哪一种:
| 任务形状 | 工具 | 判断标准 |
|---|---|---|
| 同一修改,目标由人逐个选择 | . |
每个位置都执行完全相同的修改 |
| 同一普通模式动作应用到每行 | :normal! |
每行可从相同初始条件开始 |
| 文本模式可直接描述 | :substitute / :global |
能写出可靠的搜索模式 |
| 多步动作依赖当前行内容 | 宏 | 需要移动、编辑、再移动到下一项 |
| 目标来自跨文件搜索或编译结果 | quickfix + :cdo |
已经有一组位置列表 |
宏:录制的是按键语言
录制到寄存器 a 的基本流程为:
qa 开始录制到寄存器 a |

动态图 3:好的宏从稳定位置开始,并在下一条记录的稳定位置结束。
宏更可靠的三个习惯:
- 开始时用
0、^或一次搜索把光标放到可预测位置; - 使用
f、t、文本对象或搜索定位,不依赖固定列数; - 让宏末尾移动到下一条记录,便于
5@a连续执行。
录错后不必全部重来。有名寄存器就是文本,可以执行 :put a 把宏放入缓冲区修改,再用 0"ay$ 以字符方式写回;也可以先查看 :registers a。
:normal!:对一组行执行普通模式命令
给整个文件的每一行末尾加分号:
:%normal! A; |

动态图 4:% 是全文件范围,normal! 在每一行执行 A;;! 表示忽略自定义映射。
范围同样能交给不同的 Ex 命令:
:'<,'>s/^/- / |
第一条用替换命令给刚才选择的行添加 - 前缀;后两条分别处理第 10 到 20 行,以及所有以 TODO 开头的行。
可视模式选择后,o 在选区两端切换活动端点,gv 重新选择上一次可视区域。这对修正选区边界很方便。
替换与 :global
常用替换模板是:
:%s/old/new/gc |
%:整个文件;省略时只处理当前行;g:替换每行中的所有匹配,而非每行第一个;c:逐项确认。
路径中斜杠很多时,可以换分隔符:
:%s#/old/path#/new/path#gc |
:global 先选出匹配的行,再对每一行执行 Ex 命令:
:g/DEBUG/d |
三条命令分别删除包含 DEBUG 的行、删除所有不以 # 开头的行,以及只给包含 TODO 的行追加分号。
这里的 |(例如 :vimgrep … | copen)是连接两条 Ex 命令,不是把前一条命令的标准输出通过 Shell 管道传给后一条。
6. 缩进、格式化、折叠和长行提示
缩进与文本格式化不是一回事
| 需求 | 命令 |
|---|---|
| 重排当前行 / 当前段落 / 全文缩进 | == / =ap / gg=G |
| 当前行增加 / 减少一级缩进 | >> / << |
按 textwidth 格式化当前段落 |
gqap |
| 格式化可视选区 | 选中后按 gq |
= 使用当前文件类型的缩进规则;如果结果不合理,先检查 :set filetype? indentexpr? cindent?,而不是继续叠加手工空格。
现代终端通常支持 bracketed paste,Vim 会识别一次外部粘贴的边界。旧笔记里把 :set paste 当成默认步骤已经不合适;只有在终端或旧版 Vim 无法正确处理粘贴时,才把它作为兼容排查手段,并在完成后立刻 :set nopaste。
折叠命令要分清开与关
| 命令 | 含义 |
|---|---|
zf{motion} |
手工创建折叠 |
za |
切换光标处折叠 |
zo / zc |
打开 / 关闭一个折叠 |
zR / zM |
打开 / 关闭所有折叠 |
zx |
根据当前 foldlevel 更新折叠,并显示光标行 |
zc 的含义是 close,不是 open。若折叠来自语法或缩进,可查看 :set foldmethod?。
80 列是排版约束,不一定要用复杂正则
如果希望正文以 80 列换行,并在下一列给出视觉提示:
set textwidth=80 |
只想提示、不想自动换行时,可把 textwidth 保持为 0,使用:
set colorcolumn=81 |
这比旧笔记里通过 :match 或 2match 拼接列号正则更清楚,也不会占用匹配高亮槽位。
7. Buffer、Window、Tab 分别是什么
很多“退出后文件怎么还在”“分屏为什么切不动”的困惑,都源于把三层对象混在一起:
| 对象 | 它代表什么 | 常用命令 |
|---|---|---|
| Buffer | 文件或临时文本在内存中的内容 | :ls、:buffer、:bnext、:bdelete |
| Window | 显示某个 buffer 的视口 | :split、:vsplit、:close |
| Tab page | 一组窗口布局 | :tabnew、gt、gT |
一个 buffer 可以同时显示在多个 window 中;关闭 window 不一定卸载 buffer;Vim 的 tab 更像“工作区布局”,不必把每个文件都放进一个 tab。
分屏工作流
:split other.txt " 水平分屏 |
窗口间移动:
<C-w>h 左 <C-w>j 下 <C-w>k 上 <C-w>l 右 |
也可用 <C-w>s、<C-w>v 拆分当前窗口。执行 :q 时关闭的是当前 window;当它是最后一个 window 时,Vim 才会退出。
8. 工程导航:把“找到结果”变成可遍历的列表
项目级编辑的关键不是一次跳到某处,而是形成闭环:
定位目标 → 打开上下文 → 修改/验证 → 跳到下一项 → 回到原位置 |

动态图 5:gf 与跳转列表适合单点导航;quickfix 适合管理一组跨文件结果。
文件名、标签和定义
gf打开光标下的文件名,<C-o>返回;路径解析可通过:set path?调整。- 生成 tags 后,
<C-]>跳到光标下标识符的标签,<C-t>返回标签栈。 gd跳到当前函数内的局部声明,gD尝试跳到当前文件内的全局声明。- Vim/Neovim 的 LSP 插件通常能提供更精确的定义、引用和重命名;那是对导航数据源的增强,不会替代 buffer、window 和跳转列表这些基础模型。
:vimgrep 与 quickfix
在项目的 Python 文件中寻找 TODO,但先不自动跳到第一个结果:
:vimgrep /TODO/j **/*.py | copen |
之后使用:
| 命令 | 含义 |
|---|---|
:cnext / :cprevious |
下一个 / 上一个结果 |
:cc |
显示当前结果的完整信息 |
:copen / :cclose |
打开 / 关闭 quickfix 窗口 |
:cdo {cmd} |
对每个结果执行一条 Ex 命令 |
:cfdo {cmd} |
对结果涉及的每个文件执行一次命令 |
例如确认列表无误后,对每个结果所在行执行替换,并只保存确实发生变化的文件:
:cdo s/TODO/DONE/g | update |
目标是一组已知文件而不是搜索结果时,可以使用参数列表:
:args **/*.py |
:argdo 遍历参数列表,:bufdo 遍历已加载的 buffer,:cdo 遍历 quickfix 中的有效位置。替换命令中的 e 标志允许某个文件没有匹配项;第一次在真实项目使用前,仍应先检查文件列表并确保版本控制状态清晰。
:make 也会把编译器或测试工具的错误解析到 quickfix。用 :set makeprg? 和 :set errorformat? 可以查看它们如何连接。
与 Shell 配合,而不是反复退出 Vim
| 场景 | 命令 |
|---|---|
| 执行一条命令 | :!git status |
| 把命令输出插入当前文件 | :read !date |
| 让外部命令过滤全文 | :%!sort -u |
| 暂停 Vim 回到原 Shell | <C-z>,完成后执行 fg |
| 在 Vim 中启动终端缓冲区 | :terminal(版本支持时) |
这一部分涉及作业控制、标准输入输出、过滤器和平台差异,已保留为独立专题:Vim 与 Shell 协作:挂起、恢复与外部命令。
9. 一份可解释的最小配置
配置的目标是把稳定偏好写下来,而不是在不理解行为时先堆插件。下面这份 ~/.vimrc 可以作为起点:
set nocompatible |
tabstop 是 Tab 字符显示宽度,softtabstop 是编辑时一次 Tab/退格感受到的宽度,shiftwidth 是 >>、<< 和自动缩进使用的宽度,expandtab 则让插入的 Tab 展开为空格。四者不要只改一个就期待所有行为一起变化。
不同文件类型需要不同规则时,优先用局部选项。简单配置可写成:
augroup personal_filetype_settings |
配置变多后,把内容分别放到 ~/.vim/after/ftplugin/python.vim、javascript.vim 等文件中更容易维护。文件里只写对应的 setlocal 即可。
Neovim 用户同样可以使用本文的编辑命令;它的主配置目录通常是 ~/.config/nvim/。插件、LSP 和 Lua 配置属于另一个层次,先把这里的编辑模型练熟,再决定哪些能力值得自动化。
配置出错时先隔离变量
vim --clean file.txt |
进入 Vim 后可继续检查:
:scriptnames |
verbose 会告诉你某个选项或映射最后在哪里被修改。旧笔记中的 Vundle、YouCompleteMe、固定 Vim 8.2 路径以及全局修改 PowerShell 执行策略都不再放进通用主线:它们依赖具体年代和环境,混在基础命令中反而会缩短笔记寿命。
10. 从真实场景反推命令
场景一:只改当前单词,不想先选中它
把光标放在单词内部:
ciw新内容<Esc> |
如果下一个目标要做完全相同的修改,移动过去按 .。
场景二:复制一段模板,中途还要删除其他内容
复制后用 "0p 粘贴;临时删除且不想污染默认寄存器时用 "_d{motion}。若要长期保存多块内容,用 "ay{motion}、"by{motion} 分别写入有名寄存器。
场景三:只给某一段的每行添加前缀
行可视模式选中目标,执行:
:'<,'>s/^/- / |
命令行中的 '<,'> 会在从可视模式按 : 时自动出现;^ 匹配行首,替换文本是连字符和一个空格。
场景四:把一列名字变成 Markdown 任务项
在第一行录制一个“移到行首 → 插入 - [ ] → 移到下一行”的宏,然后用计数执行。任务需要多个动作且每行结构一致,宏比复杂替换更直观。
场景五:逐个修复项目里的废弃接口
先用 :vimgrep 或 :grep 建立 quickfix 列表,用 :cnext 查看上下文;确认变化可机械执行后,再考虑 :cdo。搜索列表与编辑动作分开,回退和复核都更容易。
11. 一次 30 分钟的练习路线
不要在真实项目里第一次尝试宏。创建一个临时文件,分四轮练习:
- 前 5 分钟:移动与对象。 练
w/b/e、f/;、%、ciw、di"、yap。 - 接着 8 分钟:搜索与重复。 用
/、n/N找目标,完成一次修改,再用.复用;用u和<C-r>往返观察撤销树。 - 接着 8 分钟:寄存器。 依次执行
yy、dd、p、u、"0p,打开:registers对照每一步。 - 最后 9 分钟:批处理。 用
:%normal! A;处理多行并撤销;再录一个只处理一行的宏,用3@a执行。
第二天在真实工作中,只要求自己识别一次“这里可以用文本对象”以及一次“这里可以用点命令”。形成场景联想,比一天背完一张快捷键表更有效。
12. 最小速查表与常见误区
对象编辑 ciw di" yap =ap gqap |
最后再纠正几条很容易从旧资料中继承的误解:
zo打开折叠,zc关闭折叠;G跳到文件末尾,>G或=G才是带范围的缩进操作;.重复最近修改,@@重复最近宏;p读取无名寄存器,不天然等于系统剪贴板;- buffer 是内容,window 是视口,tab page 是窗口布局;
:normal!中的!用来跳过自定义映射,适合可复现的批处理;:set paste是兼容排查手段,不应成为现代终端下的常驻设置;- Vim 内置加密不适合替代密码管理器、磁盘加密或可靠的 secrets 管理。
参考入口
Vim 自带帮助就是与当前版本最匹配的参考资料:
:help user-manual |
这份主文负责编辑语言和工程工作流;涉及 Shell 作业控制、过滤器、外部构建工具的完整说明,请继续阅读:Vim 与 Shell 协作:挂起、恢复与外部命令。