vim9

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

1. 什么是 Vim9 脚本? Vim9-script 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 Vim 脚本随着时间推移日渐庞大,同时需要保持后向兼容。这意味着过去所做的错误决定 常常不能再改变,而和 Vi 的兼容性限制了可能的解决方案。执行颇慢,每行在每次执行 时都要先解析。 Vim9 脚本的主要目标是大幅提高性能。这是通过把命令编译为可有效执行的指令完成 的。可以期待执行速度有成十上百倍的提高。 一个次要目标是避免 Vim 特定的构造,而尽量和包括 JavaScript、TypeScript 和 Java 这样的主流编程语言接近。 只有不 100% 后向兼容才能达到所需的性能提高。例如,要使函数参数可通过 "a:" 字典 获取,会增加不少开销。其它一些差别,比如错误处理的方式,则更细微一些。 Vim9 脚本语法和语义用于: - 使用 :def 命令定义的函数 - 首个命令是 vim9script 的脚本文件 - 上述上下文中定义的自动命令 Vim9 脚本文件里如果用了 :function ,会用老式的语法并使用最高的 scriptversion 。这容易混淆,所以不鼓励。 Vim9 脚本和老式的 Vim 脚本可以混合。没有必要重写旧脚本,它们会继续工作。可用一 些 :def 函数来编写要求快速的代码。

