You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

646 lines
38 KiB

2 years ago
# 第三章 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 的默认模式,在其他大多模式下按 `<Esc>` 键都将回到普通模式
。在该模式下按键被解释为普通命令,用以完成快速移动、查找、复制粘贴等操作。
* 插入模式,类似其他“正常”编辑的模式,键盘上的字母、数字、标点等可见符号当作直
接的字符插入到当前缓冲文件中。从普通模式进入插件模式的命令有:`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}` 部分可直接表示一般字符,但若要映射(或
被映射)的是不可打印字符,则需要特殊的标记(`<>`尖括号内不分大小写):
* 空格:`<Space>` 。映射命令之后的各个参数要用空格分开,所以若正是要重定义空格
键意义,就得用 `<Space>` 表示。同时映射命令尽量避免尾部空格,因为有些映射会
把尾部空格当作最后一个参数的一部分。始终用 `<Space>` 是安全可靠的。
* 竖线:`<BAR>`。`|` 在命令行中一般用于分隔多条语句,因此要重定义这个键要用
`<BAR>` 表示。
* 叹号:`<Bang>`。`!` 可用于很多命令之后,用以修饰该命令,使之做一些相关但不同
的工作,相当于特殊的额外参数。映射中要用到这个符号最好也以 `<Bang>` 表示。
* 制表符:`<Tab>`,回车:`<CR>`
* 退格:`<BS>`,删除键: `<DEL>`,插入键: `<Ins>`
* 方向键:`<UP>` `<DOWN>` `<LEFT>` `<RIGHT>`
* 功能键:`<F1>` `<F2>`
* Ctrl 修饰键:`<C-x>` (这表示同时按下 Ctrl 键与 x 键)
* Shift 修饰键:`<S->`,对于一般字母,直接用大写字母表示即可,如 `A` 即可,不
必有`<S-a>`。一般对特殊键可双修饰键时才用到,如 `<C-S-a>`
* Alt `<A->` 或 Meta `<M->` 修饰键。在 term 中运行的 vim 可能不方便映射这个修
饰键。
* 小于号:`<lt>`,大于号 `<gt>`
* 直接用字符编码表示:`<Char->`,后面可接十进制或十六进制或八进制数字。如
`<Char-0x7f>` 表示编码为 `127` 那个字符。这种方法虽然统一,但如有可能,优先
使用上述意义明确方便识记的特殊键名表示法。
此外,还有几个特殊标记并不是特指哪个可从键盘输入的按键:
* `<Leader>` 代表 `mapleader` 这个变量的值,一般叫做快捷键前缀,默认是 `\`。同
时还有个 `<LocalLeader>`,它取的是 `maplocalleader` 的变量值,常用于局部映射
* `<SID>` 当映射命令用于脚本文件中(应该经常是这种情况),`<SID>` 用于指代当前
脚本作用域的函数,故一般用于 `{rhs}` 部分。当 vim 执行映射命令时,实际会把
`<SID>` 替换为 `<SNR>dd_` 样式,其中 `dd` 表示当前脚本编号,可用
`:scriptnames` 查看所有已加载的脚本,同时也列出每个脚本的编号。
* `<Plug>` 一种特殊标记,可以避免与用户能从键盘输入的任何按键冲突。常用于插件
中,表示该映射来自某插件。与 `<SID>` 关联某一特定脚本不同,`<Plug>` 并不关联
特定插件的脚本文件。它的意义请继续看下一节。
### 键映射链的用途与陷阱
键映射是可传递的,例如若有以下映射命令:
```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` 了。然而,这似乎仍然很无趣,真正有意义的是用于 `<Plug>`
假设在某个插件文件中有如下映射命令:
```vim
: map <Plug>(do_some_funny_thing) :call <SID>ActualFunction()<CR>
: map x <Plug>(do_some_funny_thing)
: map <C-x> <Plug>(do_some_funny_thing)
: map <Leader>x <Plug>(do_some_funny_thing)
```
在第一个映射命令中,其 `{lhs}` 部分是 `<Plug>(do_some_funny_thing)`,这也是一
个“按键序列”,不过第一键是 `<Plug>`(其实不可能从键盘输入的键),然后接一个左
括号,接着是一串普通字符按键,最后还是个右括号。其中左右括号不是必须的,甚至
可以不必配对,中间也不一定只能普通字符,加一些任意特殊字符也是允许的。不过当前许
多优秀的插件作者都自觉遵守这个范式:`<Plug>(mapping_name)`。
该命令的 `{rhs}` 部分是 `:call <SID>ActualFunction()<CR>`,表示调用当前脚本中
定义的一个函数,用以完成实际的工作。然而 `<Plug>...` 是不可能由用户按出来的键
序列,所以需要再定义一个映射 `:map x <Plug>...`,让一个可以方便按出的键 `x`
触发这个特殊键序列 `<Plug>...`,并最终调用函数工作。当然了,在普通模式下的几乎
每个普通字母 vim 都有特殊意义(不一定是 `x`,而`x`表示删除一个字符),你可能不
应该重定义这个字母按键,可加上 `<Leader>` 前缀修饰或其他修饰键。
那么为何不直接定义 `:map x :call <SID>ActualFunction()<CR>` 呢?一是为了封装隐
藏实现,二是可为映射取个易记的映射名如 `<Plug>(mapping_name)`。这样,插件作者
只将 `<Plug>(mapping_name)` 暴露给用户,用户也可以自己按需要喜好重定义触发键映
射,如 `:map y <Plug>(mapping_name)`
因此,`<Plug>` 不过是某个普通按键序列的特殊前缀而已,特殊得让它不可能从键盘输
入,主要只用于映射传递,同时该中间序列还可取个意义明确好记的名字。一些插件作者
为了进一步避免这个中间序列被冲突的可能性,还在序列中加入插件名,比如改长为:
`<Plug>(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
```
结论就是:除了有意设计的 `<Plug>` 映射必须用 `:map` 命令外,其他映射尽量习惯用
`:noremap` 命令,以避免可能的循环映射的麻烦。例如对本节开始提出的示例规范改写
如下:
```vim
: nnoremap <Plug>(do_some_funny_thing) :<C-u>call <SID>ActualFunction()<CR>
: nmap x <Plug>(do_some_funny_thing)
: nmap <C-x> <Plug>(do_some_funny_thing)
: nmap <Leader>x <Plug>(do_some_funny_thing)
```
其中,`:<C-u>` 并不是什么特殊语法,只不过表示当按下冒号刚进入命令行时先按个 `<C-u>`
用以先清空当前命令行,确保在执行后面那个命令时不会被其他可能的命令行字符干扰。
(比如若不用 `nnoremap` 而用 `noremap` 时,在可视模式选了一部分文本后,按冒号
就会自己加成 `:'<,'>`,此时在命令行中先按 `<C-u>` 就能把前面的地址标记清除。在
很小心地用了 `nnoremap` 时,还会不会有特殊情况导致干扰字符呢,也不好说,反正加
`<C-u>` 没坏处。但若你的函数本就设计为允许接收行地址参数,则最好额外定义
`:vnoremap`,不用 `<C-u>` 的版本。)
### 各种映射命令
前面讲了最基础的 `:map` 命令,还有更安全的 `:noremap` 命令,以及各种模式前缀限
定的命令 `:nnoremap` `:inoremap` 等。这已经能组合出一大群映射命令了,不过它们
仍只算是一类映射命令,就是定义映射的命令。此外,vim 还提供了其他几个映射相关的
命令。
* 退化的映射定义命令用于列表查询。不带参数的 `:map` 裸命令会列出当前已重定义的
所有映射。带一个参数的 `:map {lhs}` 会列出以 `{lhs}` 开头的映射。同样支持模
式前缀缩小查询范围,但由于只为查询,没有 `nore` 中缀的必要。定义映射的命令,
至少含 `{lhs}``{rhs}` 两个参数。
* 删除指定映射的命令 `:unmap {lhs}`,需要带一个完全匹配的左参数(不像查询命令
只要求匹配开头,毕竟删除命令比较危险)。可以限定模式前缀,如 `nunmap {lhs}`
只删除普通模式下的映射 `{lhs}`。注意,模式前缀始终是在最前面,如果你把 `un`
也视为 `map` 命令的中缀的话。
* 清除所有映射的命令 `:mapclear`。因为清除所有,所以不需要参数了。当然也可限定
模式前缀,如 `:nmapclear`,表示只清除普通模式下的映射。另外还可以有个
`<buffer>` 参数,表示只清除当前 buffer 内的局部映射。这类特殊参数在下节继续
讲解。
### 特殊映射参数
映射命令支持许多特殊参数,也用 `<>` 括起来。但它们不同于特殊键标记,并不是左
参数或右参数序列的一部分。同时必须紧跟映射命令之后,左参数 `{lhs}` 之前,并用
空格分隔参数。
* `<buffer>` 表示只影响当前 buffer 的映射,`:map` `:unmap``:mapclear` 都可
接收这个局部参数。
* `<nowait>` 字面意思是不再等待。较短的局部映射将掩盖较长的全局映射。
`<nowait>` 这个参数很少用到。但其中涉及到的一个映射机制有必要了解。假设有如下
两个映射定义:
```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` 这件事。这样的迟滞
效应可不是个好体验。
于是就提出 `<nowait>` 参数,与 `<buffer>` 参数联用,可避免等待:
```vim
: nnoremap <buffer> <nowait> x local-thing
```
这样,在当前 buffer 中按下 `x` 键时就能直接做 `local-thing` 这件事了。
尽管有这个效用,但 `<nowait>` 在实践中还是用得很少。用户在自行设定快捷键时,最
好还是遵循“相同前缀等长快捷键”的原则。也就说当定义 `x1``x2` 快捷键后,就最好
不要再定义 `x``x123` 这样的变长快捷键了。规划整齐点,体验会好很多。当然,
如实在想为某个功能定义更方便的快捷键快,可定义为重复按键 `xx`,因为重复按键
的效率会比按不同键快一点。(想想 vim 内置的 `dd``yy` 命令)
```vim
: nnoremap xx most-used-thing
```
另一方面,局部映射参数 `<buffer>` 却是非常常用,鼓励多用。局部映射会覆盖相同的
全局映射,而且当 `<nowait>` 存在时,会进一步隐藏全局中更长的映射。
* `<silent>` 在默认情况下,当按下某个映射的 `{lhs}` 序列键中,vim 下面的命令行
会显示 `{rhs}` 序列键。加上这个 `<silent>` 参数时,就不会回显了。我的建议是
一般没必要加这个参数禁用这个特性。当映射键正常工作时,你不必去理会它的回显,
但是当映射键没按预想的工作时,你就可在回显中看到它实际映射成什么 `{rhs}`
,这可帮助你判断是由于映射被覆盖了还是映射本身哪里写错了。
* `<special>` 这是相对过时的参数了,它指示当前这个映射命令中接受 `<>` 标记特殊
键。在默认不兼容 vi 的设置下,不必加这个参数也能直接用 `<>` 表示特殊键。
* `<script>` 当坚持用 `:noremap` 代替 `:map` 这个参数也没什么用了。它的本意是
限定右参数 `{rhs}` 不会再与脚本外部的映射相互作用了。
* `<unique>` 唯一性要求是确保不会覆盖原来已定义的映射。在使用命令 `:map
<unique> {lhs} {rhs}` 时,如果发现 `{lhs}` 在此前已定义,这条重定义映射的命
令就会失败。
这个参数一般用在共享插件中,为了避免覆盖用户自己已定义的映射。不过在脚本中,还
有两个函数能作更好的控制。内建函数 `mapcheck()` 用于判断一个 `{lhs}` 是否已
被映射,`hasmapto()` 用于判断一个 `{rhs}` 是否有映射过。具体用法请用 `:help`
查问相应的函数说明。
* `<expr>` 这是通过一个表达式间接计算出 `{rhs}` 的用法。这是个相对高级的用法,
将在下一节详细讨论。
### \*表达式映射
常规的映射定义 `:map {lhs} {rhs}` 只是简单的将一个键序列转换解析为另一个序列,
所以这是一种静态的映射。如果在映射定义中结合表达式的思想,通过某种表达式计算出
所要转换的 `{rhs}`,那就能极大地扩展映射的功能,达到静态映射所无法实现的灵活性
有两种方式在映射定义中使用表达式。一种是 `<expr>` 参数,另一种是表达式寄存器
`@=`。我们先讨论后一种方式。`=` 是一种特殊的寄存器,那么普通的寄存器又是什么概
念呢?那就从宏开始说起吧。虽然乍看之下宏与映射的关系远着呢,但究其本质也是通过
少量按键来实现需要大量按键的功能。
假设有这么个需求,将每两行连接为一行,怎么处理比较方便快捷。不妨打开在第一章示
例生成的 `~/.vim/vimllearn/helloworld.txt` 作为示例编辑文件吧,如果这个文件你
未保存或丢失了,重新生成也是极快的。
vim 普通模式下有个命令 `J` 用于将光标当前行与下一行连接为一行,就是删去其中的
回车符。如果光标初始在第一行,那么 `J` 就能将第一行与第二行合一行,光标停留在
第一行;再按 `j` 下移到第二行,也就是最初的第三行,再按 `J` 合并……于是你可用这
个按键序列 `JjJjJjJj...` 来将当前 buffer 内的每两行合并为一行。
这都是些重复按键呀,可以用宏来节省操作呢。假设撤销刚才讨论的操作,从最初打开的
`helloworld.txt` 重新开始,(普通模式下)请依次按这些键 `qaJjq`
* `q` 是录制宏的命令,`qa` 表示将宏保存到寄存器 `a`中;
* `Jj` 就是刚才我们讨论的手动操作,将当前行与下一行合并,再将光标下移一行;
* `q` 再一个 `q` 表示结束录制宏。
现在我们已经有了 `a` 宏,就可以用 `@a` 命令播放这个宏了。可见其效果与在录制时
的操作 `Jj` 是一样的。然后我们可以进一步在播放宏的命令之前加个重复数字。因为原
来的 `helloworld.txt` 有 100 行,录制宏时合了两行,尝试播放宏时又合了两行,所
以还需要再合并 48 次。用这个命令 `48@a` 就可以瞬间将剩余的文本行两两合并了。也
可以使用 `48@@` 命令,因为 `@@` 是表示播放上一次播放过的宏。
(注:上述操作要产生相同结果,需要未打开折行选项,即 `:set nowrap`,或没有将
`j` 映射为 `gj` 或其他,同时 `J` 命令也未被映射)
那么宏到底又是什么,宏里面到底保存了什么神秘的东西。其实它一点都不神秘,宏就是
一个寄存器而已。你可以用 `:reg` 命令(全名`:registers`)查看所有寄存器的内容,
或者特定地 `:reg a` 查看寄存器 `a` (宏 `a` )的内容。可见它就是保存着 `Jj`
两个字符而已。可以将它粘贴出来再确认下 `o<Esc>"ap`
* `o<Esc>` 表示用 `o` 命令打开新一行,然后用 `<Esc>` 回到普通模式。如果你按刚
才的批量宏操作后,光标应该位于 buffer 的最后一行;此时在最后新加了一空行,光
标也在这空行上。
* `"ap` 粘贴命令 `p` 应属常见,在这之前先按 `"a` 表示从寄存器 `a` 中粘贴内容。
执行完这个命令后,就会发现已经将寄存器 `a` 的内容 `Jj` 粘贴到当前 buffer 末尾了。
常规寄存器有 26 个,即以 `a-z` 字母命名。我们可以试试其他寄存器,比如先用 `v`
选定 `Jj` 这两个字符,再用命令 `"by` 将这两个字符复制进寄存器 `b` 中。你可以用
`:reg` 命令再次查看下寄存器内容,确认 `a``b` 两个寄器都保存着 `Jj` 了。
题外话:我们平时使用复制命令 `y` 与粘贴命令 `p` 都不会加寄存器前缀的,这时它们
使用的是默认寄存器,其名就是双引号 `"`,它其实是关联着最近使用的寄存器,与最近
使用那个寄存器内容相同。可以在当前行继续尝试 `p` 命令与 `""p` 命令(或在使用每
个命令之前先输入一个空格,分隔内容方便查看),可见它们都粘贴出了 `Jj`。此外还
有大写字母的寄存器,但它们不是额外的寄存器,只是表示往相应的寄存器中附加内容。
比如若 `v` 选定 `Jj` 内容后,再按 `"Ap` ,就表示将这两字符附加到原来的 `a`
存之后了。可以用 `:reg` 查看 `a` 寄存器的内容已变成 `JjJj` 了。
为了说明宏即是寄存器,先用 `q!` 强制关闭当前的 `helloworld.txt` 而不保存,再重
新打开原始的有 100 行的 `helloeworld.txt`。如果光标不在首行(vim 有可能会记住
光标位置的)则用 `gg` 回到首行。然后直接用命令 `50@b`,看看会发生啥。没错,这
命令也将 buffer 内的文本行两两合并了,相当于执行了 50 次 `Jj` 命令。
所以 `@a``@b` 操作,正式地讲不叫“播放”宏,而是“读取寄存器,将其内容当作普
通命令来执行”。其实,当作普通命令来执行的内容,不仅可以放在内部寄存器,也可以
放在外部文件中。比如,只将 `Jj` 这两个字符保存到一个 `Jj.txt` 文件中,然后执行
ex 命令 `:source! Jj.txt`。当 `:source` 命令之后加个 `!` 符号,就是表示所读的
文件不是当作 ex 命令的脚本了,而是当作普通命令的“宏”了。在这个命令之前,请将光
标移到首行,至少不要末行,否则就看不到 `j` 的效果了。同时由于这个文件只保存了
一组 `Jj`,所以它只合并了两行。不过普通命令的序列组合可读性比较差,且很大程度
地依赖操作上下文,所以一般不会保存到外部文件,临时录制保存到寄存器较为常见。当
然你也可以先简单思考一下如何组织操作序列,明确地写出来,再复制或剪切到某个寄存
器中。
当明白了 `@a` 的执行意义,也就能更好地理解 `@=` 的意义了。这里,`=` 与 `a`
样是个寄存器,这个特殊寄存叫做表达式寄存器。
请在普通模式下,按下这两个键 `@=`,此时光标将跳到命令行的位置,不过前面不是 `:`
而是 `=` 了。vim 在等待你输入一个有效的表达式,再按回车执行。比如输入
`"Jj"<CR>`,这里 `<CR>` 表示回车结束输入并执行,注意 `"Jj"` 需要引号括起,这样
它才是个字符串常量表达式,否则若裸用 `Jj`,回车后 vim 会报错说 `Jj` 是个未定义
变量。
然后这整个按键序列 `@="Jj"<CR>` 的效果是什么?就是与普通命令 `Jj` 一样,合并两
行并下移。可以用 `:reg` 查看寄存器 `=` 中的内容也正是 `Jj`。所以,`@=` 的意图
是让用户临时输入一个表达式,vim 将计算该表达式的值,然后将结果值(应是字符串)
当作普通命令来执行。如果 `@=` 之后直接回车,不输入表达式,则延用原来保存在 `=`
寄存器中的值。
当你终于明白了 `@=` 的意义之后,就可以用 `@=` 来构建表达式映射了(终于回到正题
了)。例如:
```vim
: nnoremap \j @="Jj"<CR>
```
这样就可以用快捷键 `\j` 来“合并两行并下移”了。当然了,在这个简单的特定实例中,
所谓快捷键 `\j` 其实并不比直接输入 `Jj` 快多少。那个映射命令似乎也可以直接写成
`:nnoremap \j Jj`。然而问题的关键是,在 `@=``<CR>` 之间,可以使用几乎任意
合法的 VimL 表达式(即使不是所有),而不会是像 `"Jj"` 这样无趣的常量表达式。
举个实用的例子:
```vim
:nnoremap <Space> @=(foldlevel(line('.'))>0) ? "za" : "}"<CR>
```
这个映射是说用空格键来切换折叠,即相当于命令 `za`,但如果当前行根本就没有折叠
,那就无所谓切换折叠了,那就换用命令 `}` 跳到下一个空行。这里用到了条件表达式
`?:`,我在脚本中很少用这个,不必省 `if else` 的输入,但在定义一些映射时条件表
达式却是极简捷实用的。
在插入模式下(包括命令行模式),不是用 `@` 键调取寄存器,而是用另一个快捷键
`<C-R>`。比如 `<C-R>a` 就表示将寄存器 `a` 的内容插入到当前光标位置上。如果用
`<C-R>=` 就表示将要读取表达式寄存器的内容了,此时光标也会跳到命令行处,允许你
输入一个表达式后按回车,vim 就将表达式的计算值插入到光标处。例如:
```vim
: inoremap <F2> <C-R>=strftime("%Y/%m/%d")<CR>
```
它定义了一个映射,使用快捷键 `<F2>` 在当前光标处插入当前日期(请参阅
`strftime()` 函数的用法)。
然后再来看 `<expr>` 参数的意义与用法,比如以下两个映射定义是等效的:
```vim
: nnoremap \j @="Jj"<CR>
: nnoremap <expr> \j "Jj"
```
可见,在使用了 `<expr>` 参数后,`@=<CR>` 就没必要了,直接将后面的 `{rhs}` 参数
部分当作一个表达式,vim 首先计算这个表达,然后将其结果值当成真正的 `{rhs}`
数来解析为按键序列。
再尝试将上面那个空格切换折叠的快捷键改写成 `<expr>`
```vim
:nnoremap <expr> <Space> (foldlevel(line('.'))>0) ? "za" : "}"
```
(注:我在 vim8.0 中测试该映射有效,但在 vim7.4 中同样的映射无效,可能在低版本
`<expr>` 对条件表达式的 `?:` 的支持不完全,但对于其他简单表达式无问题)。
除了应用条件表达式,当计算 `{rhs}` 需要涉及更复杂的逻辑时,还可以包装在一个函
数中,那就几乎有着无限的可能了。仍以切换折叠的示例,改写成函数就如:
```vim
: function! ToggleFold()
: if foldlevel(line('.')) > 0
: return "za"
: else
: return "}"
: endif
: endfunction
:nnoremap <expr> <Space> ToggleFold()
```
不过要注意,VimL 函数的默认返回值是数字 `0`,如果在函数中忘了返回值,或在某个
分支中忘了返回值,那就可能导致奇怪的结果。例如,将上面的 `ToggleFold()` 函数改
写成:
```vim
: function! ToggleFold()
: if foldlevel(line('.')) > 0
: let l:rhs = "za"
: else
: let l:rhs = "}"
: endif
: " return l:rhs
: endfunction
:nnoremap <expr> <Space> ToggleFold()
```
假装忘了返回 `l:rhs`,那么快捷键 `<Space>` 将取得 `ToggleFold()` 的默认返回值
`0`,就是移到行首的意思了。取消 `:return l:rhs` 行的注释,可使之恢复正常使用。
当然了,用于表达式映射 `<expr>` 的函数还是有些限制的:
* 不能改变 buffer 内容
* 不能跳到其他窗口或编辑另一个 buffer
* 不能再使用 `:normal` 命令
* 虽然可在函数内移动光标,以便实现某些逻辑,但在返回 `{rhs}` 后会自动恢复光标
,所以移动光标是无效的。
总之,映射的表达式函数尽量保持逻辑简明,以返回一个字符串作为 `{rhs}` 为主,避
免在其内执行有其他副作用的操作。更多内容请参考帮助 `:help :map-<expr>`
### \*命令后缀映射
定义命令后缀映射的命令是 `:omap`,当然最好用 `:onoremap`。要能定义有趣的命令后
缀映射,首先就要理解命令后缀模式(Operator-pending,直译操作符悬挂模式)。
Vim 普通模式下的许多命令都是“操作符+文本对象”范式。比如最常见的 `y` `d` `c`
是操作符,当你按下这几个键之一后,就进入了所谓的“命令后缀”模式,vim 会等待你输
入后续的操作目标即文本对象。文本对象包括以下两大类:
1. 使用移动命令后光标扫描过的文本区域,即光标停靠点与原来光标位置之间的区域。
2. 预定义的文本对象,常用的有:
- `ap` `ip` 一个段落,段落由空行分隔,`ap` 包括下一个空行,`ip` 不包括。
- `a(` `i(``a)` `i)` 一个小括号,`a-` 表示包括括号本身,`i-` 只是括号内
部部分。
- `a[` `a]` `a{` `a}`,`i[` `i]` `i{` `i}` 与小括号类似。
- `a"` `a'` ,`i"` `i'` 与小括号类似,但是由引号括起的部分。
Vim 允许用户分别独立定义操作符与文本对象,然后任意组合。命令后缀映射就是可用
`:omap` 自定义文本对象。
还是举个例子。假如你需要经常操作双引号的字符串,觉得每次用 `i"` 略麻烦,因为它
实际上是三个键,还要按个 `Shift` 键呢。你想选个单键来代替这三个键,比如说 `q`
键吧。首先,你可能尝试作如下映射定义:
```vim
: nnoremap dq di"
: nnoremap cq ci"
```
然而,这只是个普通模式下的映射,并非命令后缀模式下映射,它不具备普适性。这里只
定义了 `dq``cq` 就表明只能用这两个快捷键,但 `yq` 就无效了(复制字符串?)
,其他自定义的操作符当然也就无效。
然后试试改成一个命令后缀映射:
```vim
:onoremap q i"
```
这样,`cq` `dq``yq` 都有效了,如果你知道如何自定义操作符,它对自定义操作符
也有效。
一个功能更丰富的例子请参考我写的一个小插件:
https://github.com/lymslive/autoplug/tree/master/autoload/qcmotion
在命令后缀模式下,单键 `q` 不仅可以模拟 `i"``a"`,还可以模拟 `i(``a(`
等括号对象(基于一定的上下文与优先级判断)。它的映射命令如下:
```vim
: onoremap q :call qcmotion#func#OpendMove()<CR>
```
不过它所调用的函数实现略复杂,不便全部引用,有兴趣的请参阅源代码。
总结下命令后缀映射的机制,对于 `:onoremap {lhs} {rhs}` 映射。首先将 `{rhs}`
作普通模式下命令(按键序列)执行。如果执行后 vim 仍在普通模式下,且移动了光标
,则将前后两个时刻的光标位置之间的区域当作文本对象。如果执行后在可视模式,则将
选择部分的文本当作文本对象。内置命令 `dw` `dp` 类似前一种情况,而 `da(` `di(`
类似后一种情况。
命令后缀映射的另一方面是操作符映射。也可以称之为命令前缀映射吧。这样,很多普通
模式下的操作就可理解为“命令前缀”与“命令后缀”的组合了。定义满足这样特性的操作符
的映射要分两步:
1. 设定选项 `operatorfunc`,其值一般是个函数名,用该函数来执行相应的工作。
2. 用命令 `g@` 激活这个函数调用。
当然了,不要将这两步分开,如果单独将 `operatorfunc` 选项设置放在 `vimrc` ,那
就只能定义一个操作符了。最好是类似如下定义:
```vim
: nnoremap {lhs} :set operatorfunc=OperaFunc<CR>g@
```
就是临时设定 `operatorfunc` 的选项值,然后激活它。这样就能为不同的 `{lhs}`
义为不同操作符了。
操作符函数 `OperaFunc()` 有一定的规范。它收受的第一个参数表示文本对象的选择模
式(即三种可视模式之一),这个参数是该操作符后面所接的文本对象自动传递给它的,
其值为以下三种,在函数内可根据不同值作不同处理:
* "line" 行选择模式
* "char" 字符选择模式
* "block" 列块选择模式
同时,在该函数内可利用 `'[``']` 这两个光标标记(mark)取得所操作文本对象的
范围。即相当于文本对象的选择范围,加上参数所指示的选择模式,就获得了足够的信
息来操作文本对象了。
### 缩写映射
缩写也是一种映射,不过只用于可输入模式下。包括插入模式与命令行模式,以及不太常
用的替换模式。其命令与映射也类似,不过将 `map` 换成 `abbreviate`,如:
```vim
: abbreviate {lhs} {rhs}
: noreabbrev {lhs} {rhs}
: iabbreviate {lhs} {rhs}
: cabbreviate {lhs} {rhs}
: unabrrev {lhs}
: abclear {lhs}
```
也包括定义(退化参数来列表查询)、删除一个、清除所有缩写的命令。同样可以用
`nore` 限定,与模式前缀限制(但只有 `i``c`分别表示插入模式与命令行模式)。
缩写的含义是当你输入 `{lhs}` 时,自动替换为 `{rhs}`。不过由于在插入模式,字符
是连续输入的,所以还有一些限定规则才能让 vim 识别刚才输入的几个字符是某个缩写
`{lhs}`
Vim 支持三类缩写,根据 `{lhs}` 中关键字位置区分。所谓关键字就是 `iskeyword`
项,一般认为数字、字符是关键字,其他标点符号与空白不是关键字。
* 全关键字(full-id),即 `{lhs}` 全部由关键字组成。必须完全匹配,即 `{lhs}`
之前不能有其他关键字。
* 关键字后缀(end-id),最后一个字符是关键字,前面的都不是关键字。
* 非关键字后缀(non-id),最后一个字符不是关键字,前面的可以是任意字符(空格与
制表符除外)。
其中,全关键字是最常用的缩写,最直接的想法是用它来纠正拼写错误,如:
```vim
: abbreviate teh the
: abbreviate higth hight
```
下面两例是另外两类缩写:
```vim
: abbreviate #i #include
: abbreviate inc# #include
```
在使用缩写时,还要输入一个额外的键来触发识别缩写,这也叫缩写的展开。一般地,输
入一个非关键字后,就会试图向前回溯寻找是否有缩写。最常用的是空格与制表符,还有
离开插入模式的 `<Esc>` 与离开命令行模式的 `<CR>`。当缩写展开后,这个触发字符也
同时会插入在被展开的 `{rhs}` 后,如果这不是想用的效果,可用一个快捷键 `<C-]>`
作为纯粹的缩写展开,而不会插入额外字符。
缩写同样支持 `<buffer>``<expr>` 参数。例如:
```vim
: abbreviate today= <C-R>=strftime("%Y/%m/%d")<CR>
: abbreviate <expr> today= strftime("%Y/%m/%d")
```
这两个缩写定义是等效的,在你输入 "today=" 之后(再空格或`<C-]>`等触发)就会替
换为今天的日期。
那么它与插入模式下的映射又有什么不同呢:
```vim
: inoremap <expr> today= strftime("%Y/%m/%d")
```
如果把 "today=" 定义为映射的话,那么在输入前面几个字符 "today" 之前都不会上屏
,接着输入 "=" 后立即上屏。这个体验并不好,因为你即使输入 "to" 时,vim 也会等
待,根据后续字符才能决定是否当作映射处理。
而定义为缩写的话,展开之前的字符是直接上屏的,是否展开的决定延迟,且可由用户
决定是否展开。如果用户想抑止 "today=" 的展开,比如确实想在这个字符串之后输入个
空格,则可用 `<C-v><Space>` 输入下一个空格。`<C-v>` 是插入模式下的转义快捷键,
它后面接入的按键都屏蔽了其特殊意义,就按其字面字符输入。
### 结语
使用映射,除了一些基本的命令语法技巧外,更重要的是自己的统一习惯。可以多多凝视
一下你的键盘布局,想想定义哪些快捷键自己会觉得比较方便与舒服。合适的快捷键对于
每个人可能会有不同,不过有些键强烈建议不要重映射,请保留其默认意义:
* 数字不要被映射,数字用于表示命令的重复次数。
* 冒号 `:` 进入命令行不要改,当然如果觉得冒号不好按,可以将其他键也映射为冒号
。两样建议保留的键是 `<Esc>` `@` 键。
* 插入模式下的 `<C-v>``<C-r>`。Vim 的插入模式的默认快捷键确实不如普通模式
方便,于是有些用户想把 Emacs 那套快捷键映射过来。或者 Window 用户想将 `<C-v>`
当作粘贴使用。然后这两个键在 Vim 映射中确实有特殊意义,经常能用来救急,还是
保留的好。此外 `<C-o>` 是临时回到普通模式使用一个普通命令,也是很有用的,尽
可能保留。
另外,关于 `<Leader>` 的使用。如果基本只用一种映射前缀,使用 `<Leader>` 是方便
的。但如果使用了多个 `<Leader>` 以对应不同类别的快捷键,则不太建议使用
`<Leader>` ,直接写出映射前缀字符就是。毕竟 `mapleader` 是个全局变量,若要经常
改变其值,就不容易维护了。
除了映射与缩写,Vim 的自定义命令与自定义菜单的用法与思想也是类似的。自定义菜单
是只用于 `gVim` 的,本教程不打算介绍,而自定义命令将在一下节介绍。