10.1 典型插件的目录规范

第十章 Vim 插件管理与开发 #

10.1 典型插件的目录规范 #

学 VimL 脚本的终极目标是写插件按需扩展 vim 的功能。在开始着手写插件之前,有必 要先了解一下典型的、功能较齐全的插件,应该如何组织目录结构,按 vim 的习惯将不 同类别的功能放在相应的子目录下。

10.1.1 vim 运行时目录 #

插件的目录,可参考 vim 本身安装的运行时目录。所谓运行时目录,顾名思义,就是在 vim 运行时如果要加载 *.vim 脚本,应该到哪里找文件。

有两个相关的环境变量,可用如下命令查看:

:echo $VIM
:echo $VIMRUNTIME

如果从源码安装 vim ,且自定义安装于家目录的话,它们的值大概如下:

$VIM =  ~/share/vim
$VIMRUNTIME = ~/share/vim/vim81

所以 $VIM 指的是 vim 安装目录,而且不同版本的 vim 都将安装在该目录下, $VIMRUNTIME 就是具体当前运行的 vim 版本的安装目录。不过此安装目录不包括 vim 程序本身(那是被安装到 ~/bin 中的),主要是 vim 运行时所需的大量 *.vim 脚 本,相当于“官方插件”。该目录有哪些文件目录,可用如下命令显示:

:!ls -F $VIMRUNTIME

就是 shell 的 ls 命令,选项 -F 只是在子目录后面添加 / ,使得容易区分子目 录与文件。也许直接从 shell 执行 ls 是被 alias 定义的别名,自动加上了一些常用 选项,但从 vim 内用 ! 调用是不读别名的。

$VIMRUNTIME 既是官方目录,显然是不建议用户在其内修改或增删的。如果不是自定义 安装在个人家目录,使用系统默认安装的 vim 的话,普通用户也无权修改。

于是 vim 提供了一个选项叫 &runtimepath (常简称 &rtp),那是类似系统 shell 的环境变量 $PATH,就是一组目录,只不过不用冒号分隔,而是用逗号分隔。可用如下 命令查看 &rtp

:echo &rtp
:echo split(&rtp, ',')

通常,~/.vim/ 目录会在 &rtp 列表中,而且往往是第一个。另外,官方目录 $VIMRUNTIME 也在 &rtp 列表较后一个位置。当 vim 在运行时需要加载脚本时,就 会依次从 &rtp 列表中每个目录(及其子目录)中查找,有时查找第一个就会停止。 所以 $VIMRUNTIME 目录并不特殊,只是 &rtp 中一个优先级并不高的目录。对用户 来说,~/.vim/ 目录才更特殊些,常被称为 vim 的用户目录。

一般建议用户将个人的 vimrc 及其他 vim 脚本放在 ~/.vim/ 目录中。可以用这个 命令:

:echo $MYVIMRC

查看当前你运行的 vim 启动时读取 vimrc 。如果显示是 ~/.vimrc ,则建议将其移 至 ~/.vim/vimrc 或软链接指向它。vim 会尝试读取 vimrc 的几个位置及顺序,也 可用如下命令查看:

:version

然后提一下,如果是 windows 操作系统,没有 ~/.vim/ 目录。但它肯定有 $VIM 安 装目录,然后用户目录就是 $VIM/vimfiles

当了解了用户目录 ~/.vim/ ,就可以参照官方目录 $VIMRUNTIME 来组织管理自己的 vim 个性化配置及扩展脚本(插件)。

10.1.2 全局插件目录 plugin/ #

最简单的插件就是将 *.vim 脚本存到(某个) &rtpplugin/ 子目录下。当 vim 启动时,就会读取(每个) &rtpplugin/ 子目录下的 *.vim 脚本并加载 。因为它们总是被加载,故有时称为全局性插件。

一般 vimer 初学阶段,倾向于完善与丰富自己的配置 vimrc 。当 vimrc 文件越来 越大感觉不便维护时,可将部分功能拆成独立脚本放在 plugin/ 目录下,毕竟这个目 录下的脚本也是能初始加载的,与合在 vimrc 中没有太大区别。可以想象一下,常规 vimrc 配置大约有如下内容:

  • 使用 set 设置的选项
  • 使用 map 系统列定义的快捷键
  • 使用 command 定义的命令
  • 自动事件命令组 augroup
  • 自定义函数
  • 为 gVim 定义的菜单
  • 其他

如果为以上某部分内容进行了重度自定义,譬如快捷键,对每键盘上每个按键都仔细自己 规划了一遍,甚至需要一些简单函数以便支付快捷键功能;那么就可尝试将这部分抽出来 ,另存为名如 ~/.vim/plugin/myremap.vim 的脚本。极端点,可以将 vimrc 中每部 分功能都拆出来扔到 plugin/ 目录。而 vimrc 只需留下这两行:

set nocompatible
filetype plugin indent on

这就是网上曾流传的所谓“最简配置”。第一行设置为不兼容 vi 模式,意即开启 vim 的 扩展功能;第二行是打开文件类型检测。另外我还建议在 vimrc 中定义一个环境变量 $VIMHOME 保存用户目录:

let $VIMHOME = $HOME . '/.vim'
if has('win32') || has ('win64')
    let $VIMHOME = $VIM . '/vimfiles'
endif

这样,在之后的 vimrc 或其他脚本的代码中,引用 $VIMHOME 就更有通用性,尤其 是在需要手动加载(:source)脚本时。

不管是从大 vimrc 拆出脚本,还是从头开始写某个功能脚本放在 plugin/ 目录,都 要注意全局插件的一些特性。

其一是某个 plugin/ 目录下的所有 *.vim 脚本加载顺序不能保证。因此每个脚本要 相应独立完成某个或某类功能,避免引用其他兄弟脚本定义的全局变量。如有这需求,类 似 $VIMHOME 环境变量,还是在 vimrc 中定义吧,保证最开始被执行到。

其次是 plugin/ 的所有脚本还包含其子目录,即更深层次下的 &rtp/plugin/**/*.vim 脚本也会被自动加载。利用这个特性,可以对该目录进一步组织管理,将相关门类功能 的脚本再放入更恰当的子目录名。但也要避免这个特性滥用,太深层次目录搜索比较耗时 ,可能会影响 vim 的启动速度。故一般不建议在 plugin/ 下再建子目录,最多再建一 层。

如果 plugin/ 中脚本太多,影响 vim 启动速度,应该将其移出 plugin/ 目录。可 能的直觉错误是在 plugin/ 下建个 backup/ 子目录,把某些不想用但想备用的脚本 扔进去,这不管用,藏不住的。可以把 *.vim 脚本后缀改为 *.vim.bak ,这就不会 被 vim 启动加载了。更好的建议是建一个与 plugin/ 平级的 plugin.bak/ 子目录 ,因为文件后缀名对 vim 编辑是重要的。

顺便说一下,在 vim 启动时,也有命令行参数可以指示 vim 在启动时跳过加载 plugin/ 的脚本。但一般日常使用时不必考虑这种差别。

10.1.3 类型插件目录 ftplugin/ #

与全局插件相对应的,是局部,具体讲,是与某种文件类型相关的插件,只在打开对应类 型的文件时才生效。

文件类型是 vim 的一个概念,每个编辑的文件,都有个独立的选项值 &filetype ,这 就是该文件的类型。直观地看,文件名后缀代表着其类型。但本质上这不是同一个概念。 vim 只是主要根据文件名后缀来判断一个文件类型,有时还根据文件的部分内容(如前几 行)来判断文件类型,用户还可以用 set filetype= 来手动设置一个类型。一种文件 类型也可以关联好几个后缀名,比如 cpphpp 都是 C++ 文件,文件类型都是 cpp, 同样情况还有 htmhtml 后缀名的文件,都认为是 html 文件类型。

文件类型插件要生效,还得在 vimrc 中添加 filetype plugin on 这行配置,这一 般也是推荐必须配置。然后在打开文件并成功检测到属于某种文件类型时,vim 就会加载 &rtp/ftplugin/{&ft}.vim 脚本。

例如,每当打开 *.cpp*.hpp 文件时,vim 都认为它属于 cpp 文件类型,它 就会加载 ~/.vim/ftplugin/cpp.vim 脚本,以其其他 &rtp 目录下的 ftplugin/cpp.vim 。实际上,vim 搜寻文件类型插件脚本时规则很宽松,还会尝试搜 索 cpp_*.vim 脚本,甚至子目录 cpp/*.vim 下的脚本。这目的是允许在同一个 ftplugin/ 目录中为一种文件类型提供多个插件脚本,它们都会被加载运行。

相比于 plugin/ 目录中的插件脚本只会在 vim 启动时执行一次,ftplugin/ 则可能 在 vim 运行时重复执行多次。每打开相应类型的文件(准确地说是 &filetype 选项值 被设置时触发)就会再次搜索并执行所有 &rtp/ftplugin 中所有匹配类型的脚本。

因此为了避免无意义重复工作,在文件类型插件脚本中,只推荐写那些确实每个文件( buffer)都需要独立设置的工作,如:

  • setlocal 设置局部选项值
  • remap 系列命令加上 <buffer> 参数,只为当前文件定义快捷键
  • command 自定义命令也加上 -buffer 参数
  • let 命令只修改 b: 作用域的变量

此外,还可以在相应的脚本中,通过 VimL 语法来控制脚本的实际执行。比如,参考官方 目录的 cpp 类型插件,使用 :e $VIMRUNTIME/ftplugin/cpp.vim 打开,内容如:

" Only do this when not done yet for this buffer
if exists("b:did_ftplugin")
  finish
endif

" in c.vim
" let b:did_ftplugin = 1

" Behaves just like C
runtime! ftplugin/c.vim ftplugin/c_*.vim ftplugin/c/*.vim

开始几行通过判断 b:did_ftplugin 变量的存在性来决定是否继续加载当前这个脚本, 一般在加载当前脚本时会将该值设为 1 ,这是 vim 官方推荐的文件类型插件的标准头 写法。注意如果每个类型插件都是这样写,那是排他的意义,那就是加载了其中第一个类 型插件的脚本,就不会再加载其他(有这个保护头的)脚本。虽然 vim 的机制会继续搜 索其他匹配的类型插件脚本,但 VimL 语句层面上控制了不会重复加载,而这种控制是用 户可选的方案。

最后一行表示 cpp 类型“继承”加载所有 c 类型的插件脚本,这是符合 C++ 语言与 C 语言特定业务关系的。这样就可以将 C/C++ 相关的都只写在 c.vim 类型插件中, 避免重复代码。事实上,那个 b:did_ftplugin 变量就只在 c.vim 中定义,不能在 cpp.vim 前面先定义,否则执行到 c.vim 是会被跳过。

有时在类型插件脚本中,比如定义局部快捷键时,不可避免要到调用特定函数以便封装具 体实现。这种函数显然也只应该随文件类型插件加载,没用到过该类型就没必要加载,但 是与局部快捷键需要为每个新打开文件定义的情况不同,函数定义最好只定义一次,不必 为每个新文件重复定义。

如果是自己写在 ~/.vim/ftplugin/{&ft}.vim 中,脚本大致结构可以如下:

if exists("b:dotvim_ftplugin")
  finish
endif
let b:dotvim_ftplugin = 1

" 设置局部选项、快捷键等

if exists("s:dotvim_ftplugin")
  finish
endif
let s:dotvim_ftplugin = 1

" 剩余只需加载一次的支持函数、代码

注意这里开头使用 b:dotvim_ftplugin 变量控制,不同于官方习惯的统一的变量 b:did_ftplugin,主要是不想有排他性。也就是说自己只想在 ~/.vim 用户目录下额 外加些设置,执行完后还想加载官方的(或安装在其他目录的第三方的)同类型插件。

同样地,也可以在用户目录中让一种文件类型继承加载另一种文件类型。但是 :runtime 命令太泛了,会搜索所有 &rtp 目录。我们自己明确知道另一个目标文件类型是哪个脚 本,就直接用 :source 会更有效率,例如在 ~/.vim/ftplugin/cpp.vim 中:

source $VIMHOME/ftplugin/c.vim

当然了,按个人实际情况,很可能都不会写纯 C 代码,那就直接维护 cpp.vim 脚本好 了,不必额外有个 c.vim 脚本。另外,也有可能不同的文件类型都有部分共同设置代 码,那也可以提取出来放在独立的 ftplugin/language.vim 脚本中,然后在各个具体 的文件类型插件脚本中都调用这个脚本:

source $VIMHOME/ftplugin/language.vim

这里假设没有哪种文件类型名恰好叫 language ,不过若防意外,也可以故意取个比较 特殊的名字,如 ftplugin/_common_.vim

10.1.4 文件类型其他相关目录 #

与文件类型相关的目录,不止 ftplugin/ 这一个。ftplugin/ 一般是通用目的的 VimL 代码,还有其他几个目录,是 vim 为了实现其他具体功能时所需读取的脚本,虽然 它们也是 *.vim 后缀名的脚本,理论上也可以写任意 VimL 代码,但实践习惯上只为 完成特定功能。

因本书的主旨是讲 VimL 的,所以对这些目录或文件只简单罗列介绍于下:

  • syntax/ 定义文件类型的语法高亮规则,基于正则匹配的;
  • compiler/ 定义相应语言的编译命令及错误格式
  • indent/ 设定缩进规则
  • filetype.vim 检测文件类型的规则,自动事件 filetypedetect
  • indent.vim 设置自动缩进的事件
  • ftplugin.vim 文件类型插件加载机制

如果阅读这些官方脚本的源码,就会发现 ftplugin.vim 等就是利用自动事件实现的。 显然也可以自己在 vimrc 中用 autocmd 实现根据文件后缀名加载特定的相关脚本。 但是由于这个需求如此常见,官方已经帮我们做好了,并且支持了大量你见过的与未见过 的编程语言。

另外,类似全局插件功能的,除了 plugin/ 外,也还有其他几个约定目录。如 colors/ 就是定义配色主题的。这里就不一一介绍了。

10.1.5 自动加载目录 autoload/ #

autoload/ 是放自动加载脚本的目录,在第 5.5 节介绍自动加载函数时就已提及。不 过由于它在现代 vim 中非常重要,故这里再单独列出。自动加载机制是顺应 vim 发展而 提出的,也是 VimL 脚本语言的一大进步,因为 autoload/ 就相当于 perl/python 等 脚本语言存放模块的搜索路径。自动事件(autocmd)是 vim 内置机制,用户无法过多 干涉,autoload/ 自动加载函数是自动事件的一个重要扩充,允许用户在 VimL 语言层 面对函数与脚本的自动加载作灵活的控制。

自动加载函数是名字中含有 # 的函数,如 part1#part2#final() ,其函数名代表着 (某个 &rtp 目录下的) autoload/ 目录下的相对路径,如 autoload/part1/part2.vim 。基于这种对应关系,定义自动加载函数的脚本不必在 vim 启动时事先加载,可以在 vim 运行时直接调用,首次调用时就会从 &rtp 中找到 相应的脚本自动加载。当然这是按 &rtp 顺序找到的第一个自动加载脚本就采用,所以 ~/.vim/autoload 往往有最高的优先级。但最好避免这种潜在的命名冲突与隐藏。

关于自动加载函数的用法,请回顾复习第 5.5 节,这里不重复了。不过全局变量名也可 以采用 # 的标记,如 g:part1#part2#varname ,只在取值时会触发自动加载。

一般在开发较大型插件时,应该将主要实现函数都放在 autoload/ 目录下,并且建议 将插件名再建一层子目录,这样该插件使用的函数名都有相同的前缀,或可称为命名空间 。而在 plugin/ftplugin/ 目录中只写简单的用户界面如快捷键、命令定义。如 此在一定程度上就相当是 vim 接口与 VimL 实现的分离,有利于大型插件的项目管理。

10.1.6 善后目录 after/ #

after/ 是个很有趣的目录,每个 &rtp 目录下的 after/ 子目录又是一个 &rtp 目录,被自动添加到原来常规的 &rtp 列表之末。该 after/ 目录的结构可以与其父 目录或其他 &rtp 目录一样。如果你了解数学上“分形”这个概念,可作此类比理解,就 是“部分与整体拥有相似的结构”。

如果使用 :echo &rtp 命令,很可能在回显消息的末尾看到如下两个目录:

$VIMRUNTIME/after
$VIMHOME/after

因为 after/ 是自动添加到 &rtp 列表末尾,而 vim 在搜索运行时脚本时按顺序搜 索 &rtp ,所以 after/ 目录可以保证尽可能后地被搜索。这机制有什么用途呢?

运行时脚本有两类明显不同的搜索方式。一种是搜索第一个匹配的脚本就停止,如 autoload/ 目录下的脚本,如此排在 &rtp 前列的具体更高的优先级。另一种是始终 搜索所有 &rtp 目录,如 plugin/ftplugin/ ,如此排到 &rtp 末尾的脚本 具有更高的优先级。

如果用户安装了许多插件,每个插件被安置在独立的 &rtp 目录中(详见下一节的插件 管理),那么不同 &rtp 目录下的同名脚本,就有可能冲突。因此在本插件目录下另建 after/plugin/after/ftplugin/ 目录可以大概率保证本插件提供的功能不被覆 盖。

但是,一般的插件,除非有特别理由,不建议添加 after/ 子目录。强行武断地排他, 提升自己的优先级。最好尊重用户的意愿,保留用户目录 $VIMHOME/after/ 让用户自 己决定如何解决冲突,覆盖其他插件的影响。

同时,也不要故意为难 vim ,在 after/ 目录下继续递归地建立 after/ 目录。

10.1.7 文档目录 doc/ #

最后要介绍的文档目录。vim 提供了详尽的在线使用手册,或叫帮助文档。在使用过程中 如有任何疑难杂症,都推荐使用 :help 尝试。如果英文水平有限的,可以下载一份中 文翻译文档。但最好还是习惯英文原文文档,毕竟命令与函数名是没办法翻译成中文的, 熟悉 vim 官方文档使用的术语,有助于更好使用 vim 。

官方文档放在 $VIMRUNTIME/doc 目录下,就是 txt 纯文本文档。不过有特殊的约定 格式,尤其是表示超链接目标与跳转到超链接的表示法,其他语法颜色对于 vim 已是司 空见惯。

用户可以并且建议为自己开发的插件编写文档,放在自己的 $rtp/doc 目录下,然后用 :helptags 生成索引(需要指定 doc/ 目录作为参数),以便支持跳转,这样就纳入 了 vim 的帮助文档系统。用不带参数的 :help 打开帮助系统首页,在末尾部分有一节 名为 LOCAL ADDITIONS 的,就列出了本地帮助文档,也就是除 $VIMRUNTIME 以外的 其他 &rtp 目录下的 doc/*.txt 文档。

最后提一句,善用帮助文档是学习与使用 vim 的不二法门。看过的任何书籍或技术博客 文章,都大概率看过就忘记的,包括你正在看的这一本,它们的价值在于领进门,帮忙建 立个概念,在实际遇到问题时还知道个搜索关键字,或者是 :help 的主题参数。至于 详细使用细节,都以 vim 帮助文档为准。