2. 和老式 Vim 脚本的差异 vim9-differences 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 总览 使用 Vim9 脚本和 :def 函数最常见区别的简要小结;细节见后: - 注释以 # 开始而不是 ": echo "hello" # 注释 - 很少需要反斜杠用来作续行符: echo "hello " .. yourName .. ", how are you?" - 很多地方需要空格。 - 赋值不用 :let ,变量用 :var 声明: var count = 0 count += 3 - 常量可用 :final:const 声明: final matches = [] # 加入 matches const names = ['Betty', 'Peter'] # 不能改变 - :final 不再能用作 :finally 的缩写。 - 变量和函数缺省局部于脚本。 - 函数声明要带参数类型和返回值类型: def CallMe(count: number, message: string): bool - 调用函数不用 :call : writefile(['done'], 'file.txt') - 不可用 :xit:t:append:change:insert 或花括号名字。 - 命令前的范围必须用冒号前缀: :%s/this/that - 除非特别指出,使用最高的 scriptversion# 开始的注释 老式 Vim 脚本以双引号开始脚本注释。Vim9 脚本则用 # 开始脚本注释。 # 声明 var count = 0 # 出现的次数 原因是双引号也是字符串的开始,在很多地方,尤其是表达式内部换行处,很难区分双引 号真正的意思,因为字符串和注释都可以后跟任意文本。为避免混淆,现在只识别 # 注 释。这和外壳脚本和 Python 程序完全相同。 Vi 里 # 是带数字列出文本的命令。Vim9 脚本可用 :number 代替。 101 number 为提高可读性,命令和开始注释的 # 之间必须有一个空格: var name = value # 注释 var name = value# 出错! 老式 Vim 脚本中 # 也用于轮换文件名。Vim9 脚本里,要改用 %%。## 可改用 %%% (代 表所有参数)。 Vim9 函数:def 定义的函数进行编译。执行成倍加快,通常有 10x 到 100x 的加速。 许多错误在编译时已经可以发现,而不用等到函数执行的时候。有严格的语法,以确保代 码易读易理解。 以下情形之一时进行编译: - 函数第一次调用时 - 函数定义所在的脚本中遇到 :defcompile 命令时 - 函数使用 :disassemble 时。 - 函数被已编译的函数调用或用作函数引用时 :def 不像 :function 那样有 "range"、"abort"、"dict" 或 "closure" 这样的选 项。 :def 函数总会在错误时中止 (除非命令使用了 :slient! 或在 :try 块里), 不接受范围也不能是 "dict" 函数,总可以有闭包。 必须指定参数类型和返回类型。可用 "any" 类型,和老式函数一样,此时类型检查会在 运行时完成。 参数用不带 "a:" 的名字访问,就像其它语言一样。没有 "a:" 字典或 "a:000" 列表。 类似于 TypeScript,通过给定名字和列表类型的末项参数来定义可变参数。例如,数值 的列表: def MyFunc(...itemlist: list<number>) for item in itemlist ... 函数和变量缺省局部于脚本 vim9-scopes 在 Vim9 脚本中,用 :function:def 来指定脚本级别的新函数时,函数局部于 脚本,就像用了 "s:" 前缀那样。"s:" 前缀可选。要定义全局函数或变量,必须使用 "g:" 前缀。autoload 脚本中的函数使用 "name#" 这样的前缀就行了。 def ThisFunction() # 局部于脚本 def s:ThisFunction() # 局部于脚本 def g:ThatFunction() # 全局 def scriptname#function() # autoload :function:def 来指定 :def 函数内的嵌套函数时,此嵌套函数局部于定 义所有的代码块。 :def 函数内不可以定义局部于脚本的函数。可以用 "g:" 前缀来定 义全局函数。 引用函数时如果不带 "s:" 或 "g:" 前缀,Vim 会依次搜索函数: - 在函数作用域内,在块作用域内 - 在脚本作用域内,可能通过导入引进 - 在全局函数列表里 不过,为了清晰起见,建议引用全局函数时总带上 "g:"。 在所有情况下,函数必须先定义,然后才能使用。这是函数调用时,或是 :defcompile 导致调用被编译时,或是调用它的函数在被编译的时候。 其结果是,无命名空间的函数和变量通常可在定义所在或导入的脚本内访问。全局函数和 变量可以在任何地方定义 (要找到需要点运气!)。 全局函数还是可以在几乎任何时候定义和删除。vim9 脚本里,局部于脚本的函数在脚本 载入时定义一次,且不能删除或替代。 编译函数时和遇到函数调用时如果函数 (还) 未定义,不触发 FuncUndefined 自动命 令。如果需要,可用自动载入函数,或调用老式函数,那里会触发 FuncUndefined重载 Vim9 脚本时缺省清除函数或变量 vim9-reload 再次载入同一老式 Vim 脚本时,不会删除任何东西,脚本里的命令会替代原有的变量和 函数,并创建新的内容。 再次载入同一 Vim9 脚本时,删除所有已有的局部于脚本的函数和变量,以便从干净的状 态开始。可用于开发插件时新版本的实验。如果进行了任何换名,不需要担心旧名字还存 在。 如果确实要保留项目,可用: vim9script noclear 可在脚本中使用此命令,以便在脚本重载时,在某些情况下用 finish 命令放弃载入。 例如,如果设置了某个缓冲区局部选项: vim9script noclear setlocal completefunc=SomeFunc if exists('*g:SomeFunc') | finish | endif def g:SomeFunc() .... 用 :var、:final 和 :const 声明变量 vim9-declaration :var 局部变量需用 :var 定义。局部常量需用 :final:const 定义。我们把两者都 称为 "变量"。 变量可以局部于脚本、函数或代码块: vim9script var script_var = 123 def SomeFunc() var func_var = script_var if cond var block_var = func_var ... 变量只在定义所在的块和嵌套块中可见。块定义结束后,变量不再可访问: if cond var inner = 5 else var inner = 0 endif echo inner # 出错! 变量必须在使用之前进行声明: var inner: number if cond inner = 5 else inner = 0 endif echo inner 要有意识地从之后的代码中隐藏某个变量,可用代码块: { var temp = 'temp' ... } echo temp # 出错! 变量声明时如果带类型但不带初始块,其值初始为零、false 或空。 Vim9 脚本中不能用 :let 。已有的变量可直接赋值,不需要任何命令。全局、窗口、标 签页、缓冲区和 Vim 变量也是一样,因为它们并非真正通过声明产生,但它们也可用 :unlet 删除。 变量和函数不能隐藏之前定义或导入的变量和函数。 变量可以隐藏 Ex 命令,如有需要请给变量换名。 全局变量和用户定义的函数必须带上 "g:" 前缀,即使脚本级别的也是如此。 vim9script var script_local = 'text' g:global = 'value' var Funcref = g:ThatFunction 因为 `&opt = value` 现在用来给选项 "opt" 赋值,所以不再能用 ":&" 来重复 :substitute 命令了。 常量 vim9-const vim9-final 常量如何工作因语言而异。有些把不能重新赋其它值的变量当作常量。JavaScript 是一 个例子。其它语言则会使值也不可更改,因此如果常量使用了列表,列表本身也不可改变。 Vim9 可以兼有两者。 可用 :const 使变量及其值为常量。可用于复合结构以确保其不会被修改。示例: const myList = [1, 2] myList = [3, 4] # 出错! myList[0] = 9 # 出错! muList->add(3) # 出错! :final 可用 :final 使变量为常量,而值仍然可更改。Java 用户对此很熟悉。例如: final myList = [1, 2] myList = [3, 4] # 出错! myList[0] = 9 # OK muList->add(3) # OK 常量写入全大写 ALL_CAPS 是惯例,但不必如此。 常量限制只适用于值本身,而不是其引用的值。 final females = ["Mary"] const NAMES = [["John", "Peter"], females] NAMES[0] = ["Jack"] # 出错! NAMES[0][0] = "Jack" # 出错! NAMES[1] = ["Emma"] # 出错! NAMES[1][0] = "Emma" # OK,现在 females[0] == "Emma" E1092 目前不支持使用解包记法的多于一个变量的声明: var [v1, v2] = GetValues() # 出错! 这是因为需要从列表项目类型中推导出类型,目前这有困难。 省略 :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() "foobar"->Process() ("foobar")->Process() 'foobar'->Process() ('foobar')->Process() 在罕见情形下函数名和 Ex 命令有二义性,用 ":" 前缀来明确你想用的是 Ex 命令。例 如,既有 :substitute 命令,又有 substitute() 函数。如果一行以 substitute( 开始,会假定使用函数,要使用命令版本,加上冒号前缀: :substitute(pattern (replacement ( 注意 尽管变量在使用前必须先定义,函数在定义前就可以调用。为了允许函数间能相互 依赖,这是有必要的。但因此效率会稍稍低些,因为函数需要依名查找。且函数名的拼写 错误只有在函数进行调用时才能发现。 省略 function() 表达式中,用户定义函数可用作函数引用,而无需通过 function() 。这里会检查参数 类型和返回类型。函数必须已经有定义。 var Funcref = MyFunction 使用 function() 时,返回类型是 "func",一个可带任何数量参数和任何返回类型的 函数。函数此时可以延后定义。 匿名函数使用 => 而不是 -> 在老式脚本里,"->" 同时用于函数调用和用于匿名函数,这里可能有混淆。另外,找到 "{" 时解析器需要知道它是匿名函数还是字典的开始,现在由于参数类型的使用,这里的 情况更复杂。 为避免这些问题,Vim9 脚本的匿名函数使用不同的语法,这和 Javascript 类似: var Lambda = (arg) => expression 匿名函数在直到 "=>" 为止的参数里不允许换行。这样可以: filter(list, (k, v) => v > 0) 这样不可以: filter(list, (k, v) => v > 0) 这样也不可以: filter(list, (k, v) => v > 0) 但可用反斜杠在解析发生前,把行进行连接: filter(list, (k, \ v) \ => v > 0) 此外,匿名函数可以包含 {} 包围的多个语句: var Lambda = (arg) => { g:was_called = 'yes' return expression } 未 实 现 为了不让字典常量的 "{" 被识别为语句块,把它包围在括号里: var Lambda = (arg) => ({key: 42}) 自动行继续 许多情况下,表达式显然会在下一行继续。这些情况不需要在行前加入反斜杠 (见 line-continuation )。例如,当列表跨越多行时: var mylist = [ 'one', 'two', ] 字典跨越多行时: var mydict = { one: 1, two: 2, } 函数调用: var result = Func( arg1, arg2 ) 对不在 []、{} 或 () 内的表达式中的二元操作符而言,可在操作符之前或之后断行。例 如: var text = lead .. middle .. end var total = start + end - correction var result = positive ? PosFunc(arg) : NegFunc(arg) 对进行方法调用的 "->" 和进行成员访问的点号,之前可断行: var result = GetBuilder() ->BuilderSetWidth(333) ->BuilderSetHeight(777) ->BuilderBuild() var result = MyDict .member 命令里如果有参数是一组命令,行首的 | 字符指示行的继续: autocmd BufNewFile *.match if condition | echo 'match' | endif E1050 要识别出现在行首的操作符,需要在范围之前加上冒号。要把 "start" 加上 print: var result = start + print 相当于: var result = start + print 而要赋值 "start" 并显示一行: var result = start :+ print 注意 +cmd 参数不需要冒号: edit +6 fname 函数声明也可以在参数间被分割为多行: def MyFunc( text: string, separator = '-' ): string 因为续行不容易识别,命令的解析必须更严格。例如,因为首行有错,第二行会看作一个 单独的命令: popup_create(some invalid expression, { exit_cb: Func}) 现在,"exit_cb: Func})" 实际是一个合法的命令: 保存任何改动到文件 "_cb: Func})" 并退出。为了避免此种错误,Vim9 脚本中在多数命令名和参数间必须有空白。 不过,不能识别作为命令参数的命令。例如,在 "windo echo expr" 之后,不能识别 "expr" 内部的换行。 注意: - "enddef" 不能用于继续行的开始,它会结束当前函数。 - 赋值语句的 LHS 不接受换行。特别是列表解包 :let-unpack 。这样可以: [var1, var2] = Func() 这样不行: [var1, var2] = Func() - :echo:execute 和类似命令的参数与参数之间不接受换行。这样可以: echo [1, 2] [3, 4] 这样不行: echo [1, 2] [3, 4] 没有花括号扩展 不能使用 curly-braces-names字典常量 传统上 Vim 支持使用 {} 语法的字典常量: let dict = {'key': value} 后来越来越明显地,使用简单文本键非常常见,所以引入了支持后向兼容的常量字典: let dict = #{key: value} 不过,此种 #{} 语法不同于任何已有的语言。因为使用常量并使用表达式作为键更为常 见,也考虑到 JavaScript 也使用此种语法,使用 {} 形式的字典常量被认为是更有用的 语法。Vim9 脚本里,{} 形式使用常量键: var dict = {key: value} 这种形式可以用于字母数字字符,下划线和连字符。如果使用其它字符,使用单引号或双 引号: var dict = {'key with space': value} var dict = {"key\twith\ttabs": value} var dict = {'': value} # 空键 如果键需要表达式,可用方括号,就像 JavaScript 那样: var dict = {["key" .. nr]: value} 没有 :xit、:t、:append、:change 或 :insert 这些命令很容易会和局部变量名混淆。 :x:xit 可用 :exit 替代。 :t 可用 :copy 替代。 比较符 字符串的比较符不考虑 'ignorecase' 选项。 For 循环 老式 Vim 脚本有一些技巧来实现在列表句柄上的 for 循环,以删除当前或前一项目。在 Vim9 脚本中,正常使用索引就可以了,如果项目被删除,列表中的对应项目会跳过。 示例老式脚本: let l = [1, 2, 3, 4] for i in l echo i call remove(l, index(l, i)) endfor 会显示: 1 2 3 4 而在编译后的 Vim9 脚本中,会得到: 1 3 一般而言,不应改变正在遍历的列表。如果需要的话,先建立备份。 空白 Vim9 脚本强制空白的正确使用。以下不再允许: var name=234 # 出错! var name= 234 # 出错! var name =234 # 出错! "=" 前后必须有空白: var name = 234 # OK 命令之后开始注释的 # 之前必须有空白字符: var name = 234# 出错! var name = 234 # OK 多数操作符需要用空白包围。 子列表 (切片) 表达式中的 ":" 两边需要空白,出现在开始和结束处的除外: otherlist = mylist[v : count] # v:count 有不同含义 otherlist = mylist[:] # 建立列表的备份 otherlist = mylist[v :] otherlist = mylist[: v] 以下位置不允许空白: - 函数名和 "(" 之间: Func (arg) # 出错! Func \ (arg) # 出错! Func (arg) # 出错! Func(arg) # 正确 Func( arg) # 正确 Func( arg # 正确 ) 条件和表达式 多数情况下,条件和表达式就像其它语言里那样的用法。有些值和老式 Vim 脚本有所不 同: 值 老式 Vim 脚本 Vim9 脚本 0 准假值 准假值 1 准真值 准真值 99 准真值 出错! "0" 准假值 出错! "99" 准真值 出错! "text" 准假值 出错! 用于 "??" 操作符和使用 "!" 时,不会报错,每个值都是准假值或准真值。这很像 JavaScript,唯一的例外是空列表和字典也被视为准假值: 类型 何时为准真值 bool true、v:true 或 1 number 非零 float 非零 string 非空 blob 非空 list 非空 (和 JavaScript 不同) dictionary 非空 (和 JavaScript 不同) func 有函数名时 special true 或 v:true job 非 NULL 时 channel 非 NULL 时 class 非 NULL 时 object 非 NULL 时 (TODO: 应为 isTrue() 返回 true 时) 布尔操作符 "||" 和 "&&" 期待值为布尔型、零或一: 1 || false == true 0 || 1 == true 0 || false == false 1 && true == true 0 && 1 == false 8 || 0 出错! 'yes' && 0 出错! [] || 99 出错! "!" 用作反转操作符时,任何类型都不会报错,结果为布尔型。"!!" 可用于把任何值转 换为布尔型: !'yes' == false !![] == false !![1, 2, 3] == true " .." 用作字符串连接时,简单类型的参数总是转化为字符串。 'hello ' .. 123 == 'hello 123' 'hello ' .. v:true == 'hello true' 简单类型是字符串、浮点数、特殊类型和布尔型。其它类型可用 string() false true null Vim9 脚本里,可用 "true" 代表 v:true、"false" 代表 v:false 而 "null" 代表 v:null。转换布尔型为字符串时使用 "false" 和 "true",而不是老式脚本里那样用 "v:false" 和 "v:true"。对 "v:none" 不作改变,因为它只用于 JSON 而其它语言里没 有类似的结构。 字符串用 [idx] 或 [idx : idx] 索引时使用的是字符索引而非字节索引。例如: echo 'bár'[1] 在老式脚本里,这会得到字符 0xc3 (一个非法字节),在 Vim9 脚本里这会得到字符串 'á'。 负索引从结尾处开始计算,"[-1]" 代表末字符。要排除末字符,用 slice() 。 如果索引超出范围,返回空字符串。 老式脚本中,接受 "++var" 和 "--var",不报错也没有效果。Vim9 脚本中则会报错。 零开始的数值不被识别为八进制,只有 "0o" 开始的数值才识别为八进制: "0o744". scriptversion-4 注意什么 vim9-gotchas Vim9 设计和常用的编程语言相近,而同时试图支持老式的 Vim 命令。这里不得不做出某 些妥协。这里是若干出人意外之处的一个小结。 Ex 命令范围需要冒号前缀。 -> 老式 Vim: 右移前行 ->func() Vim9: 继续行上的方法调用 :-> Vim9: 右移前行 %s/a/b 老式 Vim: 在所有行上替代 x = alongname % another Vim9: 续行上的取余操作符 :%s/a/b Vim9: 在所有行上替代 't 老式 Vim: 跳转到位置标记 t 'text'->func() Vim9: 方法调用 :'t Vim9: 跳转到位置标记 t 有些 Ex 命令在 Vim9 脚本中和赋值语句会引起混淆: g:name = value # 赋值 g:pattern:cmd # 非法命令 - 报错 :g:pattern:cmd # :global 命令 :def 定义的函数会对整个函数进行编译。老式函数可以随时放弃,因而以下的行不会 被立即解析: func Maybe() if !has('feature') return endif use-feature endfunc Vim9 函数作为一个整体被编译: def Maybe() if !has('feature') return endif use-feature # 可能会报编译错误 enddef 一个临时解决方法是把它分割为两个函数: func Maybe() if has('feature') call MaybyInner() endif endfunc if has('feature') def MaybeInner() use-feature enddef endif 或者把不支持的代码放在 if 块里,带上计算值为 false 的常量表达式: def Maybe() if has('feature') use-feature endif enddef vim9-user-command 编译函数的另一个副作用是在编译时对用户命令的存在与否进行检查。如果用户命令在之 后定义,会报错。这样可以: command -nargs=1 MyCommand echom <q-args> def Works() MyCommand 123 enddef 这样会报错,"MyCommand" 无定义: def Works() command -nargs=1 MyCommand echom <q-args> MyCommand 123 enddef 一个临时解决方法是用 :execute 间接执行命令: def Works() command -nargs=1 MyCommand echom <q-args> execute 'MyCommand 123' enddef 注意 在不识别的命令里,不检查 "|" 和后跟的命令。下例会报丢失 endif 的错误: def Maybe() if has('feature') | use-feature | endif enddef 其它差异 模式使用假定 'magic' 置位,除非被显式覆盖。 不使用 'edcompatible' 选项值。 不使用 'gdefault' 选项值。

