# 第三章 Vim 常用命令 ## 3.2 快捷键重映射 几乎每个初窥门径的 vimer 都曾为它的键映射欣喜若狂吧,因为它定制起来实在是太简 洁了,却又似能搞出无尽的花样。 快捷键,或称映射,在 Vim 文档中的术语叫 "map",它的基本用法如下: ```vim map {lhs} {rhs} map 快捷键 相当于按下的键序列 ``` 其中快捷键 `{lhs}` 不一定是单键,也可能是一个(较短的)按键序列,然后 vim 将其 解释为另一个(可能较长较复杂的)的按键序列 `{rhs}`。为方便叙述,我们将 `{lhs}` 称为“左参数”,而将 `{rhs}` 称为“右参数”。左参数是源序列,也可叫被映射键,右参 数是目标序列,也可叫映射键。 例如,在 vim 的默认解释下,普通模式下大写的 `Y` 与两个小写的 `yy` 是完全相同的 功能,就是复制当前行。如果你觉得这浪费了快捷键资源,可将 `Y` 重定义为复制当前 行从当前光标列到列尾的部分,用下面这个映射命令就能实现: ```vim : map Y y$ ``` 然而,映射虽然初看起来简单,其中涉及的门道还是很曲折的。让我们先回顾一下 Vim 的模式。 ### Vim 的主要模式 模式是 Vim 与其他大多数编辑器的一个显著区别。在不同的模式下,vim 对用户按键的 响应意义有根本的差别。Vim 支持很多种模式,但最主要的模式是以下几种: * 普通模式,这是 Vim 的默认模式,在其他大多模式下按 `` 键都将回到普通模式 。在该模式下按键被解释为普通命令,用以完成快速移动、查找、复制粘贴等操作。 * 插入模式,类似其他“正常”编辑的模式,键盘上的字母、数字、标点等可见符号当作直 接的字符插入到当前缓冲文件中。从普通模式进入插件模式的命令有:`aAiIoO` - `a` 在当前光标后面开始插入, - `i` 在当前光标之前开始插入, - `A` 在当前行末尾开始插入, - `I` 在当前行行首开始插入, - `o` 在当前行下面打开新的一行开始插入, - `o` 在当前行上面打开新的一行开始插入。 * 可视模式(visual),非正式场合下也可称之为“选择”模式。在该模式下原来的移动命 令变成改变选区。选区文本往往有不同的高亮模式,使用户更清楚地看到后续命令将要 操作的目标文本区域。从普通模式下,有三个键分别进入三种不同的可视模式: - `v` (小写 v)字符可视模式,可以按字符选择文本, - `V` (大写 V)行可视模式,按行选择文本(jk有效,hl无效), - `Ctrl-v` 列块可视模式,可选择不同行的相同一列或几列。 (Vim 还另有一种 "select" 模式,与可视模式的选择意义不同,按键输入直接覆盖替 换所选择的文本) * 命令行模式。就是在普通模式时按冒号 `:` 进入的模式,此时 Vim 窗口最后一行将变 成可编辑输入的命令行(独立于当前所编辑的缓冲文件),按回车执行该命令行后回到 普通模式。 本教程所说的 VimL 语言其实不外也是可以在命令行中输入的语句。此外还有一种“Ex 模式”,与命令行模式类似,不过在回车执行完后仍停留在该模式,可继续输入执行命 令,不必每次再输入冒号。在“Ex模式”下用 `:vi` 命令才回到普通模式。 大部分初、中级 Vim 用户只要掌握这四种模式就可以了。对应不同模式,就有不同的映 射命令,表示所定义的快捷键只能用于相应的模式下: * 普通模式:nmap * 插入模式:imap * 可视模式:vmap (三种不同可视模式并不区分,也包括选择模式) * 命令模式:cmap 如果不指定模式,直接的 `map` 命令则同时可作用于普通模式与可视选择模式以及命令 后缀模式(Operator-pending,后文单独讲)。而 `map!` 则同时作用于插入模式与命令 行模式,即相当于 `imap` 与 `cmap` 的综合体。其实 `vmap` 也是 `xmap`(可视模式 )与 `smap` (选择模式)的综合体,只是 `smap` 用得很少,`vmap` 更便于记忆(`v` 命令进入可视模式),因此我在定义可视选择模式下的快捷键时倾向于用 `vmap`。 在其他情况下,建议用对应模式的映射命令,也就是将模式简名作为 `map` 的限定前缀。 而不建议用太过宽泛的 `map` 或 `map!` 命令。 ### 特殊键表示 在 `map` 系列命令中,`{lhs}` 与 `{rhs}` 部分可直接表示一般字符,但若要映射(或 被映射)的是不可打印字符,则需要特殊的标记(`<>`尖括号内不分大小写): * 空格:`` 。映射命令之后的各个参数要用空格分开,所以若正是要重定义空格 键意义,就得用 `` 表示。同时映射命令尽量避免尾部空格,因为有些映射会 把尾部空格当作最后一个参数的一部分。始终用 `` 是安全可靠的。 * 竖线:``。`|` 在命令行中一般用于分隔多条语句,因此要重定义这个键要用 `` 表示。 * 叹号:``。`!` 可用于很多命令之后,用以修饰该命令,使之做一些相关但不同 的工作,相当于特殊的额外参数。映射中要用到这个符号最好也以 `` 表示。 * 制表符:``,回车:`` * 退格:``,删除键: ``,插入键: `` * 方向键:`` `` `` `` * 功能键:`` `` 等 * Ctrl 修饰键:`` (这表示同时按下 Ctrl 键与 x 键) * Shift 修饰键:``,对于一般字母,直接用大写字母表示即可,如 `A` 即可,不 必有``。一般对特殊键可双修饰键时才用到,如 ``。 * Alt `` 或 Meta `` 修饰键。在 term 中运行的 vim 可能不方便映射这个修 饰键。 * 小于号:``,大于号 `` * 直接用字符编码表示:``,后面可接十进制或十六进制或八进制数字。如 `` 表示编码为 `127` 那个字符。这种方法虽然统一,但如有可能,优先 使用上述意义明确方便识记的特殊键名表示法。 此外,还有几个特殊标记并不是特指哪个可从键盘输入的按键: * `` 代表 `mapleader` 这个变量的值,一般叫做快捷键前缀,默认是 `\`。同 时还有个 ``,它取的是 `maplocalleader` 的变量值,常用于局部映射 。 * `` 当映射命令用于脚本文件中(应该经常是这种情况),`` 用于指代当前 脚本作用域的函数,故一般用于 `{rhs}` 部分。当 vim 执行映射命令时,实际会把 `` 替换为 `dd_` 样式,其中 `dd` 表示当前脚本编号,可用 `:scriptnames` 查看所有已加载的脚本,同时也列出每个脚本的编号。 * `` 一种特殊标记,可以避免与用户能从键盘输入的任何按键冲突。常用于插件 中,表示该映射来自某插件。与 `` 关联某一特定脚本不同,`` 并不关联 特定插件的脚本文件。它的意义请继续看下一节。 ### 键映射链的用途与陷阱 键映射是可传递的,例如若有以下映射命令: ```vim : map x y : map y z ``` 当用户按下 `x`,vim 首先将其解释为相当于按下 `y`,然后发现 `y` 也被映射了,于 是最终解释为相当于按下 `z`。 这就是键映射的传递链特性。那这有什么用呢,为什么不直接定义为 `:map x z` 呢?假 如 `z` 是个很复杂的按键命令,比如 `LongZZZZZZZ`,那么就可先为它定义一个简短的 映射名,如 `y`: ```vim : map y LongZZZZZZZ : map x1 y : map x2 y ``` 然后再可以将其他多个键如 `x1` 与 `x2` 都映射为 `y`,不必重复多次写 `LongZZZZZZZ` 了。然而,这似乎仍然很无趣,真正有意义的是用于 ``。 假设在某个插件文件中有如下映射命令: ```vim : map (do_some_funny_thing) :call ActualFunction() : map x (do_some_funny_thing) : map (do_some_funny_thing) : map x (do_some_funny_thing) ``` 在第一个映射命令中,其 `{lhs}` 部分是 `(do_some_funny_thing)`,这也是一 个“按键序列”,不过第一键是 ``(其实不可能从键盘输入的键),然后接一个左 括号,接着是一串普通字符按键,最后还是个右括号。其中左右括号不是必须的,甚至 可以不必配对,中间也不一定只能普通字符,加一些任意特殊字符也是允许的。不过当前许 多优秀的插件作者都自觉遵守这个范式:`(mapping_name)`。 该命令的 `{rhs}` 部分是 `:call ActualFunction()`,表示调用当前脚本中 定义的一个函数,用以完成实际的工作。然而 `...` 是不可能由用户按出来的键 序列,所以需要再定义一个映射 `:map x ...`,让一个可以方便按出的键 `x` 来 触发这个特殊键序列 `...`,并最终调用函数工作。当然了,在普通模式下的几乎 每个普通字母 vim 都有特殊意义(不一定是 `x`,而`x`表示删除一个字符),你可能不 应该重定义这个字母按键,可加上 `` 前缀修饰或其他修饰键。 那么为何不直接定义 `:map x :call ActualFunction()` 呢?一是为了封装隐 藏实现,二是可为映射取个易记的映射名如 `(mapping_name)`。这样,插件作者 只将 `(mapping_name)` 暴露给用户,用户也可以自己按需要喜好重定义触发键映 射,如 `:map y (mapping_name)`。 因此,`` 不过是某个普通按键序列的特殊前缀而已,特殊得让它不可能从键盘输 入,主要只用于映射传递,同时该中间序列还可取个意义明确好记的名字。一些插件作者 为了进一步避免这个中间序列被冲突的可能性,还在序列中加入插件名,比如改长为: `(plug_name_mapping_name)`。 不过,映射传递链可能会引起另一个麻烦。例如请看如下这个映射: ```vim : map j gj : map k gk ``` 在打开具有长文本行的文件时,如果开启了折行显示选项(`&wrap`),则 `gj` 或 `gk` 命令表示按屏幕行移动,这可能比按文件行的 `j` `k` 移动更方便。所以这两个键的重 映射是有意义的,可惜残酷的事实是这并没有达到想要的效果。作了这两个映射命令之后 ,若试图按 `j` 或 `k` 时,vim 会报错,指出循环定义链太长了。因为 vim 试图作以 下解释: ``` j --> gj --> ggj --> gggj --> ... ``` 无尽循环了,当达到一些深度限制后,vim 就不干了。 为了避免这个问题, vim 提供了另一套命令,在 `map` 命令之前加上 `nore` 前缀改为 `noremap` 即可,表示不要对该命令的 `{rhs}` 部分再次解析映射了。 ```vim : noremap j gj : noremap k gk ``` 当然,前面还提到,良好的映射命令习惯是显式限定模式,模式前缀还应在 `nore` 前缀 之前,如下表示只在普通模式下作此映射命令: ```vim : nnoremap j gj : nnoremap k gk ``` 结论就是:除了有意设计的 `` 映射必须用 `:map` 命令外,其他映射尽量习惯用 `:noremap` 命令,以避免可能的循环映射的麻烦。例如对本节开始提出的示例规范改写 如下: ```vim : nnoremap (do_some_funny_thing) :call ActualFunction() : nmap x (do_some_funny_thing) : nmap (do_some_funny_thing) : nmap x (do_some_funny_thing) ``` 其中,`:` 并不是什么特殊语法,只不过表示当按下冒号刚进入命令行时先按个 ``, 用以先清空当前命令行,确保在执行后面那个命令时不会被其他可能的命令行字符干扰。 (比如若不用 `nnoremap` 而用 `noremap` 时,在可视模式选了一部分文本后,按冒号 就会自己加成 `:'<,'>`,此时在命令行中先按 `` 就能把前面的地址标记清除。在 很小心地用了 `nnoremap` 时,还会不会有特殊情况导致干扰字符呢,也不好说,反正加 上 `` 没坏处。但若你的函数本就设计为允许接收行地址参数,则最好额外定义 `:vnoremap`,不用 `` 的版本。) ### 各种映射命令 前面讲了最基础的 `:map` 命令,还有更安全的 `:noremap` 命令,以及各种模式前缀限 定的命令 `:nnoremap` `:inoremap` 等。这已经能组合出一大群映射命令了,不过它们 仍只算是一类映射命令,就是定义映射的命令。此外,vim 还提供了其他几个映射相关的 命令。 * 退化的映射定义命令用于列表查询。不带参数的 `:map` 裸命令会列出当前已重定义的 所有映射。带一个参数的 `:map {lhs}` 会列出以 `{lhs}` 开头的映射。同样支持模 式前缀缩小查询范围,但由于只为查询,没有 `nore` 中缀的必要。定义映射的命令, 至少含 `{lhs}` 与 `{rhs}` 两个参数。 * 删除指定映射的命令 `:unmap {lhs}`,需要带一个完全匹配的左参数(不像查询命令 只要求匹配开头,毕竟删除命令比较危险)。可以限定模式前缀,如 `nunmap {lhs}` 只删除普通模式下的映射 `{lhs}`。注意,模式前缀始终是在最前面,如果你把 `un` 也视为 `map` 命令的中缀的话。 * 清除所有映射的命令 `:mapclear`。因为清除所有,所以不需要参数了。当然也可限定 模式前缀,如 `:nmapclear`,表示只清除普通模式下的映射。另外还可以有个 `` 参数,表示只清除当前 buffer 内的局部映射。这类特殊参数在下节继续 讲解。 ### 特殊映射参数 映射命令支持许多特殊参数,也用 `<>` 括起来。但它们不同于特殊键标记,并不是左 参数或右参数序列的一部分。同时必须紧跟映射命令之后,左参数 `{lhs}` 之前,并用 空格分隔参数。 * `` 表示只影响当前 buffer 的映射,`:map` `:unmap` 与 `:mapclear` 都可 接收这个局部参数。 * `` 字面意思是不再等待。较短的局部映射将掩盖较长的全局映射。 `` 这个参数很少用到。但其中涉及到的一个映射机制有必要了解。假设有如下 两个映射定义: ```vim * nnoremap x1 something * nnoremap x2 another-thing ``` 因为定义的是两个按键的序列,当用户按下 `x` 键时,vim 会等待一小段时间,以判断 用户是否想用 `x1` 或 `x2` 快捷键,然后触发相应的映射定义。如果超过一定时间后用 户没有按任何键,就按默认的 `x` 键意义处理了。当然如果后面接着的按键不匹配任何 映射,也是按实际按键解释其意义。 因此,若还定义单键 `x` 的映射: ```vim : nnoremap x simple-thing ``` 当用户想通过按 `x` 键来触发该映射时,由于 `x1` 与 `x2` 的存在,仍然需要等待一 小段时间才能确定用户确实是想用 `x` 键来触发 `simple-thing` 这件事。这样的迟滞 效应可不是个好体验。 于是就提出 `` 参数,与 `` 参数联用,可避免等待: ```vim : nnoremap x local-thing ``` 这样,在当前 buffer 中按下 `x` 键时就能直接做 `local-thing` 这件事了。 尽管有这个效用,但 `` 在实践中还是用得很少。用户在自行设定快捷键时,最 好还是遵循“相同前缀等长快捷键”的原则。也就说当定义 `x1` 或 `x2` 快捷键后,就最好 不要再定义 `x` 或 `x123` 这样的变长快捷键了。规划整齐点,体验会好很多。当然, 如实在想为某个功能定义更方便的快捷键快,可定义为重复按键 `xx`,因为重复按键 的效率会比按不同键快一点。(想想 vim 内置的 `dd` 与 `yy` 命令) ```vim : nnoremap xx most-used-thing ``` 另一方面,局部映射参数 `` 却是非常常用,鼓励多用。局部映射会覆盖相同的 全局映射,而且当 `` 存在时,会进一步隐藏全局中更长的映射。 * `` 在默认情况下,当按下某个映射的 `{lhs}` 序列键中,vim 下面的命令行 会显示 `{rhs}` 序列键。加上这个 `` 参数时,就不会回显了。我的建议是 一般没必要加这个参数禁用这个特性。当映射键正常工作时,你不必去理会它的回显, 但是当映射键没按预想的工作时,你就可在回显中看到它实际映射成什么 `{rhs}` 了 ,这可帮助你判断是由于映射被覆盖了还是映射本身哪里写错了。 * `` 这是相对过时的参数了,它指示当前这个映射命令中接受 `<>` 标记特殊 键。在默认不兼容 vi 的设置下,不必加这个参数也能直接用 `<>` 表示特殊键。 * `