开发一个 Vim 文件路径搜索插件
涛叔如果要我只能推荐一款 vim 插件,那我选 CtrlP。大约在 2013 年,我最早接触到这个插件(使用别人的 vimrc 配置)。当时还不知道它叫 CtrlP,只知道按一下 Ctrl + P 就可以按照文件路径搜索,而且是模糊匹配的,非常方便。后来我逐行研究当时用的 vimrc 文件,终于知道了 CtrlP 的大名。从此,再也没有离开过它。
CtrlP 是用纯 VimL 实现的,文件一多查询就会很慢。后来我偶然发现了 fzf。fzf 是 Go 语言开发的,速度极快,而且 fzf 在搜索的时候是边搜索边匹配边显示,所以显得更快。而且 fzf 自带了 vim 插件,当即放弃了 CtrlP。
fzf 自带的 fzf.vim 有八百多行,本文将用不到五十行的代码重新打造一个 fzf.vim。如果你对开发 vim 插件还不太了解,请先阅读这篇入门文章。
terminal 特性最早由 neovim 引入的,后来 vim 也实现了这个特性,却不跟 neovim 兼容,也不知道是不是故意的。兼容性问题体现在以下几个方面:
- 启动函数不同。neovim 使用
termopen()
,vim 使用term_start()
。 - 启动参数不同。退出回调参数在 neovim 上是
on_exit
,在 vim 上是exit_cb
。 - 启动行为不同。neovim 使当前窗口展示终端,默认处于 Normal 模式,得切换到 Insert 模式才能输入命令;vim 会新开一个窗口,打开就能输入命令。
- 读取方式不同。neovim 的
on_exit
回调可以直接使用getline()
函数读取 terminal 内容;vim 的exit_cb
回调则需要使用专门的term_getline()
函数读取。关于 terminal 的更多信息请移步:h terminal
。
理论讲完了,下面说一下实现思路。在底部打开一个新窗口,并在新窗口中启动 terminal,然后执行 fzf 命令。fzf 搜索完成后会将结果输出到 terminal。terminal 在 fzf 退出后会调用 on_exit/exit_cb
指定的回调函数。我们需要在回调中读取文件路径、关闭 terminal 窗口、打开匹配到的文件。下面上代码:
function! fzf#Open()
" 此处在底部新开一个窗口,高度为 9 行。
" keepalt 大家可以自己研究一下,有用,但不加问题也不大。
keepalt below 9 new
if has('nvim')
let options = {'on_exit': 'OpenFile'}
call termopen('fzf', options)
startinsert " 切换到 insert 模式
else
" vim 默认自己打开新窗口,加上 curwin 参数,指定使用当前窗口
" vim 还支持 term_name 参数用来设置 terminal 窗口的标题,挺好。
let options = {'term_name':'FZF','curwin':1,'exit_cb':'OpenFile'}
" b: 表示当前 buffer 可见,是一类介于全局变量和局部变量之间的变量
" term_start() 的返回值 term_getline() 要用到
let b:term_buf = term_start('fzf', options)
endif
endfunction
" 回调函数
function OpenFile(...)
" 获取 terminal 的当前工作目录。需要在 close 之前,请思考为什么。
let root = getcwd()
" 读取 fzf 返回结果
if has('nvim')
" neovim 中可以直接使用 getline()
let path = getline(1)
else
" vim 需要使用专门的函数
let path = term_getline(b:term_buf, 1)
endif
" 此处使用 close 关闭窗口,也可以使用 bdelete 删除 buffer
" 之前的 new 其实是创建了一个 buffer、一个 window、并将 buffer 和 window 关联。
" 删除 buffer 会关闭 window,关闭 window 也会删除 buffer。
silent close
if filereadable(path)
" 打开匹配的文件,execute 类似 js 的 eval 函数
execute 'edit '.root.'/'.path
endif
endfunction
fzf 默认只搜索当前目录。有时我们希望搜索整个项目,最简单的办法是在执行 fzf 前临时将工作目录切换到项目根路径。
function s:findRoot()
" 获取 git 仓库根目录
let result = system('git rev-parse --show-toplevel')
if v:shell_error == 0
return substitute(result, '\n*$', '', 'g')
endif
return "."
endfunction
function! fzf#Open()
keepalt below 9 new
let root = s:findRoot()
if root != '.'
" 执行 lcd 只改变当前 buffer 的工作目录
execute 'lcd '.root
endif
" ...
endfunction
本文基于 terminal 特性实现了一个迷你的 fzf 插件,对有志于学习的 vim 插件开发的朋友有一定的参考价值。