撤销、重复与宏:重复劳动的终结者
学习目标
- 搞懂撤销单元:一次插入会话、一次普通模式修改、一条
:命令各算一步 —— 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 打十个字 Esc | 1 个 | 十个字整体消失,回到按 A 之前 |
| ciw 补新词 Esc | 1 个 | 旧词回来、新词消失,一口气 |
| 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. 如果本章只记住三件事
- 撤销按意图计:一段插入会话、一条命令各算一步,u 退整段、Ctrl+r 拿回来 —— 这是敢批量操作的安全网;
- `.` 单格记忆,宏成套重放:一个动作用「移动 + .」,一套动作用 qa…q + @a,范围明确上
:normal; - 宏设计三纪律:结尾自动推进(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 毕业。
常见陷阱
:42 这类绝对定位,每次回放都把光标甩到同一个地方 —— 第一次对,第二次开始全乱。宏内导航只用相对移动:j/n/w/f,或以「当前行」为锚的操作(yyp 就不依赖位置)。
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。