Vimscript 实例研究:Grep运算符(Operator),第二部分
目前为止,我们已经完成了一个原型,是时候扩充它,让它更加强大。
记住:我们初始目标是创建"grep运算符"。我们还需要做一大堆新的东西来达成目标, 但要像前一章的过程一样:从简单的东西开始,并逐步改进直到它满足我们的需求。
在开始之前,注释掉~/.vimrc
中在前一章创建的映射。我们还要用同样的快捷键来映射新的运算符。
新建一个文件
创建一个新的运算符需要许多命令,把它们手工打出来将很快变成一种折磨。 你可以把它附加到~/.vimrc
,但让我们为这个运算符创建一个独立的文件。我们有足够的必要这么做。
首先,找到你的Vimplugin
文件夹。在Linux或OS X,这将会是~/.vim/plugin
。 如果你是Windows用户,它将位于你的主目录下的vimfiles
文件夹。(如果你找不到,在Vim里使用`:echo $HOME命令) 如果这个文件夹不存在,创建一个。
在plugin/
下新建文件grep-operator.vim
。这就是你放置新运算符的代码的地方。 一旦文件被修改,你可以执行:source %
来重新加载代码。 每次你打开Vim,这个文件也会被重新加载,就像~/.vimrc
。
不要忘了,在你source之前,你_必须_先保存文件,这样才能看到变化!
骨架(Skeleton)
要创建一个新的Vim运算符,你需要从两个组件开始:一个函数还有一个映射。 先添加下面的代码到grep-operator.vim
:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
function! GrepOperator(type)
echom "Test"
endfunction
保存文件并用:source %
source它。尝试通过按下<leader>giw
来执行"grep整个词"。 Vim将在接受iw
动作(motion)后,输出Test
,意味着我们已经搭起了骨架。
函数部分是简单的,没有什么是我们没讲过的。不过映射部分比较复杂。 我们首先对函数设置了operatorfunc
选项,然后执行g@
来以运算符的方式调用这个函数。 看起来这有点绕,不过这就是Vim工作的原理。
暂时把这个映射看作黑魔法吧。稍后你可以到文档里一探究竟。
可视模式
我们已经在normal模式下加入了这个运算符,但还想要在visual模式下用到它。 在之前的映射下面添加多一个:
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
保存并source文件。现在在visual模式下选择一些东西并按下<leader>g
。 什么也没发生,但Vim确实输出了Test
,所以我们的函数已经运行了。
之前我们就见过<c-u>
,但是还没有解释它是做什么的。试一下在可视模式下选中一些文本并按下:
。 Vim将打开一个命令行就像平时按下了:
一样,但是命令行的开头自动添加了'<,'>
!
Vim为了提高效率,插入了这些文本来让你的命令在被选择的范围内执行。 但是这次,我们不需要它添倒忙。我们用<c-u>
来执行"从光标所在处删除到行首的内容",移除多余文本。 最后剩下一个孤零零的:
,为调用call
命令作准备。
我们传递过去的visualMode()
参数还没有讲过呢。 这个函数是Vim的内置函数,它返回一个单字符的字符串来表示visual模式的类型: "v"
代表字符宽度(characterwise),"V"
代表行宽度(linewise),Ctrl-v
代表块宽度(blockwise)。
动作类型
我们定义的函数接受一个type
参数。我们知道在visual模式下它将会是visualmode()
的返回值, 但是在normal模式下呢?
编辑函数体部分,让代码像这样:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
function! GrepOperator(type)
echom a:type
endfunction
Source文件,然后继续并用多种的方式测试它。你可能会得到类似下面的结果:
- 按下
viw<leader>g
显示v
,因为我们处于字符宽度的visual模式。 - 按下
Vjj<leader>g
显示V
,因为我们处于行宽度的visual模式。 - 按下
<leader>giw
显示char
,因为我们在字符宽度的动作(characterwise motion)中使用该运算符。 - 按下
<leader>gG
显示line
,因为我们在行宽度的动作(linewise motion)中使用该运算符。
现在我们已经知道怎么区分不同种类的动作,这对于我们选择需要搜索的词是很重要的。
复制文本
我们的函数将需要获取用户想要搜索的文本,而这样做最简单的方法就是复制它。 把函数修改成这样:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
function! GrepOperator(type)
if a:type ==# 'v'
execute "normal! `<v`>y"
elseif a:type ==# 'char'
execute "normal! `[v`]y"
else
return
endif
echom @@
endfunction
哇。好多新的东西啊。试试按下<leader>giw
,<leader>g2e
和vi(<leader>g
看看。 每次Vim都会输出动作所包括的文本,显然我们已经走上正道了!
让我们把这段代码一步步分开来看。首先我们用if
语句检查a:type
参数。如果是'v'
, 它就是使用在字符宽度的visual模式下,所以我们复制了可视模式下的选中文本。
注意我们使用大小写敏感比较==#
。如果我们只用了==
而用户设置ignorecase
, "V"
也会是匹配的,结果_不会_如我们所愿。重视防御性编程!
if
语句的第二个分支则会拦住normal模式下使用字符宽度的动作。
剩下的情况只是默默地退出。我们直接忽略行宽度/块宽度的visual模式和对应的动作类型。 Grep默认情况下不会搜索多行文本,所以在搜索内容中夹杂着换行符是毫无意义的。
我们每一个if
分支都会执行normal!
命令来做两件事:
- 在可视状态下选中我们想要的文本范围:
- 先移动到范围开头,并标记
- 进入字符宽度的visual模式
- 移动到范围结尾的标记
- 复制可视状态下选中的文本。
先不要纠结于特殊标记方式。你将会在完成本章结尾的练习时学到为什么它们会不一样。
函数的最后一行输出变量@@
。不要忘了以@
开头的变量是寄存器。@@
是"未命名"(unnamed)寄存器: 如果你在删除或复制文本时没有指定一个寄存器,Vim就会把文本放在这里。
简明扼要地说:我们选中要搜索的文本,复制它,然后输出被复制的文本。
转义搜索文本
既然得到了Vim字符串形式的需要的文本,我们可以像前一章一样将它转义。修改echom
命令成这样:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
function! GrepOperator(type)
if a:type ==# 'v'
normal! `<v`>y
elseif a:type ==# 'char'
normal! `[v`]y
else
return
endif
echom shellescape(@@)
endfunction
保存并source文件,然后在可视模式下选中带特殊字符的文本,按下<leader>g
。 Vim显示一个被转义了的能安全地传递给shell命令的文本。
执行Grep
我们终于可以加上grep!
命令来实现真正的搜索。替换掉echom
那一行,代码看起来就像这样:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
function! GrepOperator(type)
if a:type ==# 'v'
normal! `<v`>y
elseif a:type ==# 'char'
normal! `[v`]y
else
return
endif
silent execute "grep! -R " . shellescape(@@) . " ."
copen
endfunction
看起来眼熟吧。我们简单地执行上一章得到的silent execute "grep! ..."
命令。 由于我们不再把所有的代码塞进单个nnoremap
命令里,现在代码甚至更加清晰易懂了!
保存并source文件,然后尝试一下,享受自己辛勤劳动的成果吧!
因为定义了一个全新的Vim运算符,现在我们可以在许多场景下使用它了,比如:
viw<leader>g
: 可视模式下选中一个词,然后grep它。<leader>g4w
: Grep接下来的四个词。<leader>gt;
: Grep到分号为止的文本。<leader>gi[
: Grep方括号里的文本.
这里彰显了Vim的优越性:它的编辑命令就像一门语言。当你加入新的动词,它会自动地跟(大多数)现存的名词和形容词搭配起来。
练习
阅读:help visualmode()
。
阅读:help c_ctrl-u
。
阅读:help operatorfunc
。
阅读:help map-operator
。