# 第十章 Vim 插件管理与开发 ## 10.1 典型插件的目录规范 学 VimL 脚本的终极目标是写插件按需扩展 vim 的功能。在开始着手写插件之前,有必 要先了解一下典型的、功能较齐全的插件,应该如何组织目录结构,按 vim 的习惯将不 同类别的功能放在相应的子目录下。 ### 10.1.1 vim 运行时目录 插件的目录,可参考 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` 脚 本,相当于“官方插件”。该目录有哪些文件目录,可用如下命令显示: ```vim :!ls -F $VIMRUNTIME ``` 就是 shell 的 `ls` 命令,选项 `-F` 只是在子目录后面添加 `/` ,使得容易区分子目 录与文件。也许直接从 shell 执行 `ls` 是被 alias 定义的别名,自动加上了一些常用 选项,但从 vim 内用 `!` 调用是不读别名的。 `$VIMRUNTIME` 既是官方目录,显然是不建议用户在其内修改或增删的。如果不是自定义 安装在个人家目录,使用系统默认安装的 vim 的话,普通用户也无权修改。 于是 vim 提供了一个选项叫 `&runtimepath` (常简称 `&rtp`),那是类似系统 shell 的环境变量 `$PATH`,就是一组目录,只不过不用冒号分隔,而是用逗号分隔。可用如下 命令查看 `&rtp` : ```vim :echo &rtp :echo split(&rtp, ',') ``` 通常,`~/.vim/` 目录会在 `&rtp` 列表中,而且往往是第一个。另外,官方目录 `$VIMRUNTIME` 也在 `&rtp` 列表较后一个位置。当 vim 在运行时需要加载脚本时,就 会依次从 `&rtp` 列表中每个目录(及其子目录)中查找,有时查找第一个就会停止。 所以 `$VIMRUNTIME` 目录并不特殊,只是 `&rtp` 中一个优先级并不高的目录。对用户 来说,`~/.vim/` 目录才更特殊些,常被称为 vim 的用户目录。 一般建议用户将个人的 `vimrc` 及其他 vim 脚本放在 `~/.vim/` 目录中。可以用这个 命令: ```vim :echo $MYVIMRC ``` 查看当前你运行的 vim 启动时读取 `vimrc` 。如果显示是 `~/.vimrc` ,则建议将其移 至 `~/.vim/vimrc` 或软链接指向它。vim 会尝试读取 `vimrc` 的几个位置及顺序,也 可用如下命令查看: ```vim :version ``` 然后提一下,如果是 windows 操作系统,没有 `~/.vim/` 目录。但它肯定有 `$VIM` 安 装目录,然后用户目录就是 `$VIM/vimfiles` 。 当了解了用户目录 `~/.vim/` ,就可以参照官方目录 `$VIMRUNTIME` 来组织管理自己的 vim 个性化配置及扩展脚本(插件)。 ### 10.1.2 全局插件目录 plugin/ 最简单的插件就是将 `*.vim` 脚本存到(某个) `&rtp` 的 `plugin/` 子目录下。当 vim 启动时,就会读取(每个) `&rtp` 的 `plugin/` 子目录下的 `*.vim` 脚本并加载 。因为它们总是被加载,故有时称为全局性插件。 一般 vimer 初学阶段,倾向于完善与丰富自己的配置 `vimrc` 。当 `vimrc` 文件越来 越大感觉不便维护时,可将部分功能拆成独立脚本放在 `plugin/` 目录下,毕竟这个目 录下的脚本也是能初始加载的,与合在 `vimrc` 中没有太大区别。可以想象一下,常规 `vimrc` 配置大约有如下内容: * 使用 `set` 设置的选项 * 使用 `map` 系统列定义的快捷键 * 使用 `command` 定义的命令 * 自动事件命令组 `augroup` * 自定义函数 * 为 gVim 定义的菜单 * 其他 如果为以上某部分内容进行了重度自定义,譬如快捷键,对键盘上每个按键都仔细自己 规划了一遍,甚至需要一些简单函数以便支持快捷键功能;那么就可尝试将这部分抽出来 ,另存为名如 `~/.vim/plugin/myremap.vim` 的脚本。极端点,可以将 `vimrc` 中每部 分功能都拆出来扔到 `plugin/` 目录。而 `vimrc` 只需留下这两行: ```vim set nocompatible filetype plugin indent on ``` 这就是网上曾流传的所谓“最简配置”。第一行设置为不兼容 vi 模式,意即开启 vim 的 扩展功能;第二行是打开文件类型检测。另外我还建议在 vimrc 中定义一个环境变量 `$VIMHOME` 保存用户目录: ```vim 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=` 来手动设置一个类型。一种文件 类型也可以关联好几个后缀名,比如 `cpp`、`hpp` 都是 C++ 文件,文件类型都是 `cpp`, 同样情况还有 `htm` 与 `html` 后缀名的文件,都认为是 `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` 系列命令加上 `` 参数,只为当前文件定义快捷键 * `command` 自定义命令也加上 `-buffer` 参数 * `let` 命令只修改 `b:` 作用域的变量 此外,还可以在相应的脚本中,通过 VimL 语法来控制脚本的实际执行。比如,参考官方 目录的 `cpp` 类型插件,使用 `:e $VIMRUNTIME/ftplugin/cpp.vim` 打开,内容如: ```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` 中,脚本大致结构可以如下: ```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` 中: ```vim source $VIMHOME/ftplugin/c.vim ``` 当然了,按个人实际情况,很可能都不会写纯 C 代码,那就直接维护 `cpp.vim` 脚本好 了,不必额外有个 `c.vim` 脚本。另外,也有可能不同的文件类型都有部分共同设置代 码,那也可以提取出来放在独立的 `ftplugin/language.vim` 脚本中,然后在各个具体 的文件类型插件脚本中都调用这个脚本: ```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 帮助文档为准。