3. 新风格函数 fast-functions 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 :def :def[!] {name}([arguments])[: {return-type}] 定义名为 {name} 的新函数。在下面的行给出函数体,直到配 对的 :enddef 为止。 {return-type} 如果省略或为 "void",不期待函数返回任何 值。 {arguments} 是零或多个参数声明的序列。有以下三种形式: {name}: {type} {name} = {value} {name}: {type} = {value} 第一种形式是必选参数,调用者必须提供。 第二三种形式是可选参数。如果调用者省略参数,使用 {value} 值。 此函数在实际调用时、使用 :disassemble:defcompile 时被编译为指令序列。语法和类型错误会在那 里发生。 在 :def:function 里可以嵌套另一个 :def ,最多 可达 50 层。 [!] 的用法同 :function注意 Vim9 脚本里局部于脚本的 函数不能删除或在之后重定义。只能通过重新载入相同的脚本 来移除。 :enddef :enddef 结束 :def 定义的函数。必须单独起一行。 如果函数定义所在的是 Vim9 脚本,可不经 "s:" 前缀直接访问局部于脚本的变量。这些 变量必须在函数编译之前定义。如果函数定义所在的是老式脚本,那么局部于脚本的变量 必须通过 "s:" 前缀才能访问,且它们不必存在 (可以在任何时候删除)。 :defc :defcompile :defc[ompile] 编译尚未编译的在当前脚本中定义的函数。 编译中如有错误,会报错。 :disa :disassemble :disa[ssemble] {func} 显示 {func} 生成的指令序列。用于调试和测试。 注意 {func} 的命令行补全可在之前附加 "s:" 来查找局部于 脚本的函数。 局限 字符串表达式计算时,局部变量不可见。例如: def MapList(): list<string> var list = ['aa', 'bb', 'cc', 'dd'] return range(1, 2)->map('list[v:val]') enddef map 参数是字符串表达式,它在函数作用域之外进行计算。可用匿名函数代替: def MapList(): list<string> var list = ['aa', 'bb', 'cc', 'dd'] return range(1, 2)->map(( _, v) => list[v]) enddef 这也适用于未编译的命令,例如 :global 。为此,可用于反引号扩展。例如: def Replace() var newText = 'blah' g/pattern/s/^/`=newText`/ enddef

