Vim9 脚本设计使大型 Vim 脚本的编写更为容易。它看来更像其它脚本语言,尤其是
Typescript。另外,函数被编译为指令以快速执行。这使 Vim9 脚本快很多,多达百倍。
这里的基本想法是脚本文件包含只用于脚本文件内部的私有项目,和可被脚本文件之外使
用的导出项目。然后,这些导出项目可用于导入它们的脚本里。这样什么在哪里定义就会
很清晰。
让我们从一个例子开始,有一个导出函数和有一个私有函数的脚本:
vim9script
export def GetMessage(count: string): string
var nr = str2nr(count)
var result = $'To {nr} we say '
result ..= GetReply(nr)
return result
enddef
def GetReply(nr: number): string
if nr == 42
return 'yes'
elseif nr == 22
return 'maybe'
else
return 'no'
endif
enddef
vim9script 命令是必须的, export 只能用于 Vim9 脚本里。
`export def GetMessage(...` 行以 export 开始,意味着此函数可以被其它脚本调
用。`def GetPart(...` 行不以 export 开头,意味着这是局部于脚本的函数,只能在
此脚本文件中使用。
现在说说导入此脚本的脚本。此例中使用如下布局,这适用于放在 "pack" 目录之下的插
件:
.../plugin/theplugin.vim
.../lib/getmessage.vim
假定 "..." 目录已添加进 'runtimepath',Vim 会寻找 "plugin" 目录里的插件并载入
"theplugin.vim"。Vim 并不识别 "lib" 目录,你可以在那里放任何脚本。
上面导出 GetMessage() 的脚本放在 lib/getmessage.vim 里。GetMessage() 函数在
plugin/theplugin.vim 里调用:
vim9script
import "../lib/getmessage.vim"
command -nargs=1 ShowMessage echomsg getmessage.GetMessage(<f-args>)
import 命令使用了相对路径,因为它以 "../" 开始,也就是上一层目录。其它类型的
路径可见 :import 命令。
现在试试插件提供的命令吧:
ShowMessage 1
To 1 we say no
ShowMessage 22
To 22 we say maybe
注意 GetMessage() 使用了导入脚本名字 "getmessage" 作为前缀。这样,对每个导入函
数,你会知道它是从哪里导入的。如果导入多个脚本,每个脚本都可以定义自己的
GetMessage() 函数:
vim9script
import "../lib/getmessage.vim"
import "../lib/getother.vim"
command -nargs=1 ShowMessage echomsg getmessage.GetMessage(<f-args>)
command -nargs=1 ShowOther echomsg getother.GetMessage(<f-args>)
如果导入脚本名字太长或者需要在很多地方使用,可以通过 "as" 参数来缩短名字:
import "../lib/getmessage.vim" as msg
command -nargs=1 ShowMessage echomsg msg.GetMessage(<f-args>)
重 载
记住一件事: 导入的 "lib/getmessage.vim" 脚本只会载入一次。第二次载入时会跳过,
因为里面的项目都已经创建了。不管是在其它脚本还是同一脚本里再次执行导入命令,都
是如此。
对插件的使用这是高效的,但在插件开发期间,这意味着导入 "lib/getmessage.vim" 之
后对它的修改不会生效。为此,只能退出 Vim 后重启。(理据: 脚本定义的项目可能在编
译过的函数里使用,再次载入脚本可能会破坏这些函数)。
使 用 全 局 值
有时需要使用全局变量或函数,以便在任何地方都能使用。一个全局变量的好例子是控制
插件行为的首选项。为了避免其它脚本使用的重名,使用别人不太可能使用的前缀。例
如,如果插件是 "mytags" 的话:
g:mytags_location = '$HOME/project'
g:mytags_style = 'fast'
将大型脚本拆分为多个文件后,被导入的脚本会在执行导入脚本时立即加载并执行。每次
:import 都会加载目标脚本,用于读取其中定义的成员。但如果被导入脚本体积较大,
启动耗时会明显增加;好处是错误可在导入阶段提前捕获。
想要缩短启动时间,可延迟加载被导入脚本,直到真正使用时才加载,这是通过
autoload 关键字实现的。沿用上面示例,仅需修改 plugin/theplugin.vim 一处:
import autoload "../lib/getmessage.vim"
脚本其余部分无需改动。但此时不会做类型检查, ../lib/getmessage.vim 也不会在导
入阶段加载;Vim 仅校验文件是否存在且可读。就连 GetMessage() 函数是否存在,也只
会在调用时才检查。因此, getmessage.vim 里的错误只会在运行期才暴露。
需要自行权衡脚本的需求: 优先快速启动,还是尽早捕获错误。
一个实用策略是开发调试阶段不写 autoload ,可以立刻看到报错;插件发布前加上
autoload 提升启动速度。
自 动 加 载 目 录
另一种延迟加载方式是在 `import autoload` 导入脚本时,不指明相对或绝对路径,示
例:
import autoload "monthlib.vim"
Vim 会在 'runtimepath' 下的 autoload 目录里检索脚本,也就是说,脚本必须放在
"autoload" 目录内。Unix 系统常见路径为 ~/.vim/autoload 。同时也会检索
'packpath' 下 "start" 目录。
以该方式导入脚本时,其导出符号会通过全局自动加载命名空间访问,而不是局限于导入
脚本的局部作用域。
因为 Vim 会遍历 'runtimepath' 内全部 "autoload" 目录,脚本名称务必唯一。插件管
理器通常会向 'runtimepath' 添加多个目录,每个目录都可带有 "autoload" 子目录。
两种 `import autoload` 用法总结:
- `import autoload "foo.vim"`
Vim 在 'runtimepath' 检索脚本。这类脚本必须放在 "autoload" 目录下,其导出符
号通过全局自动加载命名空间访问。
- `import autoload "/some/path/foo.vim"`
解析后的路径位于 "autoload" 目录下时,导出符号同样归入全局自动加载命名空间;
否则符号仅属于导入脚本的局部作用域。
通过全局自动加载命名空间访问符号的主要优势是,其他脚本可方便导入,符号开箱即
用。但脚本重载时存在一些缺陷,详见 vim9-reload 。
符号属于脚本局部作用域时,跨脚本共享符号会更加繁琐,没有通用方案,实现方式取决
于插件目录结构。
最后,不带 autoload 且不指定路径的脚本非自动加载导入写法,示例:
import "monthlib.vim"
Vim 会在 'runtimepath' 的导入目录查找 "monthlib.vim"。
这种形式下,添加或移除 autoload 关键字会改变 Vim 查找脚本的位置。而使用相对
路径或绝对路径时,查找位置不受影响。
52.3 不经导入/导出的自动载入
write-library-script
在导入/导出机制引入之前,已经有另一套有用的机制,有些用户会觉得它比较简单些。
基本想法是调用一个有特殊名字的函数,然后这个函数在某自动载入脚本中。我们把这种
脚本称为库脚本。
此自动载入机制是基于有 "#" 字符的函数名的:
mylib#myfunction(arg)
Vim 会识别有内嵌 "#" 字符的函数名,如果该函数还没有定义,查找 'runtimepath'
里的 "autoload/mylib.vim"。该脚本必须定义 "mylib#myfunction()" 函数。显然
"mylib" 名字是 "#" 之前的部分,加上 ".vim" 后就被用作脚本名。
在 mylib.vim 脚本里可以放上许多其它函数,你可以自由组织库脚本的函数。但必须使
函数名 "#" 前面的部分匹配脚本名。否则 Vim 无法知道载入哪个脚本。这是和导入/导
出机制不同之处。
如果你真的热情高涨写了很多库脚本,现在可能想要用子目录吧。例如:
netlib#ftp#read('somefile')
这里脚本名取自函数名最后 "#" 之前的部分。中间的 "#" 用斜杠替代,最后加上
".vim"。这样得到的名字就是 "netlib/ftp.vim"。Unix 上,这里使用的库脚本可以是:
~/.vim/autoload/netlib/ftp.vim
其中的函数应该如此定义:
def netlib#ftp#read(fname: string)
" 用 ftp 读入文件 fname
enddef
注意定义所用的函数名必须和调用的函数名完全相同。最后一个 '#' 之前的部分必须准
确匹配子目录和脚本名。
同样的机制可以用来定义变量:
var weekdays = dutch#weekdays
会载入脚本 "autoload/dutch.vim",它应该包含这样的内容:
var dutch#weekdays = ['zondag', 'maandag', 'dinsdag', 'woensdag',
\ 'donderdag', 'vrijdag', 'zaterdag']
进一步的阅读可见: autoload 。
有些人觉得维护多个文件很麻烦,而把所有内容放在一个脚本。为了避免这样引起的启动
时的延迟,有一个机制只定义很小的一部分,而其余部分的载入会延迟到实际使用的时
候。 write-plugin-quickload
基本的方法是调用插件两次。第一次定义用户命令和映射,提供需要的功能。第二次定义
实现这些功能的函数。
听起来很吓人,快速载入意味着载入两次!我们的意思是,第一次载入很快,把脚本的大
部分内容延迟到第二次才载入,只有实际使用这些功能时才会这么做。当然,如果你总是
用这些功能,实际上更慢了!
这里使用 FuncUndefined 自动命令。和上述的 autoload 功能不同。
下例演示如何这是如何完成的:
" 演示快速载入的 Vim 全局插件
" Last Change: 2005 Feb 25
" Maintainer: Bram Moolenaar <Bram@vim.org>
" License: This file is placed in the public domain.
if !exists("s:did_load")
command -nargs=* BNRead call BufNetRead(<f-args>)
map <F19> :call BufNetWrite('something')<CR>
let s:did_load = 1
exe 'au FuncUndefined BufNet* source ' .. expand('<sfile>')
finish
endif
function BufNetRead(...)
echo 'BufNetRead(' .. string(a:000) .. ')'
" 读入功能在此
endfunction
function BufNetWrite(...)
echo 'BufNetWrite(' .. string(a:000) .. ')'
" 写回功能在此
endfunction
第一次载入脚本时,没有设置 "s:did_load"。这时执行 "if" 和 "endif" 之间的命令。
它以 :finish 命令结束,这样脚本的其余部分不再执行。
第二次载入脚本时,"s:did_load" 已经存在,这时执行 "endif" 之后的命令。这里定义
(可能很长的) BufNetRead() 和 BufNetWrite() 函数。
如果把该脚本放到你的 plugin 目录,Vim 启动时会执行它。下面列出发生事件的序列:
1. 启动期间执行脚本时,定义 "BNRead" 命令并映射 <F19> 键。定义 FuncUndefined
自动命令。":finish" 命令使脚本提前终止。
2. 用户输入 BNRead 命令或者按了 <F19> 键。BufNetRead() 或 BufNetWrite() 函数会
被调用。
3. Vim 找不到这些函数并因此触发了 FuncUndefined 自动命令事件。因为模式
"BufNet*" 匹配要调用的函数,执行命令 "source fname",其中 "fname" 被赋予脚
本的名字,不管它实际在何处都没问题。这是因为该名字来自 "<sfile>" 扩展的结果
(见 expand() )。
4. 再次执行脚本。"s:did_load" 变量已经存在,此时定义函数。
注意后来载入的函数匹配 FuncUndefined 自动命令的模式。要确信其它插件没有定义
匹配此模式的函数。
有些情况下,在老式脚本里你会想使用 Vim9 脚本里的项目。例如在 .vimrc 里初始化插
件。最好的方法是用 :import 。例如:
import 'myNicePlugin.vim'
call myNicePlugin.NiceInit('today')
这会找到 Vim9 脚本文件里的 "NiceInit" 导出函数,并使之成为局部于脚本的项目
"myNicePlugin.NiceInit"。即使不给出 "s:", :import 也总使用脚本命名空间,如果
"myNicePlugin.vim" 已经执行过,不会再次执行。
除了避免把任何项目放进全局命名空间 (因为命名冲突会导致意料之外的错误) 以外,这
也意味着无论其中的项目被导入多少次,一份脚本只执行一次。
有些情况下,例如为了测试用途,可能就想要直接执行 Vim9 脚本。这没问题,但你只能
得到其中的全局项目。该 Vim9 脚本要确保这些全局项目使用独一无二的名字。例如:
source ~/.vim/extra/myNicePlugin.vim
call g:NicePluginTest()
52.6 Vim9 例子: 注释和高亮抽出插件
注 释 包
Vim 带有一个 comment 插件,用 Vim9 脚本编写。 comment-install
看一下在 $VIMRUNTIME/pack/dist/opt/comment/ 的包的内容吧。
高 亮 抽 出 插 件
Vim 带有一个 highlight-yank 插件,用 Vim9 脚本编写 hlyank-install 。这里是一
个简化实现:
vim9script
def HighlightedYank(hlgroup = 'IncSearch', duration = 300, in_visual = true)
if v:event.operator ==? 'y'
if !in_visual && visualmode() != null_string
visualmode(1)
return
endif
var [beg, end] = [getpos("'["), getpos("']")]
var type = v:event.regtype ?? 'v'
var pos = getregionpos(beg, end, {type: type, exclusive: false})
var m = matchaddpos(hlgroup, pos->mapnew((_, v) => {
var col_beg = v[0][2] + v[0][3]
var col_end = v[1][2] + v[1][3] + 1
return [v[0][1], col_beg, col_end - col_beg]
}))
var winid = win_getid()
timer_start(duration, (_) => m->matchdelete(winid))
endif
enddef
autocmd TextYankPost * HighlightedYank()
完整的示例可以看看位于 $VIMRUNTIME/pack/dist/opt/hlyank/ 的包。