Skip to content

Vim 的编辑语言:操作符、文本对象、寄存器与宏 ​

学 Vim 的第一周,大部分人记下了一张快捷键表:dd 删一行,yy 复制一行,dw 删一个词。可是光标在一个单词中间时 dw 只删了半个词,删掉一行再粘贴,出来的却不是刚才复制的内容。

Vim 的命令不是一张需要死记的快捷键表,而是一门小语言:操作符(删除 d、修改 c、复制 y)加上范围(动作 w、$,或文本对象 iw、i(),再加上可选的计数。理解了这个结构,几十个命令可以由十几个零件组合出来;也只有理解了它,才能解释为什么 dw 只删了半个词。

本文在 Vim 9.1 的空配置下,逐个验证这门语言的几个关键部分:动作与文本对象、点号重复、寄存器、宏、:g 与替换、参数列表。实验见文末。

操作符d c y > gU+计数2 3+范围:动作 或 文本对象w $ f, t) | iw aw i( i" ip it光标在 quick 的 i 上dw → the qubrown foxdiw → the brown foxdaw → the brown foxf(a, g(b, c), d),光标在 b 上:ci( → f(a, g(X), d);c2i( → f(X)yy keep,dd trash:p 粘贴 trash(删除覆盖了无名寄存器),"0p 粘贴 keep宏 3@a 遇到没有逗号的一行就停止,后面的行没有处理;:g/,/normal @a 只处理匹配的行替换用 \v 减少反斜杠、\zs 限定替换范围;批量改文件用 :args + :argdo + update
图 1 · 实测(Vim 9.1,空配置):命令读作「操作符 + 计数 + 范围」。光标在词中间时,动作 w 只覆盖从光标到下一个词开头的部分,文本对象 iw、aw 覆盖整个词。删除会覆盖无名寄存器,最近一次复制留在 0 号寄存器。宏遇到错误就停止,用 :g 只对符合格式的行执行。

一、动作与文本对象 ​

text
光标在 quick 的 i 上:
  dw   → the qubrown fox      从光标删到下一个词的开头
  diw  → the  brown fox       删掉光标所在的整个词,留下两个空格
  daw  → the brown fox        删掉整个词和它后面的一个空格

w 是一个动作:从光标位置移动到下一个词的开头。dw 删除的是这段移动经过的范围,所以光标在词中间时只删了后半个词和空格。iw、aw 是文本对象:它们不关心光标在词的哪个位置,选中的是「光标所在的那个词」(i 表示 inner,不含周围空白;a 表示 a,包含一侧的空白)。

编辑代码时,文本对象往往比动作更合适,因为它表达的是「改这个东西」而不是「从这里改到那里」:

text
f(a, g(b, c), d),光标在 b 上:
  ci(   → f(a, g(X), d)       修改最内层括号里的内容
  c2i(  → f(X)                计数 2:外面一层括号

常用的文本对象:iw/aw(词)、is/as(句)、ip/ap(段落)、i(/i[/i{/i<(各种括号)、i"/i'(引号)、it(HTML 标签)。

二、点号:重复上一次修改 ​

text
第一行 A // checked,然后 j. 两次 → x = 1; // checked | y = 2; // checked | z = 3; // checked

. 重复上一次修改,包括操作符、范围和插入的文本。把一次修改设计成「可以被点号重复」的形式(例如用 ciw 而不是 bcw,用 A 而不是 $a),是高效使用 Vim 的关键习惯:修改一次,然后在其他位置按 .。

三、寄存器:删除也会覆盖 ​

text
第 1 行 yy,第 2 行 dd,然后 p 再 "0p → keep | end | trash | keep

复制的「keep」被后来删除的「trash」顶掉了:d、c、x 删除的内容都会进入无名寄存器,p 粘贴的是最近一次删除或复制的内容。最近一次复制的内容一直保存在 0 号寄存器里,"0p 能把它取回来。

其他常用的寄存器:"a 到 "z 是命名寄存器(大写字母表示追加);"_ 是黑洞寄存器,删除时不想覆盖任何内容就用 "_d;"+ 是系统剪贴板(需要 Vim 编译时支持);". 是上一次插入的文本。

四、宏:录下来的命令序列 ​

宏就是把一串按键存进寄存器,再用 @ 执行。实验里的宏把 a,1 变成 1,a(交换逗号两侧):

text
宏执行 3@a           → 1,a | 2,b | 3,c
第 2 行没有逗号时 3@a → 1,a | d | c,3       第 2 行出错,宏停止,第 3 行没有处理
:g/,/normal @a       → 1,a | d | 3,c       只对有逗号的行执行

宏中的任何一个命令失败(这里是 dt, 找不到逗号),整个宏连同剩余的计数都会停止。这既是保护(不会在错误的位置继续修改),也是陷阱:处理大量数据时,一行格式不对就会让后面的行全部没有处理。用 :g/模式/normal @a 只对匹配的行执行宏,可以避开不符合格式的行。

录制宏的几个习惯:开头先用 0 或 ^ 把光标放到确定的位置;结尾移动到下一个要处理的位置(j);尽量使用文本对象和查找(f、t),而不是固定的列数。

五、:g 与替换:在编辑器里提取和改写 ​

text
:g/^TODO/normal A (alice)                       → TODO fix login (alice) | ok | TODO add test (alice)
:%s/\v(\w+)\s*\=\s*(\w+)/\2 = \1/              → 10 = width | 20 = height
:%s/price: \zs\d\+/***/                         → price: *** | price: ***

:g/模式/命令 对所有匹配的行执行一个 Ex 命令,配合 normal 可以执行任意普通模式命令。

替换命令里的正则有 Vim 自己的语法:默认模式下 +、(、| 都要加反斜杠;开头加 \v(very magic)之后,语法接近其他语言的正则。\zs 和 \ze 标出「真正要替换的部分」从哪里开始和结束,前后的内容只用来定位,相当于其他语言里的环视。同一个正则在不同语言里的差异,见同一个正则,在 Java、Python、JavaScript 里结果不同。

六、参数列表、缓冲区、窗口与标签页 ​

text
:args *.txt | argdo %s/foo/bar/ge | update   → f1.txt: bar one;f2.txt: two bar;skip.md: foo

几个容易混淆的概念:

  • 缓冲区:打开的文件在内存里的内容。:ls 列出所有缓冲区,:b 名字 切换。
  • 窗口:查看某个缓冲区的视口。一个缓冲区可以同时显示在多个窗口里。
  • 标签页:一组窗口的布局,不是「一个文件一个标签」。
  • 参数列表:启动时或用 :args 指定的一组文件,是批量处理的单位。

批量修改多个文件,用参数列表比打开所有缓冲区更可控::args 精确地选定文件,:argdo 对每个文件执行命令,e 标志让没有匹配的文件不报错,update 只在文件被修改时写入。

七、检查清单 ​

  • 把命令读成「操作符 + 范围 + 计数」;修改一个对象时优先用文本对象。
  • 把修改设计成可以被 . 重复的形式。
  • 删除会覆盖无名寄存器;取回最近的复制用 "0p,不想覆盖用 "_d。
  • 宏开头确定光标位置、结尾移动到下一处;用 :g 只对符合格式的行执行。
  • 替换时用 \v 减少反斜杠,用 \zs / \ze 限定替换范围。
  • 批量修改多个文件用 :args + :argdo + update。

配套实验

参考资料

  • Vim 帮助::help operator、:help text-objects、:help registers、:help recording、:help :global、:help /\v、:help /\zs、:help arglist
  • Vim 官方文档(在线版)

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权