vim9

vim9.txt 适用于 Vim 8.2 版本。 最近更新: 2020年3月 VIM 参考手册 by Bram Moolenaar 译者: Willis 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 Vim9 脚本命令和表达式。 多数表达式的帮助可见 eval.txt 。此文件是关于 Vim9 脚本的新语法和特性。 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 1 什么是 Vim9 脚本? vim9-script 2. 差别 vim9-differences 3. 新风格函数 fast-functions 4. 类型 vim9-types 5. 命名风格、导入和导出 vim9script 9. 理据 vim9-rationale

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: