usr_52

usr_52.txt 适用于 Vim 9.2 版本。 最近更新: 2025年10月 VIM 用户手册 by Bram Moolenaar 译者: Willis 编写更大型插件 插件在开始完成更复杂的功能时,会越来越庞大。本文件说明如何确保仍然可以快速载 入,和如何把大文件分割成小部分。 52.1 导出和导入 52.2 自动加载 52.3 不经导入/导出的自动加载 52.4 可用的其它机制 52.5 在老式脚本中使用 Vim9 脚本 52.6 Vim9 例子: 注释和高亮抽出插件 下一章: usr_90.txt 安装 Vim 前一章: usr_51.txt 创建插件 目录: usr_toc.txt

52.1 导出和导入

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'

52.2 自动加载

将大型脚本拆分为多个文件后,被导入的脚本会在执行导入脚本时立即加载并执行。每次 :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 。

52.4 可用的其它机制

有些人觉得维护多个文件很麻烦,而把所有内容放在一个脚本。为了避免这样引起的启动 时的延迟,有一个机制只定义很小的一部分,而其余部分的载入会延迟到实际使用的时 候。 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 自动命令的模式。要确信其它插件没有定义 匹配此模式的函数。

52.5 在老式脚本中使用 Vim9 脚本 source-vim9-script

有些情况下,在老式脚本里你会想使用 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/ 的包。

下一章: usr_90.txt 安装 Vim 版权: 参见 manual-copyright vim:tw=78:ts=8:noet:ft=help:norl: