Vim 简介

最近有兴趣折腾了一下 Vim,不免对这个上古神器的强大感到震撼。这篇文章简要记录下基本操作,只能说是冰山一角。

Vim 有三种基本模式:普通模式、插入模式和命令行模式。普通模式是打开 Vim 之后的默认模式,也是进行模式转换的桥梁。在普通模式下,可以进行常规的浏览、替换、删除、粘贴等命令;在普通模式下按 i 即可切换到插入模式,这种模式与常规的文本编辑器相同,可以自由添加、修改、删除文本内容;按 Esc 可以从插入模式切换回普通模式,再次键入 : 会切换到命令行模式。这种模式支持各种命令的使用,在我看来也是 Vim 的强大之处所在。

注意

Vim 命令对大小写敏感,因此在使用 Vim 时务必确保大写锁定处于关闭状态。通常情况下,大家会建议将键盘的 Caps Lock 键重新映射为 Esc ,对于 Manjaro 系统,可以通过系统设置中的键盘的高级选项进行修改,非常方便。

当本文提及大写的命令如 A 时,实际上是指 Shift + a 的组合键。

网络上流传有这么一个段子:“问:如何产生随机字符串?答:让新手退出 Vim。”

退出 Vim 首先要确保处于普通模式,如果不是很确定,可以多按几次 Esc。然后输入 :quit 即可退出 Vim。这个命令可以这样来看,首先键入的 : 使 Vim 进入到命令行模式,而 quit 就是执行的命令。

可以猜想,如果只是写入文档而不退出(一般编辑器下的"保存"按钮),应当执行 :write ;如果想看某个命令的帮助文档,应该会涉及 :help 命令,实际上的确如此。在普通模式下输入 :help quit 就可以看到相应的帮助文档,其中 :q[uit] 的中括号部分表示可以省略。因此 :quit 可以简写为 :q

如果文档进行了改动,直接运行 :q 会抛出警告,这时需要写入后退出,或者不保存退出。汇总这些相关的命令,如下:

  • :q — 退出 Vim (关闭文档);
  • :w — 写入(保存)文档但不退出;
  • :wq — 保存并关闭当前文档;
  • :q! — 忽略所有更改并关闭当前文档。

移动光标需要在普通模式下进行,用于浏览文档或为编辑做准备。

普通模式下,光标移动可以通过以下按键实现:

  • h — 左移一个字符;
  • j — 下移一行;
  • k — 上已一行;
  • l — 右移一个字符;
  • 0 — 移动到本行开头;
  • $ — 移动到本行末尾;
  • gg — 移动到文档首行开头;
  • G — 移动到文档最后一行开头。

Vim 的命令在执行前可以指定重复次数,例如下移15行可以使用 15j 来完成。特别地,在 G 之前指定数字可以跳转到指定行。为了在 Vim 中显示行号,可以在普通模式下执行 :set number

技巧
当文档单行内容过长而 Vim 的 wrap 设置被启用(默认启用)时,过长的文字会在屏幕内自动换行,但这并不是真正的换行。Vim 默认的 j 命令是对实际行进行操作,遇到这种情况时难免会出乎我们的期望,因此可以使用 gjgk 进行屏幕行的跳转。

为了默认使用屏幕行的跳转,我们会将 jgj 的功能互换,可以在配置文件 .vimrc 中添加以下内容:

text

nnoremap k gk
nnoremap gk k
nnoremap j gj
nnoremap gj j

比普通模式更快地,Vim 可以基于单词进行移动,主要命令如下:

  • w — 移动到下一个单词开头(word);
  • e — 移动到当前单词结尾(end);
  • b — 向前移动到单词开头(backward);
  • ge — 向前移动到单词结尾;
  • W — 移动到下一个字符串开头;
  • E — 移动到下一个字符串结尾;
  • B — 向前移动到字符串开头;
  • gE — 向前移动到字符串结尾。

对比可以发现,小写命令针对单词,而大写命令针对的是字符串,两者的区别在于对特殊字符的处理方式:单词认为 ' 是单词之间的分割,而字符串则严格以空格作为分割。例如 let's go 这个短语会被 Vim 解读成 4 个单词或 2个字符串。

Vim 可以使用 f 对行内的单个字符进行查找,结合 ; 向后继续查找,当跳转过头时使用 , 向前查找。同样,也可以使用 / 对字符串进行查找,使用 nN 分别向后、向前继续查找。

我们用下面的一个例子来对比各种移动光标的方法:

text

The quick brown fox jumps over the lazy dog.

假设现在光标处于这段话的开头,现在希望将光标跳转到 over 的开头,有这么几种做法:

  • wwwww — 所见即所得,只要不觉得麻烦就可以达到效果;
  • 5w — 与上一个命令等价,虽然按键少了,但需要一定的计算;
  • fo;; — 首先 fo 指定了对字符 o 的查找,光标跳转到 brown 的中间,然后继续查找两次达成目标;
  • fvh — 对 v 进行查找,然后向左移动一个字符;
  • /over<Enter> — 对字符串 over 进行查找并跳转(其中 <Enter> 表示按回车键)。

对比这几种方法,第一种最直观,但是当句子特别长时跳转很慢;第二种虽然看起来简单,但数数的功夫容易得不偿失,也很少使用;第三种方法应该说是最常用也是最容易被人接受的,当句子较长时能够获得比较快的跳转速度;第四种是第三种的高阶用法,查找不常出现的字符以提高跳转速度;最后一种单次使用时输入较为复杂,通常结合替换使用。

% 可以用于配对符号之间的跳转,例如在 ( 处按 % 可以跳转到相应的 ),这个功能特别适合对代码进行检查。

m` 构成标记(mark)——跳转对。例如, ma 将当前位置标记为 a ,普通模式下使用 `a 就可以跳转到刚刚标记的位置。需要说明的是,标记只能是单个字符,并且区分大小写;单个文档可以存在多个标记。

类比于英语中动词可以分为及物动词和不及物动词,Vim 中有些命令可以直接执行,例如 j 就是下移一行;也有些命令需要指定作用范围,这里我们以删除命令 d (delete)为例进行说明。

为了执行删除操作,按下 d 之后还应当给定删除范围。前面介绍跳转的时候说过 w 是从当前位置到下一个单词开头,确切地表明了范围。因此 dw 就是从当前位置删除到下一个单词的开头。类似地, d2w 就是从当前位置删除到后面第三个单词的开头(删除两个单词)、 d$ 从当前位置删除到本行结尾。

如果光标处于某个想删除的单词的中间,而使用 dw 删除时,光标之前部分不会被删除。为此,Vim 还提供了 ai 这两个修饰词(可以理解为 around 和 inside)来进一步修饰范围, a 可以将选区扩展到光标两边,包括限定词; i 将选区扩展到两边,但是不包括限定词。例如, daw 将删除整个单词以及后面的一个空格,不管光标处于单词的哪个位置。使用 ai 进行修饰时,除了使用 w 设定范围外,还可以使用配对符号进行扩展。例如,当光标处于某一对括号内部任意位置,使用 da) 可以删除整个括号内的内容,并连同括号一起删除。

特别地,Vim 的一些命令重复按两次表示对当前行进行操作,如 dd 为删除当前行。

除了使用命令划定范围外,与普通编辑器相同,可以先选定范围再执行操作。按 v 进入 Vim 的可视模式(visual mode),然后再利用前面介绍的各种跳转命令,可以发现光标遍历的位置高亮,这时候再执行 d 即可删除高亮选区。特别地, V 直接对整行进行选取。

组合键 Ctrl + v 在 Vim 中不再是粘贴,在普通模式下使用该组合键可以进入 Vim 的块选取模式,对应通常编辑器的列编辑功能,能够同时对多行进行相同的修改。

在普通模式下可以对文档进行部分删除和简单的替换工作,常用有:

  • x — 删除当前字符;
  • r — 使用另一个字符替换当前字符(replace),例如 re 是将光标下的字符替换为 e
  • d — 删除操作,在上面已经讨论,不再赘述;
  • . — 重复上一次操作。

编辑文档最主要的方式是进入编辑模式,像普通编辑器一样编辑文档。Vim 提供以下基本的方式进入插入模式:

  • i — 从当前光标之前进入插入模式(insert);
  • I — 从当前行(或可选模式下的选区)之前进入插入模式;
  • a — 从当前光标之后进入插入模式(append);
  • A — 从当前行(或可选模式下的选区)之后进入插入模式;
  • o — 向下插入新行并进入插入模式;
  • O — 向上插入新行并进入插入模式;
  • s — 删除当前字符并进入插入模式;
  • S — 删除当前行并进入插入模式。

Vim 还可以使用 c 命令进行更改(change),例如 caw 表示删除当前的单词并进入插入模式。这种方法在修改程序时非常有效,例如修改字符串变量 sys_name = "Windows 10" 时,将光标移至双引号内部任意位置,使用 ci" 即可删除字符串变量的值并进行修改。

前面介绍了 / 进行搜索,而替换则需要结合命令行 :s 实现(substitute),例如将 old 替换为 new ,有以下几种不同的方式:

  • :s/old/new — 从光标之后对当前行进行搜索,将第一个 old 替换为 new
  • :s/old/new/g — 从光标之后对当前行进行搜索,将所有 old 替换为 new
  • :%s/old/new/g — 对全文进行搜索,将所有 old 替换为 new
  • :%s/old/new/gc — 对全文进行搜索,将所有 old 替换为 new,并在执行前进行确认。

在普通模式使用组合键 Ctrl + a 可以对光标下的数字加一,而使用 Ctrl + x 则进行减一操作。当光标不处于数字上,Vim 会自动从光标之后搜索当前行的第一个数字,执行相应操作。同样地,这种方式可以指定次数运行,例如 10<Ctrl>a 是将当前数字加 10 。

注意

应当注意,Ctrl + aCtrl + x 都是针对单词的操作,因此小数将以小数点分界,作为两个整数来对待。

如果数字以 0 开头,Vim 默认将其解释为八进制,因此对 007 进行 <Ctrl>a 命令后,得到的是 010 而不是 008。为了只使用十进制,可以在配置文件 .vimrc 中添加 set nrformats= 进行设置。

在插入模式中,可以使用 Ctrl + r + = 触发计算功能,输入计算表达式后,按 Enter 即可将计算结果输出到当前位置。例如 <Ctrl>r=1+1<Enter> 将会输入 2 。

Vim 提供多种寄存器,用于储存复制或删除的内容。寄存器可以通过 " 进行引用,有以下几种常见寄存器:

  • [a-z] — 有名寄存器,用于各种自定义内容;
  • " — 无名寄存器,不指定寄存器时默认使用该寄存器;
  • 0 — 复制专用寄存器(书里这么写的,我也不知道为啥要起这个名字,实际上复制不一定非要用这个);
  • + — 系统剪切板,用于系统级的复制粘贴;
  • _ — 黑洞寄存器,所有在这里的东西都丢失,彻彻底底地删除;
  • = — 表达式寄存器,可以用于计算;
  • % — 当前文件名。

还有一些其他寄存器,这里不多赘述。

以复制粘贴这个经典的例子来介绍寄存器的使用。

复制可以使用 y 来实现(yank),而粘贴使用 p 完成(paste)。复制时需要指定范围,可以用限定词来指定,如 yaw 为复制当前单词;也可以用可视模式进行选择。p 默认将内容粘贴在光标之后(Vim 会自动判断是否换行),也可用 P 粘贴在之前,或使用可视模式进行选择替换。特别地, yy 将对当前行进行复制。

上面这种直接复制的方法没有指定寄存器,默认使用无名寄存器 ",即 yy 命令等价于 ""yy (第一个 " 表示使用寄存器,第二个 " 表示使用无名寄存器)。使用无名寄存器容易导致复制的内容丢失,我们看下面一个例子:

text

1) 使用 `yy` 命令复制本行;
2) 使用 `dd` 命令删除本行;
3)尝试在此使用 `p` 命令将第一行内容粘贴到本行之下。

如果按照上面的提示做,会发现最后粘贴的是第二行的内容。这是因为第二行使用 dd 删除的内容并没有真正的删除,而是以剪切的方式储存在寄存器内,而前面的 yy 和这里的 dd 均没有显式指定寄存器,所有都使用的是无名寄存器。无名寄存器第一次复制的内容被第二次删除的内容覆盖,因此粘贴时读取的无名寄存器是第二行内容。

为此,可以改变操作顺序,如先删除,后复制。也可以使用显式指定其他寄存器以避免内容覆盖,例如可以使用 "0yy"0p 进行复制粘贴;或者可以将删除内容放置到其他寄存器如 "_dd

可以同时使用多个寄存器,例如复制帐号和密码时,可以使用 "ay 将帐号复制到寄存器 a ,而使用 “by 将密码复制到寄存器 b。粘贴分别使用 "ap"bp 即可。

上述的粘贴是在普通模式进行,为了在插入模式直接读取寄存器内容,可以使用 Ctrl + r 组合键,指定寄存器后即可将内容粘贴到正文。

忘记寄存器内容时,可以使用 :reg 命令进行查看。

寄存器中还可以保存命令(宏),并在特定的时候执行(回放)。普通模式下按 q 并指定寄存器即可开启宏的录制,再次按 q 即停止录制。在使用寄存器内保存的宏时,使用 @ 和寄存器名即可。

例如,如果要在 Markdown 中使用 Enter 这样的键盘样式,需要输入指令 <kbd>Enter</kbd>,非常麻烦。因此可以录制一个宏,自动在单词两边分别添加 <kbd></kbd> 。假如我将这个宏录制到寄存器 k,具体步骤如下:

  1. 普通模式下将光标移动到需要添加键盘样式的单词处;
  2. 键入 qk 开始录制宏,并指定将宏录制到有名寄存器 k
  3. 使用 ciw 删除当前单词并保留两边的空格,进入插入模式,输入前缀 <kbd>
  4. 在插入模式中使用<Ctrl>r" 读取无名寄存器的内容,即上一步删除的单词,然后输入后缀 </kbd>
  5. Esc 返回普通模式,按 q 停止录制。

如此做,只需要在另一个需要添加键盘样式的单词处使用 @k 即可调用宏。使用 @ 回放宏的时候,也可以像普通命令一样指定次数。一个经典的例子就是在各行前面添加行号,需要结合一定的脚本,使用以下方式实现:

  1. 在命令行运行 :let i=1 设定行号初值,并将光标移动到需要编号的那一行;
  2. 使用 qq 开始录制宏,并将其录制到寄存器 q
  3. 使用 I\<Ctrl>r=i\<Enter>) 将变量 i 的值插入到行的最前面,并添加 ) 进行修饰;
  4. Esc 回到普通模式,运行 :let i+=1`` 进行递归,然后按 j` 换行;
  5. q 停止宏录制,然后使用 22@q> 将宏重复 22 次(2@在同一个键位,用起来方便而已)。

当行数不足以满足指定的重复次数时,Vim 会自动停止回放宏,因此无需担心指定次数不准确的问题。

  1. Neil D. Vim实用技巧. 杨源, 车文隆, 译. 人民邮电出版社, 2014.