Vim 修炼场
CHAPTER 10 · 进阶篇 终

撤销、重复与宏:重复劳动的终结者

预计阅读 35 分钟(含练习约 1.5 小时)· 前置:第 5、8、9 章 · 本章演示 7 个

学习目标

  • 搞懂撤销单元:一次插入会话、一次普通模式修改、一条 : 命令各算一步 —— u 退的是「一段意图」不是一个字母,Ctrl+r 沿时间线前进
  • 掌握 .(dot)的机制与边界:单格记忆,随时被新修改覆盖;练成「先完整做一次,再移动 + 重复」的黄金节奏
  • 学会宏:qa 录制、@a 回放、@@ 再来、22@a 连发 —— 把一段按键存进寄存器批量重放
  • 用好数字引擎 Ctrl+a / Ctrl+x,配合宏批量生成与修改编号
  • 入门 :normal:把任意普通模式按键批量发给每一行,并与 :g 组合
  • 建立宏的三条设计纪律:结尾自动推进、只用相对移动、连发前先试一发 —— 以及录砸了之后的修复套路

1. 重复的三个等级:本章地图

先承认一个事实:写代码时,你做的修改经常是同一个修改的几十次复制。给 20 行补分号、把一个变量名的 8 处出现全改掉、给 30 个常量统一加前缀 —— 鼠标用户的命运是每处都「选中、删、打字」地重做一遍;Vim 用户的答案是:认真做一次,剩下的交给重复工具。

Vim 对「重复」的回答分三个等级,按你要重复的东西选:单动作的重复用 .(重放最近一次修改,零成本);多动作成套的重复用宏(把一串按键录进寄存器,随时整段重放);范围明确的批量用命令(:normal 把按键一次性发给每一行)。三个工具不是三选一,是同一根杠杆的三档:先用最低档,. 撬不动了再升宏,宏都嫌啰嗦了再上命令。

而压舱这一切的是撤销。批量操作之所以敢放开手脚,是因为 u 一步就能回滚一整条命令、一整段插入。所以本章把撤销放在最前面讲透 —— 它是后面三档工具共同的安全网,也是你敢「先斩后奏」的底气。学完本章,你对「改 100 处」这件事的心理感受会从「不可能」变成「做一次,然后重复」。

2. u 与 Ctrl-r:会话即单元

Vim 的撤销不是字符级的,是意图级的:你按下一个修改、打完所有想打的字、退出 —— 这一整段算一个撤销单元。先看最容易被低估的规则:插入模式里打多少字,都只是一个单元:

演示里两处关键:①那段注释打了十几个字符,u 一步整体消失 —— 不是退一个字母,是退「那次想加注释的意图」;②Ctrl+r(redo)沿时间线反向走,退多了随时拿回来,u 与 Ctrl-r 之间可以反复横跳地审查改动。把常见动作折算成单元数,一张表看清:

你做的事撤销单元一次 u 退掉什么
A 打十个字 Esc1 个十个字整体消失,回到按 A 之前
ciw 补新词 Esc1 个旧词回来、新词消失,一口气
x x x(三次单发)3 个只退一个字符 —— 单发修改逐个计
:%s/a/b/g(几百处替换)1 个整条命令的所有替换一次回滚
:g/TODO/d(删了几十行)1 个删掉的行全部回来(第 8 章演过)
宏 10@a(回放十次)逐次可退连按 u 一次次往回退

两个补充件:U(大写)是「行级撤销」—— 一键退掉本行最近的所有修改,行内改砸了比连按 u 更快,但它自己也是个修改(会占用 . 的记忆,小心);至于「撤销分支」(退了又改,旧时间线存成树,用 g- 走旧分支)属于冷知识,IDE 插件支持参差,知道有这回事即可,日常线性时间线完全够用。设计哲学值得咀嚼一句:把「一次意图」定为单元,意味着撤销的终点永远是上一个完整、正确的状态 —— 你不会停在「打了半句的注释」上。这也是 Vim 鼓励小步提交意图的原因:每个单元小而完整,u 的分辨率才刚好够用。

3. `.` 的机制:单格记忆

.(dot)重放最近一次修改。它大概是 Vim 里性价比最高的一个键 —— 但前提是认清它的两条脾气:它只记修改,不记移动;它的记忆只有一格,随时被下一个修改覆盖。

演示的后半段是特意安排的「事故」:x 之后 . 好好用;一次 dd 之后,. 重复的变成了 dd —— z 行整行消失。`.` 是单格记忆,新修改随时覆盖旧记忆,这不是缺陷,是它的设计:你永远不需要猜它记的是哪一次 —— 就是最近这一次。哪些动作算「修改」,一张表划清:

算修改(`.` 会记)不算修改(不影响 `.`)
x d… c… y… p ~ J所有移动:hjw f / n % …
整段插入会话:A+文字+Esc(入口也算!)Esc 本身、模式切换本身
>> << ==(缩进也算修改)滚动 Ctrl+d、窗口切换
一条 : 命令(:s、:g、:normal)gv Ctrl+o 等跳转类

注意第二行那个括号:「入口也算」。. 重放的不只是你打的字,还有你进入插入的方式 —— 用 A 进入,重放时也会到行尾;用 ciw 进入,重放时也会先吃掉光标下的词。这就解释了一个流传已久的 Vim 谚语:高手按 Esc 的频率高得反常 —— 因为只有完整闭合的会话才能被 `.` 存档,常开着的插入模式等于把重复工具闲置了。选入口时多想半秒(该用 A 还是 $a?该用 ciw 还是 dw 再 i?),入口本身带着「相对定位」的语义,重放才不会跑偏。

4. `.` 的黄金节奏:移动 + 重复

认清机制之后,`.` 的正确用法自然浮现:把第一个目标完整、认真地做对,然后只剩「移动到下一个目标 + 按点」。看四行补分号的标准作业:

A ; Esc 三键是「投资」,之后每组 j . 两键一行,改动越多摊销越便宜。这个「移动 + 重复」的节奏在本站已经出现过两次:第 8 章的 * + n + . 改名流水线(n 负责「下一个目标」),第 7 章的 Vjj > . 逐行加缩进 —— 三处其实是同一个模式:移动键负责导航,点负责开火,手指在两种角色间交替,像打鼓。

`.` 的适用边界也在这时显形:它只会重放一个动作(尽管这个动作可以是一整段插入会话)。当每处的修改需要两个以上动作 —— 又改行首又补行尾、又删一行又给下一行加编号 —— `.` 就记不住了,按了点也只是重复最后一小步。这不是 `.` 的失败,是它的分工到此为止:单动作重复交给点,成套动作交给宏。下一节,升级。

5. 宏:把按键序列存进抽屉

宏(macros)一句话讲完:把一串按键录下来,存在一个寄存器里,之后一键整段重放。第 9 章埋的伏笔在这里兑现 —— 命名寄存器 "a–"z 里存的除了文本,还可以是按键序列;所谓「宏」,就是当按键序列用的寄存器。录制语法三件套:

qa 开始录制(存进 a)→ 干活(照常按)→ q 结束;回放用 @a(把 a 里的序列再敲一遍),@@ 重复最近一次回放的宏。看把 §4 做不到的「一行两个动作」交给宏:

几个要点从演示里直接读出来:①录制期间一切照常生效 —— 你一边录一边看到第一行被真实加工,所见即所录;②宏的结尾是一个 j,于是每次回放结束,光标自动停在下一个工作位上 —— 这是宏设计的第一条纪律:让宏自己往前走,回放才能连发;③回放后光标落在第三行,正好是下一个待办 —— 宏是「带位置语义的程序」,不是简单的录像回放。还有个冷知识:宏执行完毕后,. 记住的是宏里做的最后一个修改 —— 偶尔能拿来偷懒,但别依赖,宏的重复请直接用 @a。

为什么宏要用 "a–"z 而不是别的寄存器?因为命名抽屉「只有你显式写它才变」—— 录进去的宏不会被后续的 y/d 意外顶掉(无名寄存器一天被顶几十次)。习惯上给宏固定抽屉:比如 a 永远放「当前任务宏」,录废了直接覆盖重录,不用换名字。宏的按键序列本质是文本,所以 :reg a 打开清单能看到它存了什么 —— 调试宏时的第一件事就是看它(§9 展开)。

6. Ctrl-a / Ctrl-x:数字引擎

普通模式下 Ctrl+a 把光标行内最近的数字加一,Ctrl+x 减一。「最近」的精确定义:光标所在或之后的行内第一个数字 —— 光标不用精确落在数字上,它自己会找。它还认得负号(-1 会正确变成 -2),带计数前缀则一步到位:5Ctrl+a 直接 +5。单用时它是「调参神器」(改数组长度、改重试次数),配上宏,它是「序列生成器」:

