supertab是vim的一个出名的插件, 相信会vim的人没几个不知道的, 我在之前的<<vim之补全1>>中首先说明的也是它, supertab实现的功能简单的说就是用tab来来调用vim的补全功能, 这和linux的终端操作习惯完全一致, 并且方便而合理. supertab是我最早接触的vim插件, 保留至今而没有抛弃, 当然很多人可能已经不再使用这个插件, 因为更加自动化的nerocachecomplete和youcompleteme插件完全可以取而代之, 不过个人保留supertab的理由是用它来实现就近补全, 也就是vim自带的ctrl+p补全, 关于为何这么做我已经在<<vim之补全1>>中用相当多的话解释过. 有兴趣和耐心的读者可以移步阅读.
这篇文章主要是说明个人supertab功能上的一些改造. 首先说明为何要对supertab动手术, vim在编辑上的高效性源自纯键盘式的操作模式和高度定制能力. 这种能力同时也给我们带来了很多负面影响, vim不像virsual Studio那个拥有完整的方案体系, VS完整到我们甚至不用去了解它的任何实现机制, VS给我们的任务是要求我们学会按照它的方式来使用, vim给我们更大的一个任务往往是要求我们学会让vim按照我们的希望的方式来使用. 这种让用户主宰一切的能力对很多喜欢自由和设计的程序员有着巨大的吸引力. 可惜的凡事都有好的一面也有坏的一面, vim就像是一匹健壮的野马, 想要让他带你跑的更快之前你必须付出相当大的精力来驯化它. 好了, 回归正题, 首先说一下supertab本身的特点: supertab在默认的配置下基本上可以和ctrl+p补全划上等号, 当前我们可以通过complete这个vim参数来配置ctrl+p的补全索引范围. 为了提高速度和精确度, 个人的配置中complete只保留了对当前缓冲,后台缓冲和其他窗口的搜索. 也就是基本上是一个标准的就近补全功能. supertab在插入模式下光标前面如果有单词时通过<tab>触发, 触发后弹出补全列表, 补全列表中的关键词是按照距离光标远近排列的,因此越近的单词出现在补全列表越靠前的位置, 但问题出现vim的作者为了实现这种就近补全, 使用了一个很巧妙的方法:将对上下文正向索引建立的补全列表从后往前倒着选择. 也也就是指ctrl+p补全的第一个选项是正向列表的最后一个. 这个方式可以大大简化和统一vim的补全列表显示算法, supertab遵循了bram的本意, 默认的tab触发后也会倒着选择, 并且在补全列表出现后通过tab键我们可以继续倒着向上选择. 可是在我个人常用的补全并不是只有ctrl+p一种, 为了最大限度的提高的补全的精确度和效率, 同时使用的又常用到还有字典补全(ctrl+x ctrl+k触发, 个人映射到"jj"双键上), tag补全(ctrl+x ctrl+]触发, 个人映射在"kk"上)等, 这些补全在触发后补全列表并不会像ctrl+p那样倒着显示, 首选项也落在了顺数第一项上, 这个时候如果我们再次使用tab键, 由于supertab的影响, 选项依然会倒着选择, 也就是会立刻从顺数第一项跳转到倒数第一项, 然后往回选择. 我们本能的使用tab键是为了能选择顺数第二个选项, 结果这个时候tab确是倒序选择, 这就是supertab最大的问题所在, 实践也证明我们习惯性操作被这种一个键在两种情形下功能不一致所打乱是一件非常恼人的事情.
为了解决这个问题, 最初的想法是通过修改supertab的源码脚本来达到tab在选择上的功能更加合理化. 可惜的是在花掉了近半天的时间琢磨supertab的脚本后依然一无所获, 由于个人对vim脚本的熟悉程度上的有限, 即便是在我读懂了大多数supertab脚本的前提下还是不知道从怎么修改. 好在花费的时间总是有所收获的, 在慢慢知道了supertab的工作原理后, 我开始想别的办法, 首先意识到的是supertab实现的功能要比我现在使用到的要多的多, 我只是把supertab当成了ctrl+p的一个更加便捷的一个映射来使用. 其他supertab的特性几乎全部可以忽略. 由于需求上的简单让我想到了试着自己实现一个最简单的supertab, 在试着自己实现supertab最基本的ctrl+p功能的同时我也找到了可以让tab转换选择方向的方法. 事实上我们的tab只需要在ctrl+p补全中倒叙选择, 其他地方几乎统一的需要它正向选择, 而问题出在找不到一个合理的机制来精确标记从ctrl+p触发到一次补全结束这个过程. 在不精通vim的脚本的前提下我想不到办法来标识ctrl+p补全的结束. 后来想想虽然找不到ctrl+p的结束位置, 但ctrl+p补全的开始位置和其他补全的开始位置都是可以精确的被找到, 因此, 可以通过一个全局标志位来实现在在需要切换选择方向的补全开始处切换tab功能, 具体实现如下:
在vim的运行时目录中任意建立一个.vim结尾的vim脚本, 个人是删除了原来的supertab.vim在~/.vim/vunder/supertab/plugin/目录下新建了一个mysupertab.vim文件, 在文件中写入如下内容:
func! MySuperTab(cmd) "cmd 参数标识是tab 还是shift+tab
if pumvisible() pumvisible函数在vim存在补全列表窗口时返回真
if g:issupertab == 0 "全局变量issupertab标识是否是ctrl+p补全, 它会在.vimrc中初始化为0, 在第一次触发ctrl+p补全时被置1
return a:cmd == 'n' ? "<c-n>" : "<c-p>" "如果cmd是n表示tab映射, issupertab == 0表示现在是其他补全, 因此返回<c-n>来正向选择
else
return a:cmd == 'n' ? "<c-p>" : "<c-n>" "issupertab == 1会触发这里, 说明现在是ctrl+p补全, 因此'n'参数将返回<c-p>来实现逆向选择
endif
endif
if strpart(getline("."), col(".") - 2, 1) == ' ' || col(".") == 1 "如果光标前面的字符是空格或者光标在行首, tab和shifttab都返回真正的tab
return "<tab>" "因此如果希望输入真实的tab需要在行首或者先输入一个空格再按下tab, 当然我们最好开启vim的expandtab属性, vim对tab前有空格
else "存在时插入tab的处理相当的智能和方便, 你可以自己的试着测试一下, 不过, 我个人的tab长度被设置成了奇葩的3
let g:issupertab = 1 "如果不存在弹出列表, 又不存在前导空格或行首标识,这里将触发ctrl+p补全, 因此我们将issupertab置1来标识之后的补全全部是ctrl+p补全, 这个标识会一直存在并且影响tab的行为是倒序选择, 因此, 任何需要正向选择的列表功能在触发时需要将issupertab置0
return a:cmd == 'n' ? "<c-p>" : "<c-n>"
endif
endfunc
" 下面的函数是其他不同补全的入口函数, 这些入口函数只是在返回正常触发组合键之前将issupertab置0以便让之后的tab选择功能是正向的,这些置0操作同样是具有粘滞性
func! Mycxck() "字典补全
let g:issupertab = 0
return "<c-x><c-k>"
endfunc
func! Mycxct() "tag补全
let g:issupertab = 0
return "<c-x><c-]>"
endfunc
func! Mycxcf() "文件路径补全
let g:issupertab = 0
return "<c-x><c-f>"
endfunc
func! Mycxci() "包含文件补全
let g:issupertab = 0
return "<c-x><c-i>"
endfunc
func! Mycxcu() "用户自定义补全
let g:issupertab = 0
return "<c-x><c-u>"
endfunc
在.vimrc需要做的相关配置如下:
set expandtab
ino jj <c-r>=Mycxck()<cr>
ino kk <c-r>=Mycxct()<cr>
ino JJ <c-r>=Mycxcf()<cr>
ino KK <c-r>=Mycxci()<cr>
ino JK <c-r>=Mycxcu()<cr>
"mysupertab
let g:issupertab = 0
imap <script> <tab> <c-r>=MySuperTab('n')<cr>
imap <script> <s-tab> <c-r>=MySuperTab('p')<cr>
实现效果:
vim刚进入时tab补全选择方向默认是正向的, 如果在插入模式下非行首和空格之后直接使用tab键将触发ctrl+p的就近补全. 此时补全列表选择方向切换到反向选择. 之后将一直保持反向,直到通过jj触发字典补全的时候tab选择列表的方式才会被切换正向补全.如果再次触发ctrl+p补全方向再次切换....
这样的切换方式基本实现了tab在补全列表上的方向的自动切换, 并且由于简化了tab的触发机制因此在vim性能上会有所提升.但存在的问题是任何需要用到vim弹出列表功能的插件为保证tab选择行为的一致性, 都需要小小的hack一下, 比如clang_complete, 默认在输入. > 和::时会触发clang_complete为保证clang_complete在弹出的补全列表上tab键的正向选择, 需要在clang_complete源码中这个三个符号的触发函数中添加let g:issupertab == 0. 又比如FuzzyFinder, 它的弹出列表我测试过, 没有修改任何代码时<tab>总是会回到第一选项, shift-tab可以正常会向上选择, 不过这里我没有修改FuzzyFinder的任何代码, 因为我已经将它的向上和向下选项选择键设置成了<ctrl+j>和<ctrl+k>, 具体是源码中下面相关的代码被修改成这样:
function s:initialize()
"---------------------------------------------------------------------------
call l9#defineVariableDefault('g:fuf_modesDisable' , [ 'mrufile', 'mrucmd', ])
call l9#defineVariableDefault('g:fuf_keyOpen' , '<CR>')
call l9#defineVariableDefault('g:fuf_keyOpenSplit' , '<C-h>')
call l9#defineVariableDefault('g:fuf_keyOpenVsplit' , '<C-l>')
call l9#defineVariableDefault('g:fuf_keyOpenTabpage' , '<C-t>')
call l9#defineVariableDefault('g:fuf_keyPreview' , '<C-@>')
call l9#defineVariableDefault('g:fuf_keyNextMode' , '<C-u>')
call l9#defineVariableDefault('g:fuf_keyPrevMode' , '<C-y>')
call l9#defineVariableDefault('g:fuf_keyPrevPattern' , '<C-f>')
call l9#defineVariableDefault('g:fuf_keyNextPattern' , '<C-b>')
call l9#defineVariableDefault('g:fuf_keySwitchMatching', '<C-i>')
由于上面的源码修改后的设置中没有出现ctrl+j和ctrl+k, 因此vimrc中如下映射可以正常工作:
ino <c-j> <down>
ino <c-k> <up>
因此FuzzyFinder的弹出列表可以正常使用ctrl+j和ctrl+k, 甚至如下设置也是可以正常工作的:
ino <c-r> <left><left><left><left><left><left>
ino <c-f> <right><right><right><right><right><right>
事实上, 只要插件中没有对ctrl+j和ctrl+k等做过映射, 插件的弹出列表中上面的映射总是可以使用的. How wanderful!!!