1. 什么是 Vim9 脚本? vim9-script
还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变
Vim 脚本随着时间推移日渐庞大,同时需要保持后向兼容。这意味着过去所做的错误决定
常常不能再改变。执行颇慢,每行在每次执行时都要先解析。
Vim9 脚本的主要目标是大幅提高性能。可以期待执行速度有成十上百倍的提高。一个次
要目标是避免 Vim 特定的构造,而尽量和包括 JavaScript、TypeScript 和 Java 这样
的主流编程语言接近。
只有不 100% 后向兼容才能达到所需的性能提高。例如,在函数中参数不通过 "a:" 字典
获取,因为字典的建立增加不少开销。其它一些差别,比如错误处理的方式,则更细微一
些。
Vim9 脚本语法和语义用于:
- 使用 :def 命令定义的函数
- 首个命令是 vim9script 的脚本文件
Vim9 脚本文件里如果用了 :function ,会用老式的语法。不过不鼓励如此。
Vim9 脚本和老式的 Vim 脚本可以混合。没有必要重写旧脚本,它们会继续工作。
2. 和老式 Vim 脚本的差异 vim9-differences
还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变
Vim9 函数
:def 不像 :function 那样有额外的参数: "range"、"abort"、"dict" 或
"closure"。 :def 函数总会在错误时中止,不接受范围也不能是 "dict" 函数。
在函数体里:
- 参数可用不带 "a:" 的名字访问。
- 没有 "a:" 字典或 "a:000" 列表。通过给定名字和列表类型来定义可变参数:
def MyFunc(...itemlist: list<type>)
for item in itemlist
...
用 :let 和 :const 声明变量
局部变量需用 :let 定义。局部常量需用 :const 定义。我们把两者都称为
"变量"。
变量可以局部于脚本、函数或代码块:
vim9script
let script_var = 123
def SomeFunc()
let func_var = script_var
if cond
let block_var = func_var
...
变量只在定义所在的块和嵌套块中可见。块定义结束后,变量不再可访问:
if cond
let inner = 5
else
let inner = 0
endif
echo inner " 报错!
变量必须在使用之前进行声明:
let inner: number
if cond
inner = 5
else
inner = 0
endif
echo inner
要有意地运用之后不可用的变量,可用代码块:
{
let temp = 'temp'
...
}
echo temp " 报错!
不能用 :let 给已存在的变量赋值,因为它意味着变量声明。全局变量是例外: 带或不
带 :let 的使用都可以,因为没有它们应在哪里声明的相关规则。
变量不能隐藏之前定义的变量。
变量可以隐藏 Ex 命令,如有需要请给变量换名。
因为 "&opt = value" 现在用来给选项 "opt" 赋值,所以不再能用 ":&" 来重复
:substitute 命令了。
省略 :call 和 :eval
可直接调用函数而无需 :call :
writefile(lines, 'file')
:call 还可用,但不鼓励。
只要以标识符开始或不可能是 Ex 命令,方法可直接调用而无需 eval 。 不能
用于字
符串常数:
myList->add(123) " 正确
g:myList->add(123) " 正确
[1, 2, 3]->Process() " 正确
#{a: 1, b: 2}->Process() " 正确
{'a': 1, 'b': 2}->Process() " 正确
"foobar"->Process() " _不_正确
("foobar")->Process() " 正确
'foobar'->Process() " _不_正确
('foobar')->Process() " 正确
如果函数名和 Ex 命令有二义性,用 ":" 来明确你想用的是 Ex 命令。例如,既有
:substitute 命令,又有 substitute() 函数。如果一行以 substitute(
开始,
会假定使用函数,要使用命令版本,加上冒号前缀:
:substitute(pattern (replacement (
注意
尽管变量在使用前必须先定义,函数在定义前就可以调用。为了函数间能相互依
赖,这是有必要的。但因此效率会稍稍低些,因为函数需要依名查找。且函数名的拼写错
误只有在调用真正执行时才能发现。
没有花括号扩展
不能使用 curly-braces-names 。
没有 :append、:change 或 :insert
这些命令很容易会和局部变量名混淆。
比较符
字符串的比较符不考虑 'ignorecase' 选项。
空白
Vim9 脚本强制空白的正确使用。以下不再允许:
let var=234 " 出错!
let var= 234 " 出错!
let var =234 " 出错!
"=" 前后必须有空白:
let var = 234 " OK
多数操作符需要用空白包围。
以下位置不允许空白:
- 函数名和 "(" 之间:
call Func (arg) " 出错!
call Func
\ (arg) " 出错!
call Func(arg) " 正确
call Func(
\ arg) " 正确
call Func(
\ arg " 正确
\ )
条件和表达式
多数情况下,条件和表达式就像 JavaScript 里那样的用法。但如果 JavaScript 和多
数人的期待不一致,会作出调整。特别是,空列表被当作假值。
任何类型的变量都可用作条件,不会报错,即使用列表或作业也可以。这很像
JavaScript,但有少许例外。
类型 何时为 TRUE
bool v:true
number 非零
float 非零
string 非空
blob 非空
list 非空 (和 JavaScript 不同)
dictionary 非空 (和 JavaScript 不同)
funcref 非 NULL 时
partial 非 NULL 时
special v:true
job 非 NULL 时
channel 非 NULL 时
class 非 NULL 时
object 非 NULL 时 (TODO: 应为 isTrue() 返回 v:true 时)
布尔操作符 "||" 和 "&&" 不改变值:
8 || 2 == 8
0 || 2 == 2
0 || '' == ''
8 && 2 == 2
0 && 2 == 0
[] && 2 == []
..
用作字符串连接时,参数总是转化为字符串。
'hello ' .. 123 == 'hello 123'
'hello ' .. v:true == 'hello true'
Vim9 脚本里,可用 "true" 代表 v:true 而用 "false" 代表 v:false。
3. 新风格函数 fast-functions
还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变
:def
:def[!] {name}
([arguments]
)[: {return-type}
定义名为 {name}
的新函数。在下面的行给出函数体,直到配
对的 :enddef 为止。
{return-type}
如果省略,函数不期待返回任何值。
{arguments}
是零或多个参数声明的序列。有以下三种形式:
{name}
: {type}
{name}
= {value}
{name}
: {type}
= {value}
第一种形式是必选参数,调用者必须提供。
第二三种形式是可选参数。如果调用者省略参数,使用
{value}
值。
注意
: 在 :def 里可以嵌套另一个 :def ,但为了后向兼
容起见,不可以在 :function 里嵌套 :def 。
[!] 的用法同 :function 。
:enddef
:enddef 结束 :def 定义的函数。
如果函数定义所在的是 Vim9 脚本,可不经 "s:" 前缀直接访问局部于脚本的变量。这些
变量必须在函数之前定义。如果函数定义所在的是老式脚本,那么局部于脚本的变量必须
通过 "s:" 前缀才能访问。
:disa :disassemble
:disa[ssemble] {func}
显示 {func}
生成的指令序列。用于调试和测试。
注意
{func}
的命令行补全可在之前附加 "s:" 来查找局部于
脚本的函数。
4. 类型 vim9-types
还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变
支持以下内建类型:
bool
number
float
string
blob
list<type>
dict<type>
(a: type, b: type): type
job
channel
尚未支持:
tuple<a: type, b: type, ...>
以下类型可用于声明,但没有变量会有此类型:
type|type
void
any
没有 array 类型,用 list<type>
代替。list 常量使用了有效实现,以避免许多小片内
存的分配。
:def 定义的函数必须声明返回类型。如果没有类型,函数什么都不返回。"void" 可用
于类型声明。
可用 :type
定义定制类型:
:type MyList list<string>
{尚未实现}
类和接口可用作类型:
:class MyClass
:let mine: MyClass
:interface MyInterface
:let mine: MyInterface
:class MyTemplate<Targ>
:let mine: MyTemplate<number>
:let mine: MyTemplate<string>
:class MyInterface<Targ>
:let mine: MyInterface<number>
:let mine: MyInterface<string>
{尚未实现}
类型推论 type-inference
一般而言: 明显的类型可以省略。例如,声明变量并给出值时:
let var = 0 " 推论为 number 类型
let var = 'hello' " 推论为 string 类型
5. 命名风格、导入和导出
vim9script vim9-export vim9-import
还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变
可专门为导入而编写 Vim9 脚本。这意味着脚本中的一切除非明确导出,都是局部的。那
些导出的项目,也只有那些项目,可以被其它脚本导入。
命令空间
:vim9script :vim9
为了识别可导入的文件,文件出现的第一个语句必须是 vim9script 语句。它告知 Vim
脚本在自己的命令空间而不是全局命令空间里被解释。如果文件这样开始:
vim9script
let myvar = 'yes'
那么 "myvar" 只存在于此文件中。如果没有 vim9script ,可被其它脚本和函数用
g:myvar
访问。
文件层级的变量和老式脚本里的局部 "s:" 变量非常类似,但省略 "s:"。
和以前一样,Vim9 脚本中仍然可用全局 "g:" 命令空间。
:vim9script 一个副作用是 'cpoptions' 选项设为 Vim 缺省值,类似于:
:set cpo&vim
其中一个效果是 line-continuation 总是打开。脚本结束时会恢复 'cpoptions' 原先
的值。
导出
:export :exp
可以这样导出项目:
export const EXPORTED_CONST = 1234
export let someValue = ...
export def MyFunc() ...
export class MyClass ...
就像这里暗示的,只能导出常数、变量、 :def 函数和类。
另外,也可用 export 语句来导出若干个已定义的 (否则就是脚本局部的) 项目:
export {EXPORTED_CONST, someValue, MyFunc, MyClass}
导入
:import :imp
导出项目可在另一个 Vim9 脚本里被单独导入:
import EXPORTED_CONST from "thatscript.vim"
import MyClass from "myclass.vim"
要一次导入多个项目:
import {someValue, MyClass} from "thatscript.vim"
如果名字有二义性,可提供其它名字:
import MyClass as ThatClass from "myclass.vim"
import {someValue, MyClass as ThatClass} from "myclass.vim"
要导入指定标识符底下的所有导出项目:
import * as That from 'thatscript.vim'
然后就可用 "That.EXPORTED_CONST"、"That.someValue" 等等。有权选择 "That" 这样
的名字,但强烈建议使用脚本文件的名字,以免混淆。
import
之后的脚本名可以是:
- 相对路径,以 "." 或 ".." 开始。这会找到相对于脚本文件自身所在位置的文件。可
用于把大型插件分割为几个文件。
- 绝对路径,Unix 上以 "/" 开始,或 MS-Windows 上以 "D:/" 开始。很少用到。
- 既非相对也不是绝对的路径。会在 'runtimepath' 项目的 "import" 子目录中寻找。
名字通常较长且唯一,以避免载入错误的文件。
vim9 脚本文件一旦导入,结果会被缓冲,下次导入相同脚本时,会使用缓冲而不会再次
读取文件。
:import-cycle
import
命令在见到时就会执行。如果该脚本 (直接或间接地) 导入当前脚本,那么
import
之后定义的项目此时处于未处理状态。所以,循环导入可以存在,但可能因此
产生未定义的项目。
在 autoload 脚本中导入
要有最佳的启动速度,应该尽量延迟脚本的载入直到实际需要为止。建议的机制是:
1. 在插件中定义指向 autoload 脚本的用户命令、函数和/或映射。
command -nargs=1 SearchForStuff call searchfor#Stuff(<f-args>)
应放在 .../plugin/anyname.vim。 "anyname.vim" 可自由选择。
2. autoload 脚本完成实际的操作。可导入其它文件中的项目,以便把功能分割成若干
片。
vim9script
import FilterFunc from "../import/someother.vim"
def searchfor#Stuff(arg: string)
let filtered = FilterFunc(arg)
...
应放在 .../autoload/searchfor.vim。文件中的 "searchfor" 必须和函数名的前缀
完全相同,这样 Vim 才能找到此文件。
3. 其它可能在插件间共享的功能,包含导出项目和其它私有项目。
vim9script
let localVar = 'local'
export def FilterFunc(arg: string): string
...
应放在 .../import/someother.vim。
在老式 Vim 脚本中导入
如果老式 Vim 脚本中使用了 import
语句,标识符即使不指定 "s:",也会使用脚本局
部 "s:" 命令空间。
9. 理据 vim9-rationale
:def 命令
插件作者一直要求有更快的 Vim 脚本。调查发现,继续保持原有的函数调用语义会使性
能提高近乎不可能,因为涉及的函数调用、局部函数作用域的设置以及行的执行引起的负
担。这里需要处理很多细节,比如错误信息和例外。创建用于 a: 和 l: 作用域的字典、
a:000 列表和若干其它部分增加了太多不可避免的负担。
因此定义新风格函数的 :def 方法应运而生,它接受使用不同语义的函数。多数功能不
变,但有些部分有变化。经过考虑,这种定义函数的新方法是区别旧风格代码和 Vim9 脚
本代码的最佳方案。
使用 "def" 定义函数来源于 Python。其它语言使用 "function",这和老式的 Vim 脚本
使用的有冲突。
类型检查
应在编译时尽可能地把 Vim 代码行编译为指令。延迟到运行时会使执行变慢,也意味着
错误只能在后期才能发现。例如,如果遇到 "+" 字符时编译成通用的加法指令,在执行
时,此指令必须检查参数类型并决定要执行的是哪种加法。如果类型是字典要抛出错误。
如果类型已知为数值型,就可用 "数值相加" 指令,这会快很多。编译时可报错,而运行
时就无需错误处理。
类型语法类似于 Java,因为容易理解,也广泛使用。类型名是 Vim 之前所用的加上新增
的 "void" 和 "bool" 等类型。
JavaScript/TypeScript 语法和语义
脚本作者抱怨 Vim 脚本语法和他们习惯使用的有出乎意外的差异。为了减少抱怨,会使
用流行的语言作为范例。与此同时,我们不想放弃老式的 Vim 脚本为人熟知的部分。
因为 Vim 已经使用 :let 和 :const ,而可选的类型检查很受欢迎,
JavaScript/TypeScript 语法最适用于变量的声明。
const greeting = 'hello' " 推论为 string 类型
let name: string
...
name = 'John'
表达式计算已经和 JavaScript 和其它语言使用的很接近。一些细节出乎意外但可以修
正。例如 || 和 && 操作符工作的方式。老式的 Vim 脚本:
let result = 44
...
return result || 0 " 返回 1
Vim9 脚本和 JavaScript 一样,保持值不变:
let result = 44
...
return result || 0 " 返回 44
另一方面,重载 "+" 同时用于加法和字符串连接有违老式的 Vim 脚本,也经常引发错
误。为此原因,我们继续使用 ".." 用于字符串连接。Lua 也如此使用 ".."。
导入和导出
老式 Vim 脚本的一个问题是所有函数和变量缺省都是全局的。可以使它们局部于脚本,
但因而就不能为其它脚本所用。
Vim9 脚本里支持和 Javascript 导入导出非常相似的机制。这是已有的 :source 命令
的变种,且工作方式符合人们期待:
- 和所有的缺省都是全局相反,除非导出,所有的都是局部于脚本的。
- 导入脚本时列出要导入的符号,避免以后新增功能时的名字冲突和可能的失败。
- 此机制允许编写大而长又有清晰 API 的脚本: 导出函数和类。
- 通过使用相对路径,同一包里导入的载入会更快,无需搜索许多目录。
- 导入一旦使用,会被缓冲而避免了再次载入。
- Vim 特定使事物局部于脚本的 "s:" 用法可以不需要了。
类
Vim 支持 Perl、Python、Lua、Tcl 和一些其它语言的接口。但这些从未广泛使用过。
Vim 9 设计时作了一个决定,淘汰这些接口并集中在 Vim 脚本上,同时鼓励脚本作者使
用任何语言编程并作为外部工具运行,使用作业和通道通信。
使用外部工具仍然有不足。一种替代方案是把工具转换为 Vim 脚本。要尽量减少翻译的
工作量,并且同时保持代码快速,需要支持工具使用的构造。因为绝大多数语言支持类,
缺少类的支持成为了 Vim 的一个问题。
之前 Vim 通过为字典增加方法,支持过一种准面向对象的编程。如果小心一点这可以工
作,但这看来不像真正的类。更有甚者,因为字典使用的缘故,速度很慢。
Vim9 脚本对类的支持提供多数语法的类支持的 "最少公共功能"。它和 Java 的方式最类
似,这也是最流行的编程语言了。
vim:tw=78:ts=8:noet:ft=help:norl: