第十章 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
脚本存到(某个) &rtp
的 plugin/
子目录下。当 vim 启动时,就会读取(每个) &rtp
的 plugin/
子目录下的 *.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=
来手动设置一个类型。一种文件 类型也可以关联好几个后缀名,比如 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
系列命令加上<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 帮助文档为准。