You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
19 KiB

2 years ago
# 第十章 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` 系列命令加上 `<buffer>` 参数,只为当前文件定义快捷键
* `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 帮助文档为准。