严格意义上来说,快捷键的绑定应该是键盘映射,将某些键映射为另一些键。
在上篇我们介绍了基本的键盘映射操作,知道了如何 :map、:imap、:vmap、:nmap这些命令来映射键盘快捷键。它们很方便,也很简单,但是有一个致命的缺点。他们是递归的,我们先来讨论什么是递归
映射的递归问题
让我们先来执行下面的命令
代码语言:javascript复制:nmap jj J
:nmap J j
这里我们原本是想 jj来实现 J的功能,更加快速的实现合并行的功能,但是我们按下之后发现,它只是将光标移动到下一行了。这并不是我们想要的。到底发生什么了呢?
因为这些命令是递归的。如何理解递归呢?我们以函数的思想来考虑,每定义一个快捷键,就相当于定义了一个函数。并且在新定义的函数中调用老函数。依照这个思路我们来分析一下上述两个命令产生的结果:
- 首先定义了一个名为 jj的函数,它的函数代码为 J()
- 然后我们定义了一个名为 J的函数,它的代码定义为 j()
- 我们执行 jj函数的时候,它在函数内部调用 J(),J函数内部调用 j()。因此它表现出来的最终效果就是 jj等效与 j。
这些映射产生的伪代码如下:
代码语言:javascript复制void J()
{
j();
}
void jj()
{
J();
}
我们在调用 jj
这个函数的时候就相当于在调用 j
有点绕是不是呢。为了讲述这个问题,我们再来看这么一个例子
代码语言:javascript复制:nmap dd o<esc>kddj
我们来分析一下它的本意:
- 首先使用 o在光标所在行之下插入一行
- 退回到普通模式,并且让光标向上移动一行
- 删除光标所在行
- 移动到下一行,也就是刚刚的插入行
看起来这个命令的作用是清除本行,但是vim并没有这么做,只有按下 <C-c>
才能停下来,而且vim中多出了许多空白行。
依照上述分析思路,我们可以对这个命令的执行结果写出如下的伪代码
代码语言:javascript复制void dd()
{
o();
esc();
k();
dd();
j();
}
从上述的代码看出这个映射会陷入无限循环,或者叫死递归。我们只能使用 <Ctrl c>来终止。
在正式进入下一步之前让我们先删除这个映射。我们可以使用 :nunmpa
来删除一条快捷键映射,输入 :nunmap dd
来终止上述出错的映射。之前介绍的那些映射命令都有 un
系列的命令,例如 map
对应 unmap
,imap
对应 iunmap
。
我们从上面的几个例子应该看出来了, 之前介绍的函数好用是好用,但是会形成递归。在上述代码中还算是比较好找,如果我们配置文件大了,不同插件有自己的映射,而我们也会定义一堆自己的映射,这个时候出问题就难查了。
为了解决这个问题,vim提供了一系列的 nore
开头的函数。它相比于之前介绍的函数来说,是非递归的。之前每个命令对应的非递归版本如下:
- nmap 对应 nnoremap
- imap 对应 inorempap
- vmap 对应 vnoremap
- cmap 对应 cnoremap
相信各位应该看出来了,我们在原来命令的基础之上添加了 nore
作为非递归版本。我们来做一个试验
:nmap x dd
:nnoremap x
我们输入 发现它只删除了一个单词,即使用
:nnoremap
只保留了 作为
x
操作符的作用,而斩断了之前 x
被映射为 dd
的操作。
那么我们何时该使用递归版本,何时使用非递归版本呢?答案是在任何时候,永远使用非递归版本,现在就请各位小伙伴忘掉非递归版本把。现在多敲几个单词将来会省去大量排错时间。
lua 配置
到此位置我们学会了怎么使用 :map
系列的命令定义快捷键,同时也知道什么是快捷键之间的递归和非递归。可以说掌握了关于 vimscript
定义快捷键的基本方法。那么如何跟 lua
对应呢?
neovim 定义了一系列的函数帮助我们定义、获取和删除快捷键
- vim.api.nvim_set_keymap: 设置快捷键
- vim.api.nvim_get_keymap: 获取快捷键
- vim.api.nvim_del_keymap: 删除快捷键
我们可以通过帮助文档查到 vim.api.nvim_set_keymap
的定义如下:
nvim_set_keymap({mode}, {lhs}, {rhs}, {*opts})
mode
是一个字符串,对应着一个模式,即我们之前说的可视模式、插入模式或者普通模式,下面是各个模式对应的字符串名称 。
字符串 | 模式 | 对应的vim 命令 |
---|---|---|
"" | 所有模式 | :map |
"n" | 普通模式 | :nmap |
"v" | 可视模式 | :vmap |
"i" | 插入模式 | :imap |
"s" | 选择模式 | :smap |
"c" | 命令模式 | :cmap |
lhs
对应着一个键位,即我们想映射的键位,如果传入空字符串,相当于通过 :map传入 <NOP>
。表示将要禁用这个键
rhs
对应着将要执行的命令,是 :map
的第二个参数
opts
代表映射的其他属性,主要是一个表,你可以暂时理解为一个字典。比如可以使用 noremap
表示禁止递归,使用 silent
表示执行命令时不回显内容
例如我们在配置文件中定义
代码语言:javascript复制vim.api.nvim_set_keymap("n", "<space>", "/nvim<CR>", {noremap = true, silent = false})
通过这段代码,我们将 空格键映射为在文件中查找 nvim
字符。因为 silent
设置的是 false
因此我们在按下空格键之后在vim
的左下角会看到 /nvim
的字样
我们可以通过设置 silent = true
来取消这个回显。
vim.api.nvim_set_keymap("n", "<space>", "/nvim<CR>", {norema=true, silent = true});
映射leader 键
常见的映射主要出现在普通模式下,普通模式下的很多按键都有其特殊用途,而且还大多挺常用的,想来想去不怎么使用,而且位置好按的也就 <space>
、H
、L
、D
这些了,将他们进行映射,映射到常用功能,减轻我们的按键负担是再好不过了。但是我们常用功能又那么多,特别是装了插件之后的。这些键完全不够使用的。
这个时候我们要延续 emacs
或者其他软件的思路了,一个键不够就两个键,比如使用下面的映射
:noremap -d dd
:noremap -c ddO
这意味着我们可以使用一个键作为前缀,后面接其他字符,将他们作为一个整体来映射一个功能。多按一个键而已,比你输入整个命令要轻松多了。
这就引入一个新的思路了,我们可以统一定义一个键作为前缀键,后面添加一些字符来整体进行映射。当然我们可以手工这么做,每次需要多个按键的时候的就手动写上 -
或者其他的。但是后期我发现 -
并不容易按到,我觉得 <space>
或者 ,更容易按到,要进行修改,那么修改的量就太大了。
vim
中有一个被称之为 leader
键的东西来解决这个问题,我们可以提前定义一个 leader
,在映射的时候使用 <leader>
来代表对应的前缀键。例如,先设置 leader
为 <space>
后面再定义 <leader>d
作为 dd
,后面可以很方便的修改 <space>d
这个映射为 ,d
,只需要修改 leader
键的定义。
可以使用 :let mapleader = <space>
来定义 leader
键为空格。这里你可以定义成你喜欢的键,我平时喜欢用空格,因为它比较大,平时也在大拇指的位置,方便按。
然后我们可以使用 :noremap <leader>d dd
来定义映射
如何在 lua
中定义 leader
键呢?
从上面的 vimscript
代码中可以看到 mapleader
是用 let
关键字来设置的,一般 let
是用来设置变量的。lua
中自定义变量可以直接定义,但是 mapleader
明显是vim
自带的变量。跟设置选项类似,neovim
提供了两种访问 vim
内部变量的方式,一种是使用函数,一种是使用 元访问器。
跟变量有关的函数主要有:
vim.api.nvim_set_var()
:设置全局变量的值vim.api.nvim_get_var()
:获取全局变量vim.api.nvim_del_var()
:删除全局变量
当然使用元访问器会更加简便,对应的元访问器为 vim.g
。所以这里我们可以使用 vim.g.mapleader = " "
来设置
到此我们已经学会了关于映射的所有初级的内容,现在已经可以完成大部分的配置工作了。至于在定义快捷键的时候是使用 leader键还是使用多个普通键,看具体场景和各位小伙伴的使用习惯了。这里我就不给建议了,一切以方便好按为主。