开发一个 VimL 跳转插件
涛叔vim 的插件大都使用 viml 开发。vim 插件一般会有一个 autoload 目录,公有的函数需要在这里定义。viml 强制要求函数名要跟所在源文件的路径对应起来。
例如,我们创建了一个 autoload/foo/bar.vim
的文件,那么 bar.vim
中定义的公有函数的名字必需以 foo#bar# 开头。反过来,如果我们想查看函数 foo#bar#baz()
的代码,我们就得找到某个 autoload/foo/bar.vim
文件。为什么这里要用某个这种描述呢?因为 vim 有个叫 runtimepath 的配置,里面存有一个逗号分割的文件夹列表。vim 在查找函数的时候会遍历 runtimepath
指定的文件夹,并搜索对应 autoload 文件夹内的 .vim 文件。假设把 runtimepath
设成 /a,/b
,那么 vim 在查找 foo#bar#baz()
函数时会依次检查 /a/autoload/foo/bar.vim
和 /b/autoload/foo/bar.vim
,查到为止。
运行的时候 vim 会自动查找,可看我们要看代码话就只能自己手动查找了。这是一个相当无聊的过程。显然,我们需要一个插件。别怕,接下来我们就用 30 行代码实现一个 viml 跳转插件,从此告别无聊的手工查找操作。
在开始之前,我们先了解一些预备知识。
首先要解决的问题是获取光标所在位置的函数。
vim 为我们提供了 expand()
函数。如果你在 vim 执行 :echo expand('<cword>')
,vim 就会输出光标所在的函数名。无论你把光标移动到 foo#bar#baz()
的哪个字母上,执行 :echo expand('<cword>')
都会输出 foo#bar#baz()
。
vim 还可以定义形如 s:foo()
这样的局部函数,这种函数只能在当前文件调用。如果你把光标移动到 s:foo()
的 foo
上,执行 :echo expand('<cword>')
,vim 只会输出 foo
。这是为什么呢?简单 google 之后(关键词 vim expand cword)发现 vim 是根据 iskeyword
配置确定 <cword>
边界的。如果我们想输出 s:foo
,则需要额外设置 set iskeyword+=:
。expand()
函数还有很多其他功能,大家可以使用 :h expand()
查阅。
再一个要解决的问题就是搜索了。我们知道,在 vim 中按 /
键开始搜索,输入内容后按回车,vim 回自动将光标移动到第一个匹配的位置。如果想在 viml 中执行搜索,则需要调用 search()
函数。第一个参数是搜索匹配条件,就是按 /
后输入的内容。第二个参数是控制位,可以控制搜索的行为,例如,w 表示从光标所在位置向下搜索,如果没有搜到,则回到文件开头继续搜索。其他信息请参考 :h search()
。
最后要解决 vim 如何打开目标文件的问题。我们要手动打开文件的话,可以执行 :e path/to/file
。但要在 viml 执行同样的操作则需要写成 execute 'e path/to/file'
。
了解了基础知识,我们就能看懂这 30 行 viml 代码了。
" 在 ~/.vim/autoload/vim.vim 声名函数
function! vim#Jump()
" 读取光标所在函数名并存储到变量 cword
setlocal iskeyword+=:
let cword = expand('<cword>')
setlocal iskeyword-=:
let flags = 'wes' " 设置默认搜索行为
" 如果函数名包含 #
if stridx(cword, '#') >= 0 && cword[len(cword)-1] != '#'
" 以 # 分割函数名,得到一个数组
let parts = split(cword, '#')
" 最后一个元素是函数名,去掉
call remove(parts, -1)
" 拼接成源文件相对路径
" foo#bar#baz() => foo/bar.vim
let fpath = join(parts, '/').'.vim'
" 读取全局配置
let vim_jump_paths = get(g:, 'vim_jump_paths', '')
" 拆分搜索路径
" 如果是正在使用插件,已经包含在 &runtimepath 配置里了
" 注意,使用 & 引用配置
" 如果只是看别人的代码,则需要将插件根目录写入 g:vim_jump_paths 配置
let paths = split(vim_jump_paths, ',') + split(&runtimepath, ',')
" 遍历搜索路径
for root in paths
" 拼接源代码真实路径
let p = root . '/autoload/' . fpath
" 文件不存在则跳过
if !filereadable(p)
continue
endif
" 在当前窗口打开文件
execute 'e ' . p
let flags = 'we'
endfor
endif
" 设置搜索模式
" 以 \v 开关表示使用标准正则匹配
let pattern = "\\vfu(nction)?!? ".cword
" 跳到对应的的函数定义
call search(pattern, flags)
endfunction
最后,大家可以在自己的 vimrc 里面映射一个快捷键:
autocmd FileType vim nnoremap <c-]> :call vim#Jump()<cr>
这次给大家介绍了一个小插件从构思到开发的过程。30 行的插件肯定没法处理所有情形,但对于跳转到 viml 函数这个需求来说是足够了。写这篇文章不是为了推广这个 30 行的插件,而是为了推广插件的开发思路。希望能给大家带来些许启发。