Vim 是一个拥有魔力的文本编辑器——这并不是比喻,而是说你在 Vim 中真的可以念咒语来操纵文本。看看我们的键盘,在 normal 模式下几乎每个键都有特定的功能,尤其是其中的数字和 26 个字母。如果把每个键都看作 Vim 这门语言中的单词,那么只要依据特定的语法,通过连续击键来遣词造句,就能施展操作文本的魔法。并且 Vim 语言的语法简单到用一句话就能描述:

verb + noun

下面就来简单讲讲魔法的基本法。

vim_keys

语法规则

Vim 的语法翻译过来就是,对什么文本(名词 noun)做什么操作(动词 verb)。其中动词指的是 Vim 中被称为 operator 的命令,例如小写的 d 键就是表示删除的 operator。但是单独按下 d 并不会起效,我们还需要指定动词的作用对象。Vim 中关于光标移动的命令被称为 motion,例如我们熟知的 hjkl 就是表示上下左右的 motion,w 是表示跳到下一个单词开头的 motion。Motion 作为名词使用时指代光标移动范围内的文本,所以句子

operator + motion

就表示对 motion 移动范围内的文本执行 operator 的操作。例如组合 dw 就表示删除当前光标到下一个单词开头前的文本。不同于英语,Vim 语法中动词和名词前都可以加上数字,以表示重复动词或名词。例如 2w 表示跳跃到下下个单词开头,那么 d2w 就表示一次性删除两个接下来的单词;同时 2dw 表示删除下一个单词的操作执行两次;同理,2d2w 就表示删除 2 * 2 = 4 个单词。于是句子可以补充成

[count] operator + [count] motion

其中 count 是大于 0 的整数,方括号表示可有可无。

除了 motion,还有一类被称作 text object 的命令能作为名词。顾名思义,text object 表示具有某种结构的一段文本对象,具体形式为

text-object = modifier + object

其中 object 是具体的文本对象,modifier 是对其范围的一点补充修饰。例如 ap 就是一个 text object,其中对象 p 表示段落,修饰词 a 表示在整个段落范围的基础上,再包含段落前或段落后的空行。不同于 motion,text object 并不能单独使用,而是必须放在 operator 之后才能发挥作用。于是组合 dap 就表示删除一整个段落及与之相邻的空行。同样可以总结为句子

[count] operator + [count] text-object

相比于 motion 的句子,这个句子不用关心光标的具体位置,只要我们的光标落入了文本对象的范围内,Vim 会自动找出文本对象的起始范围进行操作。

至此 Vim 的语法基本上就讲完了,没错就这么点内容,但其中蕴含的思想是很值得玩味的。一般的文本编辑器只能提供非常原子化的操作:光标只能上下左右移动,字符只能单个单个增删。但 Vim 将具体的操作、光标的移动模式和结构化的文本分别抽象为 operator、motion 和 text object,再将它们映射到单个按键上,并按语法赋予其相互组合的能力,使编辑文本的逻辑能用简单的命令序列具象化地表达出来。这种操作哲学是一般的文本编辑器所欠缺的。

正如学英语不能只学语法不背单词,Vim 里我们也需要掌握动词和名词才能正常造句,更别说实践过程中的许多迷惑点都是源于对词汇性质的不了解。所以下面继续来介绍常用的词汇。

常用的 operator

Vim 共有 16 个 operator,但最常用的无非以下几个:

  • d:取自 delete,表示删除。例如 dw 表示删除当前光标到下一个词之前的内容。
  • c:取自 change,表示替换,相当于 d 之后自动进入 insert 模式。例如 cw 效果同 dw,但删除完毕后会进入 insert 模式以便马上输入新的替换文本。
  • y:取自 yank,表示复制到寄存器中。例如 yw 表示复制当前光标到下一个词之前的内容。因为词与词之前可能有空格或标点,所以 yw 会把这些多余的间隔也复制进去。
  • gu:把文本变成小写(lowercase)。这是一个两个键组成的 operator,例如 guiw 能把一个词变成全小写(其中文本对象 iw 会在后面讲解)。
  • gU:把文本变成大写(uppercase)。例如 gUiw 能把一个词变成全大写。
  • >:向右缩进一个 tab 的距离。默认作用于行,所以即便是 >w 也会使整行向右缩进。一个比较有用的例子是 >ip>ap,表示使整个段落向右缩进。
  • <:向左缩进一个 tab 的距离。用法同 >

单个 operator 后面必须接一个名词才能起作用。但当 operator 的按键被重复两次时,就可以省去名词,此时表示作用于光标所处的这一行。例如 dd 表示删除当前行,yy 表示复制当前行,>> 表示当前行向右缩进。此外也可以加上重复次数,例如 3dd 表示删除从当前行开始往下共 3 行。

作为对 y 的补充,提一下并非 operator 的粘贴命令 p:小写的 p 表示在当前光标左边(当前行上面)粘贴字符(行),而大写的 P 表示在当前光标右边(当前行下面)粘贴内容(行)。

常用的 motion

Motion 有两个非常重要的属性需要预先说明一下。

首先,若 motion 的移动发生在行与行之间,就称其是 linewise 的;若移动发生在字符间,就称其是 characterwise 的。例如 jk 就是 linewise 的,而 w 显然是 characterwise 的。

