第一部分
在本章和下一章中,我们将使用Vimscript来实现一个相当复杂的程序。我们将探讨一些闻所未闻的东西, 也将在实战中把之前学过的东西联系起来。
在本实例研究中,遇到不熟悉的内容,你得用:help
弄懂它。如果你只是走马观花,就将所获无多。
Grep
如果你未曾用过:grep
,现在你应该花费一分钟读读:help :grep
和:help :make
。 如果之前没用过quickfix window,阅读:help quickfix-window
。
简明扼要地说::grep ...
将用你给的参数来运行一个外部的grep程序,解析结果,填充quickfix列表, 这样你就能在Vim里面跳转到对应结果。
我们将会添加一个"grep运算符"到任意Vim的内置(或自定义!)的动作中,来选择想要搜索的文本, 让:grep
更容易使用。
用法
在写下每一个有意义的Vimscript程序的第一步,你需要思索一个问题:“它会被用户怎么使用呢?”。 尝试构思出一种优雅,简易,符合直觉的调用方法。
这次我会替你把这活干了:
- 我们将创造一个"grep运算符"并绑定到
<leader>g
。 - 它将表现得同其他任意Vim运算符一样,还可以加入到组合键(比如
w
和i{
)中。 - 它将立刻开始搜索并打开quickfix窗口展示结果。
- 它将_不会_跳到第一个结果,因为当第一个结果不是你想要的时候,这样做会困扰你。
一些你将怎么使用它的用例:
<leader>giw
: Grep光标下的词(word)。<leader>giW
: Grep光标下的词的大写形式(WORD)。<leader>gi'
: Grep当前所在的单引号括住的词。viwe<leader>g
: 可视状态下选中一个词并拓展选择范围到下一词,然后Grep。
有很多,_很多_其他的方法可以用它。看上去它好像需要写很多,很多代码, 但事实上我们只需要实现"运算符"功能然后Vim就会完成剩下的工作。
一个原型
在埋头写下巨量(trickey bits)的Vimscript之前,有一个也许会帮上忙的方法是简化你的目标并实现它, 来推测你最终解决方案可能的"外形"。
让我们简化我们的目标为"创造一个映射来搜索光标下的词"。这有用而且应该更简单,所以我们能更快得到可运行的成果。 目前我们将映射它到<leader>g
。
我们从一个映射骨架开始并逐渐填补它。执行这个命令:
:nnoremap <leader>g :grep -R something .<cr>
如果你阅读过:help grep
,你就能轻易理解这个命令。我们之前也看过许多映射,这里没有什么是新的。
显然我们还没做什么,所以让我们一步步打磨这个映射直到它符合我们的要求。
搜索部分
首先我们需要搜索光标下的词,而不是something
。执行下面的命令:
:nnoremap <leader>g :grep -R <cword> .<cr>
现在试一下。<cword>
是一个Vim的command-line模式的特殊变量, Vim会在执行命令之前把它替换为"光标下面的那个词"。
你可以使用<cWORD>
来得到大写形式(WORD)。执行这个命令:
:nnoremap <leader>g :grep -R <cWORD> .<cr>
现在试试把光标放在诸如foo-bar
的词上面。Vim将grepfoo-bar
而不是其中的一部分。
我们的搜索部分还有一个问题:如果这里面有什么特殊的shell字符,Vim会毫不犹豫地传递给外部的grep命令。 这样会导致程序崩溃(或更糟:铸成某些大错)。
让我们看看如何使它挂掉。输入foo;ls
并把光标放上去执行映射。grep命令失败了, 而Vim将执行ls
命令!这肯定糟透了,如果词里包括比ls
更危险的命令呢?
为了解决这个问题,我们将调用参数用引号括起来。执行这个命令:
:nnoremap <leader>g :grep -R '<cWORD>' .<cr>
大多数shell把单引号括起来的内容当作(大体上)字面量,所以我们的映射现在更加健壮了。
转义Shell命令参数
搜索部分还有一个问题。在that's
上尝试这个映射。它不会工作,因为词里的单引号与grep命令的单引号发生了冲突!
为了解决问题,我们可以使用Vim的shellescape
函数。 阅读:help escape()
和:help shellescape()
来看它是怎样工作的(真的很简单)。
因为shellescape()
要求Vim字符串,我们需要用execute
动态创建命令。 首先执行下面命令来转换:grep
映射到:execute "..."
形式:
:nnoremap <leader>g :execute "grep -R '<cWORD>' ."<cr>
试一下并确信它可以工作。如果不行,找出拼写错误并改正。 然后执行下面的使用了shellescape
的命令。
:nnoremap <leader>g :execute "grep -R " . shellescape("<cWORD>") . " ."<cr>
在一般的词比如foo
上执行这个命令试试。它可以工作。再到一个带单引号的词,比如that's
,上试试看。 它还是不行!为什么会这样?
问题在于Vim在拓展命令行中的特殊变量,比如<cWORD>
,的之前,就已经执行了shellescape()
。 所以Vim shell-escaped了字面量字符串"<cWORD>"
(什么都不做,除了给它添上一对单引号)并连接到我们的grep
命令上。
通过执行下面的命令,你可以亲眼目睹这一切。
:echom shellescape("<cWORD>")
Vim将输出'<cWORD>'
。注意引号也是输出字符串的一部分。Vim把它作为shell命令参数保护了起来。
为解决这个问题,我们将使用expand()
函数来强制拓展<cWORD>
为对应字符串, 抢在它被传递给shellescape
之前。
让我们单独看看这一部分是怎么工作的。把你的光标移到带单引号的词(比如that's
)上去, 并执行下面命令:
:echom expand("<cWORD>")
Vim输出that's
,因为expand("<cWORD>")
以Vim字符串的形式返回当前光标下的词。 是时候加入shellescape
的部分了:
:echom shellescape(expand("<cWORD>"))
这次Vim输出'that'\''s'
。 如果觉得这看上去真可笑,你大概没有感受过看透了各种shell转义的疯狂形式后的淡定吧。 目前,不用为此而纠结。就相信Vim接受了expand
的输出并正确地转义了它。
目前我们已经得到了光标下的词的彻底转义版本。是时候连接它到我们的映射了! 执行下面的命令:
:nnoremap <leader>g :exe "grep -R " . shellescape(expand("<cWORD>")) . " ."<cr>
试一下。这个映射不再有问题,即使我们用它搜索带古怪符号的词。
"从简单的Vimscript开始并一点点转变它直到达成你的目标"这样的工作方式将会被你一再取用。
整理整理
在完成映射之前,还要处理一些小问题。首先,我们说过我们不想自动跳到第一个结果, 所以要用grep!
替换掉grep
。执行下面的命令:
:nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>
再一次试试,发现什么都没发生。Vim用结果填充了quickfix窗口,我们却无法打开。 执行下面的命令:
:nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
现在试试这个映射,你将看到Vim自动打开了包含搜索结果的quickfix窗口。 我们所做的仅仅是在映射的结尾续上:copen<cr>
。
最后一点,在搜索的时候,我们要移除Vim所有的grep输出。执行下面的命令:
:nnoremap <leader>g :silent execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
我们完成了,试一试并犒劳一下自己吧!silent
命令仅仅是在运行一个命令的同时隐藏它的正常输出。
练习
把我们刚刚做出来的映射加入到你的~/.vimrc
文件。
如果你未曾读过:help :grep
,去读它。
阅读:help cword
。
阅读:help cnext
和help cprevious
。修改你的grep映射,试一下它们。
设置:cnext
和:cprevious
的映射,让在匹配内容间的移动更加方便。
阅读:help expand
。
阅读:help copen
。
在我们创建的映射中加入height参数到:copen
命令中,看看quickfix窗口能不能以指定的高度打开。
阅读:help silent
。
第二部分
目前为止,我们已经完成了一个原型,是时候扩充它,让它更加强大。
记住:我们初始目标是创建"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
。
第三部分
我们新鲜出炉的"grep运算符"工作得很好,但是写Vimscript的目的,就是要体贴地改善你的用户的生活。 我们可以额外做两件事,让我们的运算符更加符合Vim生态圈的要求。
保护寄存器
由于把文本复制到未命名寄存器中,我们破坏了之前在那里的内容。
这并不是我们的用户想要的,所以让我们在复制之前先保存寄存器中的内容并于最后重新加载。 修改代码成这样:
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>
function! GrepOperator(type)
let saved_unnamed_register = @@
if a:type ==# 'v'
normal! `<v`>y
elseif a:type ==# 'char'
normal! `[v`]y
else
return
endif
silent execute "grep! -R " . shellescape(@@) . " ."
copen
let @@ = saved_unnamed_register
endfunction
我们在函数的开头和结尾加入了两个let
语句。 第一个用一个变量保存@@
中的内容,第二个则重新加载保存的内容。
保存并source文件。测试一下,复制一些文本,接着按下<leader>giw
来执行运算符, 然后按下p
来粘贴之前复制的文本。
当写Vim插件时,你_总是_应该尽量在修改之前保存原来的设置和寄存器值,并在之后加载回去。 这样你就避免了让用户陷入恐慌的可能。
命名空间
我们的脚本在全局命名空间中创建了函数GrepOperator
。 这大概不算什么大问题,但当你写Vimscript的时候,事前以免万一远好过事后万分歉意。
仅需增加几行代码,我们就能避免污染全局命名空间。把代码修改成这样:
nnoremap <leader>g :set operatorfunc=<SID>GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call <SID>GrepOperator(visualmode())<cr>
function! s:GrepOperator(type)
let saved_unnamed_register = @@
if a:type ==# 'v'
normal! `<v`>y
elseif a:type ==# 'char'
normal! `[v`]y
else
return
endif
silent execute "grep! -R " . shellescape(@@) . " ."
copen
let @@ = saved_unnamed_register
endfunction
脚本的前三行已经被改变了。首先,我们在函数名前增加前缀s:
,这样它就会处于当前脚本的命名空间。
我们也修改了映射,在GrepOperator
前面添上<SID>
,所以Vim才能找到这个函数。 如果我们不这样做,Vim会尝试在全局命名空间查找该函数,这是不可能找到的。
欢呼吧,我们的grep-operator.vim
脚本不仅非常有用,而且是一个善解人意的Vimscript公民!
练习
阅读:help <SID>
。
享受一下,吃点零食犒劳自己。
转载本站内容时,请务必注明来自W3xue,违者必究。