4. 类型 vim9-types 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 支持以下内建类型: bool number float string blob list<{type}> dict<{type}> job channel func func: {type} func({type}, ...) func({type}, ...): {type} 尚未支持: tuple<a: {type}, b: {type}, ...> 以下类型可用于声明,但不会有值有此类型: {type}|{type} {尚未实现} void any 没有 array 类型,用 list<{type}> 代替。list 常量使用了有效实现,以避免许多小片 内存的分配。 可以用具体程度不同的方式来声明偏函数和函数: func 任何类型的函数引用,不检查参数和返回值类型 func: {type} 任何数量和类型的参数,特定返回类型 func({type}) 指定参数类型的函数,无返回值 func({type}): {type} 指定参数类型和返回类型的函数 func(?{type}) 指定可选参数类型的函数,无返回值 func(...{type}) 带可变数目的指定类型的函数,无返回值 func({type}, ?{type}, ...{type}): {type} 带以下的函数: - 必选参数的类型 - 可选参数的类型 - 可变数目参数的类型 - 返回类型 如果返回类型为 "void",函数什么都不返回。 引用也可是 Partial ,此时它保存了额外参数和/或字典,而这些对调用者是不可见 的。因为调用方式相同,声明方式也是一致的。 可用 :type 定义定制类型: :type MyList list<string> 定制类型必须以大写字母开头,以避免和之后新增的内建类型名字起冲突,这和用户函数 类似。 {尚未实现} 类和接口可用作类型: :class MyClass :var mine: MyClass :interface MyInterface :var mine: MyInterface :class MyTemplate<Targ> :var mine: MyTemplate<number> :var mine: MyTemplate<string> :class MyInterface<Targ> :var mine: MyInterface<number> :var mine: MyInterface<string> {尚未实现} 变量类型和类型转换 variable-types Vim9 脚本或 :def 函数中声明的变量有类型,或者显式指定,或者通过初始化推导得 出。 全局、缓冲区、窗口和标签页变量没有特定类型,其值可在任何时候改变,类型的改变亦 然。因此,编译后的代码假定其为 "any" 类型。 如果 "any" 类型不合适,而实际的类型希望保持不变时,这会有问题。例如,要声明列 表: var l: list<number> = [1, g:two] 编译时 Vim 不知道 "g:two" 类型,而表达式类型成为 list<any>。此时会生成指令,在 赋值之前检查列表类型,但这对效率有些影响。 type-casting 为避免此种情况,可用类型转换: var l: list<number> = [1, <number>g:two] 编译后的代码只会检查 "g:two" 是否为数值,如果不是会报错。这叫类型转换。 类型转换的语法是: "<" {type} ">"。"<" 之后或 ">" 之前不能有空格 (为避免和小于 号大于号操作符混淆)。 其语义为,有必要时执行运行时类型检查。这里值并不改变。如果要改变类型,如转换为 字符串型,可用 string() 函数。或用 str2nr() 把字符串转换为数值。 类型推论 type-inference 一般而言: 明显的类型可以省略。例如,声明变量并给出值时: var var = 0 # 推论为 number 类型 var var = 'hello' # 推论为 string 类型 列表和字典的类型来自其值的公共类型。如果所有值为相同类型,则使用该类型为列表或 字典的类型。如果有不同类型的混合,则用 "any" 类型。 [1, 2, 3] list<number> ['a', 'b', 'c'] list<string> [1, 'x', 3] list<any> 更严格的类型检查 type-checking 在老式 Vim 脚本中,在期待数值的地方,字符串会自动转换为数值。这方便实际为数值 的情形如 "123",但对不以数值开始的字符串会出现奇怪的问题 (但不报错)。这通常会 导致很难发现的漏洞。 Vim 脚本中这里会更严格。只要使用的值匹配期待的类型,绝大多数地方和以前一样工 作。有时会报错,破坏后向兼容性。例如: - 期待布尔型时使用非 0 或 1 的数值。 E1023 - 设置数值选项时使用字符串值。 - 期待字符串时使用数值。 E1024 一个后果是,传递给 map() 的列表或字典里的项目类型不能改变。编译代码里以下会报 错: map([1, 2, 3], (i, v) => 'item ' .. i) E1012: Type mismatch; expected list<number> but got list<string> 相反地,可用 mapnew()

