开发一个 VimL 跳转插件

2019-06-22 ⏳2.7分钟(1.1千字)

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 行的插件,而是为了推广插件的开发思路。希望能给大家带来些许启发。