开发一个简单的 Vim 搜索插件

2021-11-07 ⏳3.9分钟(1.6千字)

最近准备写一篇面向 Go 开发新手的 vim 入门文章,于是开始整理自己的 vim 配置和插件体系。在整理文件相关的插件时,我发现自己用了 ack 这个插件。ack 本身没有文件搜索能力,它是使用 ack/ag 这一类的工具来搜索文章。插件本身只起到调用命令和显示结果的作用。既然比较简单,为什么不自己实现一个呢?于是便用了今天文章的主角 ag.vim。这是一个仅有 15 行的插件,却实现了 ack 插件的基本功能。我是怎么做到的呢?一起来看看吧。

Unix 环境下最常用的文件操作工具是 grep。但是 grep 速度比较拉胯,于是就有了 ag。ag 的功能跟 grep 类似,但速度更快。不过 ag 的包名叫 the_silver_searcher,确实有点奇怪。也就是说,我们在使用 homebrew 或者 apt-get 安装的时候,要输入 the_silver_searcher 而不是 ag。但使用的时候要输入 ag。

感谢网友 TZX 留言提示:

ag 是 silver 的化学元素符号。

有人觉得 ag 还是不够快,于是又开发了rg,它的全称是 ripgrep。但无论如何,ag 和 rg 都支持所谓的vimgrep 输出格式:

# ag 'Ag\b' --vimgrep
# rg 'Ag\b' --vimgrep
pack/vendor/start/ag/README.md:10:10:" search Ag

这是 vimgrep 规定输出格式。每一行对应一条搜索结果,每条结果分成四部分,以:分隔。第一部分是文件路径,第二和第三部分分别是搜索结果的行号和列号,最后一部分是搜索结果。

Vim 本身又提供 QuickFix 功能,可以显示 vimgrep 格式的搜索内容,并且支持跳转到对应的文件位置。相关功能可以参考我之前的文章开发一个 Todo 插件

显示 QuickFix 窗口很简单,执行:copen就好了。问题的关键是如何向里面追加内容。这就得用到setqflist()函数了。大家可以通过:h setqflist()查看它的详细参数和功能。对于我们的使用场景,第一个参数传空列表[]。第二个参数表示具体的动作(action),我们需要用到a表示追加,f表示清空。第三个参数则是要显示的搜索结果了。这是一个字典,字段有很多,但我们需要用到的只有三个:

说了半天比较抽象,我们不妨试验一下。以上面的的输出结果为例,我们可以这样调用setqflist()

call setqflist([], 'a', { 'title': 'ag Ag\b', 'lines': ['pack/vendor/start/ag/README.md:10:10:" search Ag'], 'efm':'%f:%l:%c:%m'})

执行之后再运行:copen就能看到一行结果。将光标移动到上面按回车 vim 就会打开对应的文件并跳转到搜索结果的位置(精确到行和列)。

以上就是界面相关的部分。下面我们来说如何执行 ag 命令。

Vim 和 NeoVim 支持通过system()执行系统命令,并返回输出结果。但这个命令会阻塞编辑器界面。也就是说,如果我们用它来搜索比较大的项目,那么在搜索完成之前,Vim 会卡住,什么也做不了。这显示不科学。我们需要异步的方式来执行搜索命令。

Vim 在很长一段时间内都不支持异步功能,而且也不想支持。这也是 NeoVim 诞生的一个重要原因。NeoVim 最早引进了 jobstart() 函数在后台异步执行系统命令。后来 Vim 的维护者迫于 NeoVim 的压力也添加了 job_start() 函数。两者功能类似,但互不兼容。今天我就以 NeoVim 的版本为例进行讲解。如果你是 Vim 用户也不要怕,可以结合 Vim 的文档:h job_start和本文内容实现相同的效果。

如果我们想异步执行搜索,可以这样:

call jobstart('ag --vimgrep Ag\b')

但问题来了,我们怎么拿到 ag 的输出结果呢?这就需要通过 jobstart() 的第二个参数设置输出回调。

call jobstart(s:cmd, { 'on_stdout':function('s:show'), 'stdin': "null" })

第二个参数也是一个字典,key 有很多,但我只需要用到 on_stdoutstdin。将 stdin 设置成 null 是为了兼容 rg 命令。如果只用 ag 则不需要。用 on_stdout 指定一个函数,这里的function('s:show')是 vim script 的语法,用于引用某个函数。NeoVim 在后台运行 ag 的时候会监听 stdout 输出,收到输出内容后就会调用 on_stdout 指定的函数。

函数s:show的参数结构如下:

function! s:show(id, lines, name) 
if a:lines == [""] | return | end
call setqflist([], 'a', { 'title': 'ag', 'lines': lines, 'efm':'%f:%l:%c:%m'})
end

第二个参数是一个列表,保存 ag 的输出结果。ag 会一边搜索一边输出,所以s:show() 会被多次调用。ag 搜索结束后,NeoVim 会给调用s:show()并给 lines 参数设置为[""],表示搜索已经完成。

所以我们只需要在s:show()把搜索结果加入到 QuicFix 列表中就可以了。

最后我们只要调用:copen就可以看到搜索的结果列表。完整代码已经在 GitHub 开源,欢迎试用。十五行的 ag.vim 对决三百多行的 ack 插件,够用了。

以上就是本文的全部内容。写到这里不禁想起一个朋友的灵魂之问——用 vim 到底图什么呢?我想,图的就是这种扩展能力吧。Vim 做为一个纯字符编辑器,只提供了基础功能。用户可以根据自己的需要编写插件,跟任意工具结合使用。我想这才是 vim 的魅力所在。