5. 命名风格、导入和导出 vim9script vim9-export vim9-import 还 在 开 发 中 - 一 切 都 可 能 失 效 - 一 切 都 可 能 会 改 变 可专门为导入而编写 Vim9 脚本。这意味着脚本中的一切除非明确导出,都是局部的。那 些导出的项目,也只有那些项目,可以被其它脚本导入。 可显式使用全局命名空间来作弊。我们假定你不会这样干就是了。 命名空间 vim9-namespace 为了识别可导入的文件,文件出现的第一个语句必须是 vim9script 语句。它告知 Vim 脚本在自己的命名空间而不是全局命名空间里被解释。如果文件这样开始: vim9script var myvar = 'yes' 那么 "myvar" 只存在于此文件中。如果没有 vim9script ,可被其它脚本和函数用 g:myvar 访问。 文件层级的变量和老式脚本里的局部 "s:" 变量非常类似,但省略 "s:"。而且不能被删 除。 和以前一样,Vim9 脚本中仍然可用全局 "g:" 命名空间。还有 "w:"、"b:" 和 "t:" 命 名空间。它们的共同点是变量不声明,且可以删除。 :vim9script 一个副作用是 'cpoptions' 选项设为 Vim 缺省值,类似于: :set cpo&vim 其中一个效果是 line-continuation 总是打开。脚本结束时会恢复 'cpoptions' 原先 的值。 导出 :export :exp 可以这样导出项目: export const EXPORTED_CONST = 1234 export var someValue = ... export final someValue = ... export const someValue = ... export def MyFunc() ... export class MyClass ... 就像这里暗示的,只能导出常数、变量、 :def 函数和类。{类尚未实现} E1042 :export 只能用于 Vim9 脚本的脚本级别。 导入 :import :imp E1094 导出项目可在另一个 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' {未实现: 用 "This as That"} 然后就可用 "That.EXPORTED_CONST"、"That.someValue" 等等。有权选择 "That" 这样 的名字,但强烈建议使用脚本文件的名字,以免混淆。 :import 也可用于老式 Vim 脚本。即使未给出 "s:" 前缀,导入项目仍然是脚本局部 的。 import 之后的脚本名可以是: - 相对路径,以 "." 或 ".." 开始。这会找到相对于脚本文件自身所在位置的文件。可 用于把大型插件分割为几个文件。 - 绝对路径,Unix 上以 "/" 开始,或 MS-Windows 上以 "D:/" 开始。很少用到。 - 既非相对也不是绝对的路径。会在 'runtimepath' 项目的 "import" 子目录中寻找。 名字通常较长且唯一,以避免载入错误的文件。 vim9 脚本文件一旦导入,结果会被缓冲,下次导入相同脚本时,会使用缓冲而不会再次 读取文件。 :import-cycle import 命令在见到时就会执行。如果该脚本 (直接或间接地) 导入当前脚本,那么 import 之后定义的项目此时处于未处理状态。所以,循环导入可以存在,但可能因此 产生未定义的项目。 在 autoload 脚本中导入 要有最佳的启动速度,应该尽量延迟脚本的载入直到实际需要为止。建议的机制是: 1. 在插件中定义指向 autoload 脚本的用户命令、函数和/或映射。 command -nargs=1 SearchForStuff searchfor#Stuff(<f-args>) 应放在 .../plugin/anyname.vim。 "anyname.vim" 可自由选择。 2. autoload 脚本完成实际的操作。可导入其它文件中的项目,以便把功能分割成若干 片。 vim9script import FilterFunc from "../import/someother.vim" def searchfor#Stuff(arg: string) var filtered = FilterFunc(arg) ... 应放在 .../autoload/searchfor.vim。文件中的 "searchfor" 必须和函数名的前缀 完全相同,这样 Vim 才能找到此文件。 3. 其它可能在插件间共享的功能,包含导出项目和其它私有项目。 vim9script var localVar = 'local' export def FilterFunc(arg: string): string ... 应放在 .../import/someother.vim。 编译 :def 函数时如果遇到 autoload 脚本中的函数,直到 :def 函数调用时才载入 该脚本。 在老式 Vim 脚本中导入 如果老式 Vim 脚本中使用了 import 语句,即使不指定 "s:",导入项目也会使用脚本 局部 "s:" 命名空间。