其次,motion 还拥有一个能影响到其作用范围的开闭性。以一个 characterwise motion 为例,若 operator + motion 组合的作用范围不包含 motion 移动范围的右边界,则称这个 motion 是 exclusive 的,反之则称为 inclusive 的。对 linewise motion 同理,根据句子的作用范围是否包含 motion 移动范围的下边界(即最后一行)来决定开闭性,不过一般 linewise motion 都是 inclusive 的。例如常用的 w 就是一个 exclusive motion,单独使用它会将光标跳到下个词的第一个字符处,但 dw 却会点到为止,刚刚好删除到那个字符之前。再比如 jk 都是 linewise motion,dj 会删除当前行和下一行,dk 会删除当前行和上一行。

这里恐怕有点绕,所以用图展示一下

exclusive_inclusive

其中绿色方块是 block 形式的光标,单向箭头是 motion 的起止点,花括号指示句子的作用范围。可见对于 exclusive 的 motion 来说,移动的起止点围成的范围和句子的作用范围总是相差一个右边界字符;而对 inclusive 的 motion 来说,两种范围是相同的。

Vim 中 motion 相当多,不信可以看看本文头图中的绿色按键有多少。这里仅介绍常用的几个:

  • hjkl:上下左右移动,其中 jk 是 linewise 和 inclusive 的,而 hl 是 characterwise 和 exclusive 的。所以 dl 只会删除当前光标处的字符,等价于 x;而 dh 会删除当前光标左边的一个字符。
  • wW:跳到下一个词的第一个字符处,是 exclusive 的。大小写的区别在于,小写形式作用于 word,大写形式作用于 WORD(其中文本对象 word 和 WORD 会在后面讲解)。
  • bB:跳到上一个词的第一个字符处,是 exclusive 的。
  • eE:跳到下一个词的最后一个字符处,是 inclusive 的。
  • gegE:跳到上一个词的最后一个字符处,是 inclusive 的。
  • 0^$0 表示移动到本行的第一列,^ 表示移动到本行第一个非空白字符处,而 $ 表示移动到本行的最后一列。其中 0^ 是 exclusive 的,而 $ 是 inclusive 的。且只有 $ 前可以加数字,表示移动到从当前行开始下面第 n 行的末尾。
  • fF:取自 find,在本行搜索指定的字符并将光标移动过去。以当前光标为起点,小写的 f 表示向后搜索,大写的 F 表示向前搜索,前者是 inclusive 的,但后者却是 exclusive 的。f 后必须接目标字符,例如 fa 会跳到当前光标后第一次出现字符 a 的位置,而 2fa 则会跳到第二次出现的位置。若没有找到,则光标不会发生移动。
  • tT:取自 till,基本同 fF,但会恰好停在搜索结果前。例如 ta 会跳到 fa 终点的前面一格,所以何时使用 ft 取决于我们对边界的处理。
  • ;:重复上一个 fFtT 的移动。例如当本行有三个 a 字母时,fa 会使光标跳到第一个 a 上,此时按下 ; 便相当于重复了 fa 的操作,跳到第二个 a 上,再按又会跳到最后一个 a 上。
  • ,:类似于 ;,不过是按反方向移动。还是三个 a 的例子,按 , 会跳回上一个 a 的位置。
  • {}:跳到上一个/下一个段落边界(即空行),是 exclusive 的。
  • G:若前面加数字,表示跳到指定行;若不加数字则表示跳到最后一行,且是 linewise 和 inclusive 的。例如 dG 表示删除当前行到最后一行的全部内容,d2G 表示删除当前行到第二行的全部内容。
  • gg:加数字时的行为同 G,但不加数字时则表示跳到第一行。例如 dgg 表示删除当前行到第一行的全部内容,等价于 d1Gd1gg

常用的 text object

第一节提过

text-object = modifier + object

其中修饰词实际上只有两个:ia,字面义分别是单词 inner 和冠词 a,但具体效果需要结合 object 来看。所以现在来介绍常用的 object:

  • word:Vim 中把由字母、数字或下划线等非空白字符构成的字符序列称为 word,word 之间由空白字符(空格、制表和换行)或标点符号分隔。在命令中用 w 表示。iw 仅表示一个 word 含有的所有字符,而 aw 还会额外包含前后的空白字符,并且当前后都有空白时则只包含后面的空白。若光标的起始位置就是在 word 前后的空白上,aw 的范围又会发生变化——这里就不细讲了,烦请读者自己尝试一下。
  • WORD:条件更宽松的 word,只要是非空白字符的序列都能算是一个词。例如 apple,banana 算是两个 word,但只能算一个 WORD。在命令中用大写的 W 表示。
  • paragraph:即视觉上行与行相连的整段文本,段落之间一般通过空行(可含空白字符)分隔。在命令中用 p 表示。ip 表示仅作用于段落的所有行,而 ap 类似于 aw,会额外包含前后的空行。
  • 括号:表示括号圈起来的文本块(可以分行),圆括号、方括号和花括号等皆可。这里以圆括号为例,在命令中用 () 表示。i( 仅表示括号内的文本,而 a( 则会包含括号本身。例如 di(ci( 就是非常实用的两个组合命令。
  • 引号:表示引号圈起来的文本,单引号和双引号皆可,可惜只限于本行。以双引号为例,i" 仅表示引号内的文本,而 a" 则会包含引号本身以及引号前后的空白。同样 di"ci" 非常便于修改程序中字符串的内容。

结语

看到这里,你应该能一窥 Vim 的魔力了吧——赋予模糊不清的操作以名字,再按韵律吟唱这些名字,魔法就会出现。如果再加上 . 命令和宏的配方,更是能让魔法自动生出更多魔法,可惜我也只是刚入门的学徒,以后有机会再来介绍更多。文中存在的不妥之处还请读者多多指出。

参考链接

VIM 中文帮助:有关移动的命令

Vim Grammar

Learn-Vim Ch04. Vim Grammar

Vim终极指南:所思即所得