演示里宏录的是 yypCtrl+a 四下:复制当前行、贴到下面、把副本的数字 +1。注意它的精妙之处 —— 这个宏天生自推进:回放结束后光标正好停在新行上,也就是下一次的工作位,连结尾的 j 都不用。这是宏设计纪律一的另一种达成方式:与其「做完挪过去」,不如让操作的副产品就是正确落位。批量生成测试数据、给几十行加编号、按序号批量改配置,全是这一个宏的变体;反过来,编号要减小、要倒序,把 Ctrl-a 换成 Ctrl-x 即可。

实战里宏与数字的组合还有个高频变体:重排编号。一堆 id = 7, id = 3, id = 12 乱序的行,先全部改成同一个数(:%s/id = \d\+/id = 0/ 之类),再用 yypCtrl+a 型宏或直接 Ctrl+a j . 逐行 +1 —— 「归零再生长」比逐个改省心得多。思路比按键重要:看到「连续编号」,条件反射应该是 Ctrl-a 而不是手敲数字。

7. :normal:把按键批量发货

第三档工具走命令行。:normal {按键} 的语义:对 range 范围内的每一行,把后面的按键当作在普通模式下敲进去。演示 3 的补分号任务,它的终极形态是一行命令:

三个知识点从演示里长出来:①不用写 Esc —— :normal 在按键序列结束时自动收尾,强制回到普通模式,所以 A; 后面什么都不用补;②一条 :normal 是一个撤销单元 —— 四行的修改一个 u 全退,批量操作配齐了后悔药;③range 位置随便换 —— :% 全文件、:2,3 行区间,而最凶的组合是接在 :g 后面::g/console/normal A // TODO 对每个 console 行行尾追加备注,:g/^\s*$/normal dd 删空行 —— 第 8 章「按内容挑行」与本章「按按键施工」合体,几乎能表达一切批量编辑。

它和 :s 的分工一句话::s 改的是「文本内容」,:normal 干的是「按键动作」。给行尾加分号,用 :s/$/;/ 其实也行,但要「行尾加分号、行首再去个空格、再砍掉行尾注释」这种多动作复合,:s 要写三条还容易正则打架,:normal 一串 A;……直接复用你的普通模式手感。判断标准:想不出正则怎么写、但手指会按 —— 就用 :normal。

8. 可连发的宏:搜索驱动

宏的真正威力在「连发」:22@a 把宏连放 22 次。但连发有个前提 —— 每次执行结束,光标必须正好落在下一个目标上。行规则的数据(演示 5)靠结构自推进;不规则的目标(散落在各处的变量名),靠搜索导航。把改名任务升级成宏版:

这套「* 设好查找词 → 录「改当前 + n 跳下一个」→ N@a 连发」是宏的头号经典模式,三个设计细节值得逐个拆:①查找词要在录制前设好(先 * N 侦查一圈再按 q),宏里的 n 才有词可跳;②宏以 n 结尾,执行完光标自动指向下一个猎物,连发的每一步都「有的放矢」;③失败即刹车 —— 最后一次执行中 n 找不到下一个匹配,出错的一步会中止宏,连发自然停止,不会空转 20 次。连停止条件都是免费的,这是搜索驱动宏最优雅的地方。

连发的操作纪律只有一条:先试一发,再连发。录完宏先按一次 @a,亲眼确认第二处被正确加工、光标落点正确,然后再放心 18@a —— 一发验证的成本是一秒,连发出错的成本是按十几次 u。顺带把「22@q」这个江湖黑话翻译了:它不是特殊按键,就是「数字 22 + @ + 寄存器名 q」的连发,你在演示 5 里已经亲手放过了。

9. 宏的调试与修复:录砸了怎么办

