Vim 的编辑语言:操作符、文本对象、寄存器与宏
学 Vim 的第一周,大部分人记下了一张快捷键表:
dd删一行,yy复制一行,dw删一个词。可是光标在一个单词中间时dw只删了半个词,删掉一行再粘贴,出来的却不是刚才复制的内容。
Vim 的命令不是一张需要死记的快捷键表,而是一门小语言:操作符(删除 d、修改 c、复制 y)加上范围(动作 w、$,或文本对象 iw、i(),再加上可选的计数。理解了这个结构,几十个命令可以由十几个零件组合出来;也只有理解了它,才能解释为什么 dw 只删了半个词。
本文在 Vim 9.1 的空配置下,逐个验证这门语言的几个关键部分:动作与文本对象、点号重复、寄存器、宏、:g 与替换、参数列表。实验见文末。
一、动作与文本对象
光标在 quick 的 i 上:
dw → the qubrown fox 从光标删到下一个词的开头
diw → the brown fox 删掉光标所在的整个词,留下两个空格
daw → the brown fox 删掉整个词和它后面的一个空格w 是一个动作:从光标位置移动到下一个词的开头。dw 删除的是这段移动经过的范围,所以光标在词中间时只删了后半个词和空格。iw、aw 是文本对象:它们不关心光标在词的哪个位置,选中的是「光标所在的那个词」(i 表示 inner,不含周围空白;a 表示 a,包含一侧的空白)。
编辑代码时,文本对象往往比动作更合适,因为它表达的是「改这个东西」而不是「从这里改到那里」:
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 标签)。
二、点号:重复上一次修改
第一行 A // checked,然后 j. 两次 → x = 1; // checked | y = 2; // checked | z = 3; // checked. 重复上一次修改,包括操作符、范围和插入的文本。把一次修改设计成「可以被点号重复」的形式(例如用 ciw 而不是 bcw,用 A 而不是 $a),是高效使用 Vim 的关键习惯:修改一次,然后在其他位置按 .。
三、寄存器:删除也会覆盖
第 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(交换逗号两侧):
宏执行 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 与替换:在编辑器里提取和改写
: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 里结果不同。
六、参数列表、缓冲区、窗口与标签页
: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。
配套实验
- codesphere-labs/engineering/vim-editing-language:Vim 9.1(Debian trixie vim-nox 2:9.1.1230-2),空配置:动作与文本对象、点号、寄存器、宏与 :g、very magic 与 \zs、参数列表(验证记录)
参考资料
- Vim 帮助:
:help operator、:help text-objects、:help registers、:help recording、:help :global、:help /\v、:help /\zs、:help arglist - Vim 官方文档(在线版)