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
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` 的,本教程不打算介绍,而自定义命令将在一下节介绍。
|