宏没有保存提示、没有断点,砸了很正常,修复套路却很固定,按顺序用:第一步永远是 u —— 宏的每次回放是独立的撤销单元,连按 u 逐步退干净(IDE 里 Cmd-z 同效);第二步判断砸在哪:是宏内容错了(重录),还是起始位置错了(退干净、摆好位置重放)?内容错的标准修法是覆盖重录 —— qa 会直接覆盖 a 抽屉,不用先删除什么;只有「宏大体对、只差最后一下」时才用追加录制 qA(大写寄存器名,往 a 里续按键,和第 9 章 "A 追加文本同理)—— 比如录完发现忘了结尾的 j,qA j q 补上即可。

排查宏内容,最快的办法是 :reg a —— 宏就是寄存器,清单里能看到它存的按键序列(比如看到 ciwcount^[An;j,一眼发现忘了 Esc 的位置)。三个预防习惯比修复更值钱:①录前摆好位 —— 光标停在第一个目标的正确位置、查找词设好,再按 q;②样板行先手做一遍 —— 想清楚每一步,手指顺过一遍再录,录制时的犹豫和错按会全部被记进去;③宏内只用相对移动(j w n f),绝不用 gg/G 这种绝对跳转 —— 第二次回放时它们会把光标甩到同一个地方,连发立刻全错。这三条写在陷阱区,值得抄到便签上。

10. 决策表:四种重复工具怎么选

本章加多光标,重复类工具齐了。按意图选:

场景首选理由
同一处小修改,要做 N 次,每处一个动作.零录制成本,「移动 + 重复」两键一行
每处要做两个以上动作(成套加工)宏 qa…一串按键整体重放,连发
全文件/选区做同一加工,一句话能圈范围:normal一行命令批量发货,单撤销单元
按内容挑行做动作(删日志行、给 TODO 行加备注):g/…/normal …:g 挑行 + normal 施工
批量生成/修改连续编号Ctrl+a + 宏yyp Ctrl-a 自推进,或归零再生长
散落各处的相同词,每处还要微调多光标(gb / Cmd+d)内容匹配圈选,逐处可调(第 7 章 §9)
真正的语义重命名IDE 的 F2跨文件懂作用域,文本工具别越权(第 8 章 §9)

选型的「判断三问」:①每处的修改是一个动作还是一套动作?一个动作用 .,一套动作用宏。②范围能用一句话(行号/选区/模式)圈定吗?能就用 :normal / :g,范围不规则靠搜索或块。③目标是「同内容」还是「同位置」?同内容想 :s、*+.、多光标;同位置想块选、宏。三问下来工具基本唯一 —— 剩下的就是把手感练熟。

11. 每日训练计划(30 分钟 × 3 天)

天时间盒内容通过标准
第 1 天0–10 分钟重放演示 1–2,读 §1–§3;靶场做 5 组「两单元修改」,数 u 次数验证粒度能预判 u 退到哪
10–30 分钟练习 1 + 练习 2 前半(`.` 节奏)j `.` 不看键盘
第 2 天0–10 分钟重放演示 3–5,读 §4–§6;宏三连各录 3 遍qa…q 收录自如
10–30 分钟练习 2 后半 + 练习 3@a / @@ / 5@a 三种回放都顺
第 3 天0–10 分钟重放演示 6–7,读 §7–§10;抄一遍设计三纪律能复述三纪律
10–30 分钟练习 4:盲测毕业考10 题对 7 题,本章毕业

12. 如果本章只记住三件事

  1. 撤销按意图计:一段插入会话、一条命令各算一步,u 退整段、Ctrl+r 拿回来 —— 这是敢批量操作的安全网;
  2. `.` 单格记忆,宏成套重放:一个动作用「移动 + .」,一套动作用 qa…q + @a,范围明确上 :normal;
  3. 宏设计三纪律:结尾自动推进(j / n / 操作副作用落位)、只用相对移动、连发前先试一发 —— 砸了就 u,重录永远不亏。

单文件的武功至此全部练成。下一章把镜头拉远:多文件与窗口 —— 缓冲区、分屏、标签页,以及在 IDE 里 vim 与原生功能如何分工。

练习

练习 1:撤销单元实验(10 分钟)

靶场任选文件:①A 打一段 10 字注释 Esc,再 o 打一行字 Esc,数一数 u 几次回到原状(答:2 次);②插入模式里打五个字、按回车继续打五个字、Esc —— 验证 u 一次退掉整段(包括换行);③一条 :%s/let/const/g 后一次 u 全回滚;④改砸一行后用 U 行级撤销。通过线:每步动手前先说出「u 会退到哪」。

练习 2:dot 节奏与边界(10 分钟)

①造 8 行缺分号代码,用 A;Esc + j . ×7 补完,计时;②复现边界事故:某行 x 删一字符,连用两次 j .,再 dd 删一行,按 . 观察「记忆被覆盖」,u 救回;③第 8 章流水线复习:* + ciw + Esc + n . 改名 6 处。通过线:①全程不看键盘,②能提前说出 `.` 此刻记的是什么。

练习 3:宏三连(10 分钟)

三个任务各录 3 遍:①双动作宏:qa ciwconst Esc A;Esc j q,然后 @a、@@ 逐行放完;②数字宏:一行 id = 1,录 yypCtrl+a,6@a 生成到 7;③搜索宏:* N 后录 ciw 新名 Esc n,N@a 连发改完全部。每个任务结束后 :reg a 看一眼宏内容。通过线:三任务的连发都不出错、不用修。

练习 4:盲测毕业考(10 分钟)

10 题先说答案再看:①重做刚撤销的修改 ②行级撤销 ③重复最近一次修改 ④把宏录进 b ⑤执行 b 里的宏 ⑥重复最近执行的宏 ⑦宏连发 7 次 ⑧光标行数字 +1 / −1 ⑨一行命令给全文件行尾加分号 ⑩给所有含 console 的行行尾加备注。答案:Ctrl-r / U / . / qb … q / @b / @@(或 @ @)/ 7@b / Ctrl-a / Ctrl-x / :%normal A; / :g/console/normal A 备注。10 对 7 毕业。

常见陷阱

🕳 插入模式里用方向键/鼠标挪光标,单元被拆散 插入过程中用方向键或鼠标移动,会把「一段会话」拆成多个撤销单元:u 只退一半,. 的重放也从移动处截断 —— 加分号加到一半的诡异状态就是这么来的。纪律:插入里要挪位置,先 Esc 挪完再进;实在要临时挪,用 Ctrl+o 单发跳(第 6 章)也能保住单元完整。
🕳 从错误的位置开录宏,连发全错 光标没摆对、查找词没设好就按 q,录出来的宏从第二发开始全部加工错位置 —— 连发 20 次就是 20 处错改。录前三件事:样板行先手动做一遍、光标摆到下一个目标、搜索宏先 * 设词。录完先单发一发验证,再连发。
🕳 宏里用了 gg / G 等绝对跳转 宏内的 gg、G、:42 这类绝对定位,每次回放都把光标甩到同一个地方 —— 第一次对,第二次开始全乱。宏内导航只用相对移动:j/n/w/f,或以「当前行」为锚的操作(yyp 就不依赖位置)。
🕳 Ctrl-a 没反应,以为快捷键坏了 Ctrl+a 找的是本行光标处及其后的第一个数字:数字在光标前面,它够不着;数字在别的行,更不关它事。没反应时先把光标挪到数字前(w 多跳几下或 f 数字),再按。另外注意终端环境下 Ctrl-a 可能被终端抢占(行首快捷键),IDE 内嵌 vim 无此问题。

VSCode / IDEA 差异

VSCode(VSCodeVim)

  • u/Ctrl+r 与 VSCode 的撤销栈打通:粒度尽量保持 vim 会话级,个别场景与 Cmd+z 有出入 —— 习惯用哪套就固定用哪套,别来回混
  • .、q 录制、@a/@@ 连发、Ctrl+a/x:VSCodeVim 全部支持,照本章用
  • :normal 可用;复杂按键序列偶有与终端 vim 行为不一致的情况,结果怪异就改用宏回放
  • 状态栏会显示正在录制的寄存器(如 RECORDING @a);VSCode 原生 Cmd+z/Cmd+shift+z 一直可用作兜底

IntelliJ IDEA(IdeaVim)

  • IdeaVim 对本章内容实现完整:q/@/:normal/Ctrl+a 的行为与终端 vim 一致,撤销粒度同样是会话级
  • u/Ctrl+r 与 IDEA 的撤销(Cmd+z)整合在同一栈里,来回切换不会乱
  • :reg a 能看到宏的按键序列,排查宏内容同样好用;宏还可以写进 .ideavimrc 预定义(进阶,第 12 章给配置全文)
  • IDEA 自带的宏录制(Tools → Record Macro)与 vim 宏是两套独立体系 —— 选一套用,别交叉,文件归属清楚才不乱

本章速记

按键 / 命令作用按键 / 命令作用
u / Ctrl+r撤销 / 重做(按会话单元)U撤销本行最近所有修改
.重放最近一次修改(单格记忆)j . / n .移动 + 重复的黄金节奏
qa … q录制宏进 a 抽屉@a / @@回放 a / 重复最近宏
22@a宏连发 22 次(失败自动停)qA … q往 a 追加按键(补录)
Ctrl+a / Ctrl+x行内最近数字 +1 / −1:%normal A;按键批量发给每行(免 Esc)
:g/模式/normal …按内容挑行执行按键:reg a查看宏存的按键序列

一句话心法:一个动作用 .,一套动作用宏,整片范围用 :normal;宏要自动推进、只用相对移动、连发前先试一发 —— 砸了就 u。