6. 将来工作: 类 vim9-classes 上面有几次提到了 "class" (类),但还未实现。 绝大多数 Vim 脚本可以创建而无需此功能,因为类的实现需要大量工作,留在将来去实 现。目前,只要确定以后会加入类就可以了。 想法: - class / endclass ,都在一个文件里 - 类名总是骆峰式的 - 单个构造函数 - 单继承,使用形式 `class ThisClass extends BaseClass` - `abstract class` - interface (不带实现的抽象类) - `class SomeClass implements SomeInterface` - 类的泛型: `class <Tkey, Tentry>` - 函数的泛型: `def <Tkey> GetLast(key: Tkey)` 再说明一遍,许多想法来自 TypeScript。 有希望的新增特性: - 把类当作界面使用 (类似于 Dart) - 用 import 来给类增加方法扩展 (类似于 Dart) 一种将来会提供的重要的类是 "Promise"。因为 Vim 是单线程的,和异步操作连接是允 许插件实现非用户阻塞式功能的自然方式。这将是调用回调和处理超时以及错误的统一方 法。

9. 理据 vim9-rationale :def 命令 插件作者一直要求有更快的 Vim 脚本。调查发现,继续保持原有的函数调用语义会使性 能提高近乎不可能,因为涉及的函数调用、局部函数作用域的设置以及行的执行引起的负 担。这里需要处理很多细节,比如错误信息和例外。创建用于 a: 和 l: 作用域的字典、 a:000 列表和若干其它部分增加了太多不可避免的负担。 因此定义新风格函数的 :def 方法应运而生,它接受使用不同语义的函数。多数功能不 变,但有些部分有变化。经过考虑,这种定义函数的新方法是区别老式风格代码和 Vim9 脚本代码的最佳方案。 使用 "def" 定义函数来源于 Python。其它语言使用 "function",这和老式的 Vim 脚本 使用的有冲突。 类型检查 应在编译时尽可能地把 Vim 代码行编译为指令。延迟到运行时会使执行变慢,也意味着 错误只能在后期才能发现。例如,如果遇到 "+" 字符时编译成通用的加法指令,在运行 时,此指令必须检查参数类型并决定要执行的是哪种加法。如果类型是字典要抛出错误。 如果类型已知为数值型,就可用 "数值相加" 指令,这会快很多。编译时可报错,而运行 时就无需错误处理,因为两个数值相加不会出错。 类型语法,包括使用 <type> 用作复合类型,类似于 Java,因为容易理解,也广泛使 用。类型名是 Vim 之前所用的加上新增的 "void" 和 "bool" 等类型。 去除臃肿和怪异行为 一旦决定 :def 函数可以和老式函数有不同的语法,我们就有自由去新增改进,使了解 常用编程语言的用户对代码能更熟悉。换而言之: 删除只有 Vim 才采取的怪异行为。 我们也可以去除臃肿,这里主要是指使 Vim 脚本和陈旧的 Vi 命令后向兼容的那些部 分。 例如: - 调用函数不再需要 :call ,而操作数据不再需要 :eval 。 - 续行不再需要前导反斜杠,可以自动判断表达式何处终止。 不过,这也意味有些部分需要改变: - 注释改用 # 而不是 " 开头,以避免和字符串混淆。这也很好,若干流行的语言都是如 此。 - Ex 命令范围需要有冒号前导,以避免和表达式混淆 (单引号可以是字符串或位置标 记,"/" 可能是除法或搜索命令,等等)。 目标是尽量减少差异。一个好的标准是如果不小心用了旧语法,很有可能你会得到错误信 息。 来自流行语言的语法和语义 脚本作者抱怨 Vim 脚本语法和他们习惯使用的有出乎意外的差异。为了减少抱怨,使用 流行的语言作为范例。与此同时,我们不想放弃老式的 Vim 脚本为人熟知的部分。 有很多方面我们跟随 TypeScript。这是新近的语言,已得到广泛流行,且和 Vim 脚本有 相似性。它也有静态类型 (总是有已知值类型的变量) 和动态类型 (在运行时可决定有不 同类型的变量) 的混合。既然老式 Vim 脚本是动态类型的,许多现有功能 (尤其是内建 函数) 依赖于这一点,而静态类型允许更快地执行,我们需要在 Vim9 脚本中支持两者的 混合。 我们无意完全照搬 TypeScript 语法和语义。只想借用可用于 Vim 的部分,使 Vim 用户 可以开心地接受。TypeScript 是个复杂的语言,有它自己的历史,优点和缺点。关于缺 点部分,可阅读此书: "JavaScript: The Good Parts"。或找找此文章 "TypeScript: the good parts" 并阅读 "Things to avoid" 一节。 熟悉其它语言 (Java、Python 等等) 的人士可能会不喜欢或不理解 TypeScript 的其它 一些部分。我们也试图避免那些部分。 避免的 TypeScript 特定项目: - 重载 "+" 同时用于加法和字符串连接。这有违老式的 Vim 脚本,也经常引发错误。为 此原因,我们继续使用 ".." 用于字符串连接。Lua 也如此使用 ".."。这也方便把更 多值转换为字符串。 - TypeScript 可用形如 "99 || 'yes'" 的表达式作为条件,但不能把该值赋给布尔型。 这不统一也很讨厌。Vim 识别带 && 或 || 的表达式,并可以把结果用作布尔型。 TODO: 要重新考虑 - TypeScript 把空串当作假值,而空列表或字典当作真值。这不统一。Vim 中空列表和 字典也都当作假值。 - TypeScript 有若干 "只读" 类型,其用途有限,因为类型转换可抹除其不可更改的性 质。Vim 则对值进行锁定,这更灵活,但只在运行时进行检查。 声明 老式 Vim 脚本中,每个赋值都要用 :let 语句,而 Vim9 中只需用之作声明。既有此 不同,最好采用另一个命令: :var 。它在很多语言中都有用到。语义或有些许不同,但 都很容易把它识别为声明。 用 :const 来定义常量很常见,但语义有差别。有些语言只使变量本身不可变,而其它 语言则使其值不可变。考虑到 "final" 在 Java 里使变量不可变已为人熟知,我们决定 采用它来实现该语义。而 :const 可用作使两者都不可变。这也用于老式 Vim 脚本 里,含义近乎相同。 最后,我们采用的和 Dart 十分类似: :var name # 可变的变量和值 :final name # 不可变的变量,可变的值 :const name # 不可变的变量和值 因为老式和 Vim9 脚本会混合使用,全局变量也会共享,所以类型检查最好可选。另外, 类型推断机制会在很多场合下不再需要对类型直接指定。TypeScript 语法最适合为声明 加入类型: var name: string # 指定字符串类型 ... name = 'John' const greeting = 'hello' # 推断为字符串类型 这是我们如何在声明在放入类型: var mylist: list<string> final mylist: list<string> = ['foo'] def Func(arg1: number, arg2: string): bool 考虑过两种其它方案: 1. 把类型放在名字前,类似于 Dart: var list<string> mylist final list<string> mylist = ['foo'] def Func(number arg1, string arg2) bool 2. 为类型放在变量名后,但不用冒号,类似于 Go: var mylist list<string> final mylist list<string> = ['foo'] def Func(arg1 number, arg2 string) bool 第一种对用过 C 或 Java 的用户很熟悉。而第二种和第一种比起来没有任何好处,我们 先排除第二种。 因为使用了类型推断机制,如果可以从值中推断,类型可以省略。这意味着在 var 后 我们不知道是跟着类型还是名字。这使解析复杂化,不仅对 Vim,对人而言也是如此。另 外,这样也没法使用和类型名重名的变量名,用 `var string string` 太混淆了。 我们最终选择了用冒号分隔名字和类型的语法。它需要引入标点符号,但实际更易分辨声 明的不同部分。 表达式 表达式计算已经和其它语言采用的方式很接近。有些细节有出入,有改进的空间。例如, 布尔条件可以接受字符串,先把它转换为数值,并检查该值是否非零。这不符一般期望, 经常引发错误,因为不以数值开头的文本会转换为零,也就被当作假值。如此,字符串用 作条件时就经常会不报错就而被当作假值,这常产生混淆。 Vim9 的类型检查更严格以防出错。需要使用条件时,例如用 :if 命令或 || 操作符 的时候,只接受类似于布尔的值: 真: true , v:true , 1 , `0 < 9` 假: false , v:false , 0 , `0 > 9` 注意 数值零为假,而数值一为真。这比绝大多数语言要宽一些。这是因为许多内建函数 会返回这些值。 如用你有任何类型的值并希望把它当作布尔型来使用,使用 !! 操作符: 真: !`!'text'`, !![99] , `!!{'x': 1}`, !!99 假: !!'' , !![] , !!{} JavaScript 这样的语言中,我们有这样方便的构造: GetName() || 'unknown' 不过,这和在使用条件的地方只接受布尔型有冲突。为此,引入 "??" 操作符: GetName() ?? 'unknown' 这里,你在显式地表达自己的意愿,按值本身去使用,而不是用作布尔值。这叫作 falsy-operator导入和导出 老式 Vim 脚本的一个问题是所有函数和变量缺省都是全局的。可以使它们局部于脚本, 但因而就不能为其它脚本所用。这就违背了软件包只能有选择性地导出项目,其它保持局 部的概念。 Vim9 脚本里支持和 JavaScript 导入导出非常相似的机制。这是已有的 :source 命令 的变种,且工作方式符合人们期待: - 和所有的缺省都是全局相反,除非导出,所有的都是局部于脚本的。 - 导入脚本时显式列出要导入的符号,避免以后新增功能时的名字冲突和可能的失败。 - 此机制允许编写大而长又有清晰 API 的脚本: 导出函数和类。 - 通过使用相对路径,同一包里导入的载入会更快,无需搜索许多目录。 - 导入一旦使用,会被缓冲而避免了再次载入。 - Vim 特定使事物局部于脚本的 "s:" 用法可以不需要了。 从老式脚本里执行 Vim9 脚本里,只能使用全局定义的项目,而不是导出的项目。考虑过 以下备选方案: - 所有导出项目成为局部于脚本的项目。这样没法控制什么项目有定义,有可能很快就会 有问题。 - 使用导出项目成为全局项目。缺点是这样就不能避免全局空间的名字冲突。 - 完全禁止 Vim9 脚本的执行,而必须使用 :import 。这样就很难用脚本进行测试,或 在命令行上执行脚本进行实验。 注意 也可在老式 Vim 脚本中使用 :import ,见上。 尽早编译函数 函数在实际调用时或使用 :defcompile 时才进行编译。为什么不尽早编译函数,以便 尽快报告语法和类型错误呢? 函数不能在遭遇时立刻编译,因为可能有之后定义的函数的前向引用。考虑定义函数 A、 B 和 C,其中 A 调用 B,B 调用 C,而 C 又调用 A。这里不可能通过对函数重新排列来 避免正向引用。 一个替代方案是先扫描整个文件以定位项目并判断其类型,这样就能找到正向引用,然后 再执行脚本并编译函数。这意味着脚本要解析两次,这会减慢速度,且脚本级别的某些条 件,如检查某特性是否支持等等,会难于使用。有过这方面的尝试,但结果是不能很好地 工作。 也可以在脚本最后编译所有函数。这样做的缺点是如果函数从未被调用,仍然要承受其编 译的开销。因为启动速度至关重要,绝大多数情况下最好延后处理,并在那时才报告语法 和类型错误。如果确实希望早报告错误,譬如测试期间, :defcompile 命令可作救济。 为什么不用嵌入式语言? Vim 支持 Perl、Python、Lua、Tcl 和一些其它语言的接口。但由于种种原因,这些从未 广泛使用过。Vim 9 设计时作了一个决定,降低这些接口的优先级,并集中在 Vim 脚本 上。 不过,插件作者可能对其它语言更加熟悉,想用已有的库或要提高性能。我们鼓励脚本作 者使用任何语言编程并作为外部工具运行,使用作业和通道通信。我们可以想办法使之更 加便捷。 使用外部工具也有其不足。一种替代方案是把工具转换为 Vim 脚本。要尽量减少翻译的 工作量,并且同时保持代码快速,需要支持工具使用的构造。因为绝大多数语言支持类, 缺少类的支持成为了 Vim 的一个问题。 Vim 支持通过为字典增加方法,支持过一种准面向对象的编程。如果小心一点这可以工 作,但这看来不像真正的类。更有甚者,因为字典使用的缘故,速度很慢。 Vim9 脚本对类的支持提供多数语法的类支持的 "最少公共功能"。它和 Java 的方式类似, 这也是最流行的编程语言了。 vim:tw=78:ts=8:noet:ft=help:norl: