1. 什么是 Vim9 脚本? Vim9-script
Vim 脚本随着时间推移日渐庞大,同时需要保持后向兼容。这意味着过去所做的错误决定
常常不能再改变,而和 Vi 的兼容性限制了可能的解决方案。执行颇慢,每行在每次执行
时都要先解析。
Vim9 脚本的主要目标是大幅提高性能。这是通过把命令编译为可有效执行的指令完成
的。可以期待执行速度有成十上百倍的提高。
一个次要目标是避免 Vim 特定的构造,而尽量和包括 JavaScript、TypeScript 和 Java
这样的主流编程语言接近。
只有不 100% 后向兼容才能达到所需的性能提高。例如,要使函数参数可通过 "a:" 字典
获取,会增加不少开销。其它一些差别,比如错误处理的方式,则更细微一些。
Vim9 脚本语法和语义用于:
- 使用 :def 命令定义的函数
- 首个命令是 vim9script 的脚本文件
- 上述上下文中定义的自动命令
- vim9cmd
命令修饰符前缀的命令
Vim9 脚本文件里使用 :function 时,仍然会用老式语法并使用最高的
scriptversion 。不过,这容易混淆,不鼓励。
Vim9 脚本和老式的 Vim 脚本可以混合。重写旧脚本并非必需,它们会继续工作。但可用
一些 :def 函数来编写要求快速的代码。
:vim9[cmd]
{cmd}
:vim9 :vim9cmd E1164
使用 Vim9 脚本语法和语义计算和执行 {cmd}
。在键入命令和老式脚本
或函数里有用。
:leg[acy] {cmd}
:leg :legacy E1189 E1234
使用老式脚本语法和语义计算和执行 {cmd}
。只在 Vim9 脚本或 :def
函数里有用。注意
因为使用老式表达式语法解析,{cmd}
不能使用局
部变量。
2. 和老式 Vim 脚本的差异 vim9-differences
总览
E1146
使用 Vim9 脚本和 :def 函数最常见区别的简要小结;细节见后:
- 注释以 # 开始而不是 ":
echo "hello" # 注释
- 很少需要反斜杠用来作续行符:
echo "hello "
.. yourName
.. ", how are you?"
- 很多地方需要空格以提高可读性。
- 赋值不用 :let E1126 ,变量用 :var 声明:
var count = 0
count += 3
- 常量可用 :final 和 :const 声明:
final matches = [] # 之后可再扩充列表
const names = ['Betty', 'Peter'] # 不能改变
- :final 不再能用作 :finally 的缩写。
- 变量和函数缺省局部于脚本。
- 函数声明要带参数类型和返回值类型:
def CallMe(count: number, message: string): bool
- 调用函数不用 :call :
writefile(['done'], 'file.txt')
- 不能用以下的旧式的 Ex 命令:
:Print
:append
:change
:d 后跟 'd' 或 'p'。
:insert
:k
:mode
:open
:s 只带标志位
:t
:xit
- 有些命令,特别是用于流控制的命令,不能缩短。例如, :throw 不能写成 :th 。
vim9-no-shorten
- 不可用花括号名字。
- 命令前的范围必须用冒号前缀:
:%s/this/that
- 不再可以用 "@r" 来执行寄存器,需要冒号前缀或使用 :exe :
:exe @a
- 除非特别指出,使用最高的 scriptversion 。
- 定义表达式映射时,在定义所在的脚本上下文里计算表达式。
# 开始的注释
老式 Vim 脚本以双引号开始脚本注释。Vim9 脚本则用 # 开始脚本注释。
# 声明
var count = 0 # 出现的次数
原因是双引号也是字符串的开始,在很多地方,尤其是表达式内部换行处,很难区分双引
号真正的意思,因为字符串和注释都可以后跟任意文本。为避免混淆,现在只识别 # 注
释。这和外壳脚本和 Python 程序完全相同。
Vi 里 # 是带行号列出文本的命令。Vim9 脚本可用 :number 代替。
:101 number
为提高可读性,命令和开始注释的 # 之间必须有一个空格:
var name = value # 注释
var name = value# 出错!
E1170
不要以 #{ 开始注释,它看来像老式的字典常数,有歧义时这会报错。#{{ 和 #{{{ 则可
以,它们用来启动折叠。
开始读取脚本文件时 vim 不知道是 vim9 脚本,直到找到 vim9script 命令为止。
在那之前,需要用老式注释:
" 老式注释
vim9script
# vim9 注释
这很难看,最好在第一行就放上 vim9script :
vim9script
# vim9 注释
老式 Vim 脚本中 # 也用于轮换文件名。Vim9 脚本里,要改用 %%。## 可改用 %%% (代
表所有参数)。
Vim9 函数
E1099
由 :def 定义的函数进行编译。执行成倍加快,通常有 10 到 100 倍的加速。
许多错误在编译时已经可以发现,而不用等到函数执行的时候。有严格的语法,以确保代
码易读易理解。
以下情形之一时进行编译:
- 函数第一次调用时
- 脚本中在函数定义后遇到 :defcompile 命令时
- 函数使用 :disassemble 时。
- 函数被已编译的函数调用或用作函数引用时 (为了检查参数和返回类型)
E1091 E1191
编译失败后,下次调用时不会再次尝试编译,而会报错: "E1091: Function is not
compiled: {name}
"。
遇到尚未创建的用户命令时,编译会失败。这种情况可用 execute() 在运行时调用该
命令。
def MyFunc()
execute('DefinedLater')
enddef
:def 不像 :function 那样有 "range"、"abort"、"dict" 或 "closure" 这样的选
项。 :def 函数总会在错误时中止 (除非命令使用了 :slient!
或错误在 :try 块里
捕获),不接受范围、不能是 "dict" 函数,而且总可以有闭包。
vim9-no-dict-function
以后会加入类,以替代 "字典函数" 机制。现在你需要显式传递入字典:
def DictFunc(self: dict<any>, arg: string)
echo self[arg]
enddef
var ad = {item: 'value', func: DictFunc}
ad.func(ad, 'item')
但可以调用老式字典函数:
func Legacy() dict
echo self.value
endfunc
def CallLegacy()
var d = {func: Legacy, value: 'text'}
d.func()
enddef
E1096 E1174 E1175
必须指定参数类型和返回类型。可用 "any" 类型,和老式函数一样,此时类型检查会在
运行时完成。
E1106
参数用不带 "a:" 的名字访问,就像其它语言一样。没有 "a:" 字典或 "a:000" 列表。
vim9-variable-arguments E1055 E1160 E1180
类似于 TypeScript,通过给定名字和列表类型的末项参数来定义可变参数。例如,数值
的列表:
def MyFunc(...itemlist: list<number>)
for item in itemlist
...
如果函数参数可选 (有缺省值),传递 v:none 作为参数会使用其缺省值。用于在使用
缺省值的参数之后需要指定其它参数的场合。例如:
def MyFunc(one = 'one', last = 'last')
...
enddef
MyFunc(v:none, 'LAST') # 首个参数使用缺省值 'one'
vim9-ignored-argument E1181
参数 "_" (下划线) 可用于忽略参数值。用于回调中不需要提供参数值但要给出一个参数
以匹配调用的场合。例如,使用 map() 时需要两个参数,键和值,要忽略键:
map(numberList, (_, v) => v * 2)
"_" 参数多次使用不会报错。也不需要指定类型。
函数和变量缺省局部于脚本
vim9-scopes
在 Vim9 脚本中,用 :function 或 :def 来指定脚本级别的新函数时,函数局部于
脚本,就像老式脚本里用了 "s:" 前缀那样。"s:" 前缀可选。要定义全局函数或变量,
必须使用 "g:" 前缀。脚本里要被别人导入和在自动载入脚本定义的函数,需要使用
"export",才能被别处使用。
def ThisFunction() # 局部于脚本
def s:ThisFunction() # 局部于脚本
export def Function() # 用于 import 和 import autoload
E1058 E1075
用 :function 或 :def 来指定 :def 函数内的嵌套函数时,如果没给出命名空
间,此嵌套函数局部于定义所在的代码块。不能用传递字符串参数的 function() 形式
访问,但函数引用本身可以:
def Outer()
def Inner()
echo 'inner'
enddef
var Fok = function(Inner) # 正确
var Fbad = function('Inner') # 不行
细节: 这是因为 "Inner" 实际会变成函数引用,指向一个名字内部生成的函数。
函数内不可以定义局部于脚本的函数。可以定义局部函数并赋值给局部于脚本的函数引用
(函数必须在脚本级别声明)。可以用 "g:" 前缀来定义全局函数。
引用函数时如果不带 "s:" 或 "g:" 前缀,Vim 会依次搜索函数:
- 在函数作用域内,在块作用域内
- 在脚本作用域内
导入的函数可根据 :import 命令给出的前缀找到。
因为使用局部于脚本的函数引用时可以不带 "s:",其名字必须以大写字母开头,即使用
了 "s:" 前缀也是如此。老式脚本里因为不能以 "funcref" 来进行引用,所以可用
"s:funcref"。而 Vim9 脚本里这种直接引用的形式是可以的,因此必须使用
"s:Funcref" 形式,以防其名和内建函数起冲突。
vim9-s-namespace E1268
Vim9 脚本级别不支持使用 "s:" 前缀。所有不带前缀的函数和变量都是局部于脚本的。
:def 函数里,"s:" 的使用取决于脚本: 老式脚本里的局部于脚本的变量和函数要用
"s:",而 Vim9 脚本里它们不用 "s:"。这和文件其余部分你能看到的用法匹配。
老式函数里必须使用 "s:",见前。不管脚本是 Vim9 还是老式的都是如此。
在所有情况下,函数必须先定义,然后才能使用。所谓使用,是指函数调用时,或
:defcompile 导致调用被编译时,或调用它的函数被编译时 (以确定返回类型)。
其结果是,无命名空间的函数和变量通常可在其定义所在或导入的脚本内找到。而全局函
数和变量可以在任何地方定义 (要找到需要点运气!用 :verbose 往往可以看到它上次
是在哪里设置的)。
E1102
全局函数还是可以在几乎任何时候定义和删除。vim9 脚本里,局部于脚本的函数在脚本
载入时定义一次,且不能自己删除或替代 (但脚本重载时是会的)。
编译函数时和遇到函数调用时如果函数 (还) 未定义,不触发 FuncUndefined 自动命
令。如果需要,可用自动载入函数,或调用老式函数,那里会触发 FuncUndefined 。
重载 Vim9 脚本时缺省清除函数或变量
vim9-reload E1149 E1150
再次载入同一老式 Vim 脚本时,不会删除任何东西,脚本里的命令会替代原有的变量和
函数,创建新的内容,而被删除的东西还会悬挂在那里。
再次载入同一 Vim9 脚本时,删除所有已有的局部于脚本的函数和变量,以便从干净的状
态开始。这可用于开发插件时新版本的实验。如果换了任何名字,不需要担心旧名字还存
在。
如果确实要保留项目,可用:
vim9script noclear
可在脚本中使用此命令,以便在脚本重载时,在某些情况下用 finish
命令放弃载入。
例如,如果设置了某个缓冲区局部选项为函数时,函数不需要再次定义:
vim9script noclear
setlocal completefunc=SomeFunc
if exists('*SomeFunc')
finish
endif
def SomeFunc()
....
用 :var、:final 和 :const 声明变量
vim9-declaration :var E1079
E1017 E1020 E1054 E1087 E1108 E1124
局部变量须用 :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 inner = 0
if cond
inner = 5
endif
echo inner
E1025 E1128
要有意识地从之后的代码中隐藏某个变量,可用代码块:
{
var temp = 'temp'
...
}
echo temp # 出错!
用户命令里这特别有用:
command -range Rename {
var save = @a
@a = 'some expression'
echo 'do something with ' .. @a
@a = save
}
自动命令也是:
au BufWritePre *.go {
var save = winsaveview()
silent! exe ':%! some formatting command'
winrestview(save)
}
不过,用 :def 函数可能更好些。
E1022 E1103 E1130 E1131 E1133
E1134
变量声明时如果带类型但不带初始块,其值初始为假值 (布尔型)、空 (字符串、列表、
字典,等等) 或零 (数值、any,等等)。特别重要的是,使用 "any" 类型时,缺省值为
数值零。例如声明列表时,可以加入项目:
var myList: list<number>
myList->add(7)
初始化变量为 null 值,如 null_list 时,和不初始化变量是不同的。以下例子会抛
出错误:
var myList = null_list
myList->add(7) # E1130: Cannot add to null list
E1016 E1052 E1066
Vim9 脚本中不能用 :let 。已有的变量可直接赋值,不需要任何命令。全局、窗口、标
签页、缓冲区和 Vim 变量也是一样,因为它们并非真正通过声明产生,但它们也可用
:unlet 删除。
E1065
不能用 :va
来声明变量,必须使用全名 :var 来书写。这是为了方便阅读。
E1178
:lockvar 不能用于局部变量。可用 :const 和 :final 代替。
exists() 和 exists_compiled() 函数不能用局部变量或参数。
E1006 E1041 E1167 E1168 E1213
变量、函数和函数参数不能隐藏在同一脚本文件里之前定义或导入的变量和函数。
变量可以隐藏 Ex 命令,如有需要请给变量换名。
全局变量必须带上 "g:" 前缀,即使脚本级别的也是如此。
vim9script
var script_local = 'text'
g:global = 'value'
var Funcref = g:ThatFunction
全局函数必须带上 "g:" 前缀:
vim9script
def g:GlobalFunc(): string
return 'text'
enddef
echo g:GlobalFunc()
自动载入函数不需要 "g:" 前缀。
vim9-function-defined-later
尽管可以不带 "g:" 前缀调用全局函数,编译时它们必须已存在。加上 "g:" 前缀后,函
数可以在其后再定义。例如:
def CallPluginFunc()
if exists('g:loaded_plugin')
g:PluginFunc()
endif
enddef
但如果像下面这么做,编译时会报错 "PluginFunc" 不存在,即使 "g:loaded_plugin"
不存在:
def CallPluginFunc()
if exists('g:loaded_plugin')
PluginFunc() # 报错,函数找不到
endif
enddef
可用 exists_compiled() 来避免此错误,但函数可能也不会被调用了,即使
"g:loaded_plugin" 之后被定义了也不会:
def CallPluginFunc()
if exists_compiled('g:loaded_plugin')
PluginFunc() # Function 可能永远不会被调用
endif
enddef
因为 `&opt = value` 现在用来给选项 "opt" 赋值,所以不再能用 ":&" 来重复
:substitute 命令了。
vim9-unpack-ignore
解包赋值里,下划线可用于忽略列表项目,类似于函数参数的忽略方法:
[a, _, c] = theList
要忽略其余项目:
[a, b; _] = longList
E1163 E1080
可以使用解包记法,一次声明多于一个变量。每个变量可给出类型或从值推断:
var [v1: number, v2] = GetValues()
仅当有带值列表的时候才建议使用此记法,一行声明一个变量更易阅读和修改。
常量
vim9-const vim9-final
常量如何工作因语言而异。有些语言把不能重新赋其它值的变量当作常量。JavaScript
是一个例子。其它语言则会使值也不可更改,因此如果常量使用了列表,列表本身也不可
改变。Vim9 可以兼有两者。
E1021 E1307
可用 :const 使变量及其值为常量。可用于复合结构以确保其不会被修改。示例:
const myList = [1, 2]
myList = [3, 4] # 出错!
myList[0] = 9 # 出错!
myList->add(3) # 出错!
:final E1125
可用 :final 使变量为常量,而值仍然可更改。Java 用户对此很熟悉。例如:
final myList = [1, 2]
myList = [3, 4] # 出错!
myList[0] = 9 # 正确
myList->add(3) # 正确
常量写入全大写 ALL_CAPS 是惯例,但不必如此。
常量限制只适用于值本身,而不是其引用的值。
final females = ["Mary"]
const NAMES = [["John", "Peter"], females]
NAMES[0] = ["Jack"] # 出错!
NAMES[0][0] = "Jack" # 出错!
NAMES[1] = ["Emma"] # 出错!
NAMES[1][0] = "Emma" # 正确,现在 females[0] == "Emma"
省略 :call 和 :eval
E1190
可直接调用函数而无需 :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(模式 (替代 (
如果表达式以 "!" 开始,解读为外壳命令,而不是条件取反。因而,以下是外壳命令:
!shellCommand->something
要用 "!" 进行取反,把表达式放在括号里:
(!expression)->Method()
注意
尽管变量在使用前必须先定义,函数在定义前就可以调用。为了允许函数间能相互
依赖,这是有必要的。但因此效率会稍稍低些,因为函数需要依名查找。且函数名的拼写
错误只有在函数被调用时才能发现。
省略 function()
表达式中,用户定义函数可用作函数引用,而无需通过 function() 。这时会检查参数
类型和返回类型。函数必须已经有定义。
var Funcref = MyFunction
而使用 function() 时,返回类型是 "func",一个可带任何数量参数和任何返回类型
(包括 void) 的函数。如果 function() 的参数用上引号,函数可以延后定义。
匿名函数使用 => 而不是 ->
vim9-lambda
在老式脚本里,"->" 既用于函数调用,也用于匿名函数,这里可能产生混淆。另外,找
到 "{" 时解析器需要知道它是匿名函数还是字典的开始,现在由于参数类型的使用,这
里的情况更复杂。
为避免这些问题,Vim9 脚本的匿名函数使用不同的语法,这和 JavaScript 类似:
var Lambda = (arg) => expression
var Lambda = (arg): type => expression
E1157
匿名函数在直到 "=>" 为止的参数里不允许换行 (这样 Vim 才能识别带括号的表达式和
匿名函数参数的区别)。这样可以:
filter(list, (k, v) =>
v > 0)
这样不可以:
filter(list, (k, v)
=> v > 0)
这样也不可以:
filter(list, (k,
v) => v > 0)
但可用反斜杠在解析发生前,把行进行连接:
filter(list, (k,
\ v)
\ => v > 0)
vim9-lambda-arguments E1172
在老式脚本里,匿名函数调用时可带任意多个额外参数,没办法警告
不要这么做。在
Vim9 脚本里参数数目必须匹配。如果要接受任何参数,或任何额外参数,可用 "..._",
这使函数接受 vim9-variable-arguments 。例如:
var Callback = (..._) => 'anything'
echo Callback(1, 2, 3) # 显示 "anything"
inline-function E1171
此外,匿名函数可以包含 {}
包围的多个语句:
var Lambda = (arg) => {
g:was_called = 'yes'
return expression
}
比如可用来实现计数器:
var count = 0
var timer = timer_start(500, (_) => {
count += 1
echom 'Handler called ' .. count
}, {repeat: 3})
结尾的 "}" 必须位于行首。后面可跟其它字符,例如:
var d = mapnew(dict, (k, v): string => {
return 'value'
})
"{" 之后不能跟随命令,那里只可以有注释。
command-block E1026
定义用户命令时也可用代码块。在代码块内使用 Vim9 语法。
如果语句包含字典,其结束花括号不能写在行首。否则,被解析为代码块的结束。下面这
样不行:
command NewCommand {
g:mydict = {
'key': 'value',
} # 错误: 会识别为代码块的结束
}
把 '}' 放在最后项目之后可以解决这个问题:
command NewCommand {
g:mydict = {
'key': 'value' }
}
理据: "}" 不能在命令之后出现,因为需要对命令进行解析才能找到它。为了一致起见,
"{" 之后也不能跟随命令。不幸的是,这就意味着不接受 "() => { command }",必须
使用换行。
vim9-curly
为了不让字典常量的 "{" 被识别为语句块,把它包围在括号里:
var Lambda = (arg) => ({key: 42})
和命令块的开始有歧义的话也是如此:
({
key: value
})->method()
自动续行
vim9-line-continuation E1097
许多情况下,表达式显然会在下一行继续。这些情况不需要在行前加入反斜杠 (见
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
注意
这意味着 here 文档里首行不能以竖线开始:
var lines =<< trim END
| 这不行
END
要么用一空行开始,要么不要用 here 文档。或者暂时在 'cpoptions' 里加入 "C" 标志
位:
set cpo+=C
var lines =<< trim END
| 这样可以
END
set cpo-=C
如果 here 文档在函数内出现,'cpoptions' 必须在 :def 前设置,在 :enddef 后恢
复。
在续行还需要反斜杠的地方,如断开长 Ex 命令时,注释可用 '#\ ' 开始:
syn region Text
\ start='foo'
#\ 注释
\ end='bar'
就像在老式脚本里可用 '"\ ' 一样。即使续行不需要反斜杠和以竖线开始行的情况,也
要如此:
au CursorHold * echom 'BEFORE bar'
#\ 一些注释
| echom 'AFTER bar'
E1050
要识别出现在行首的操作符,需要在范围之前加上冒号。要把 "start" 加上 "print":
var result = start
+ print
相当于:
var result = start + print
而要赋值 "start" 并显示一行:
var result = start
:+ print
范围后必须跟 Ex 命令。不带冒号时不用 :call 就可以调用函数,但在范围后需要:
MyFunc()
:% call MyFunc()
注意
+cmd 参数不需要冒号:
edit +6 fname
函数声明中也可以在参数间换行:
def MyFunc(
text: string,
separator = '-'
): string
因为续行不容易识别,命令的解析必须更严格。例如,因为首行有错,下面的第二行会看
作一个单独的命令:
popup_create(some invalid expression, {
exit_cb: Func})
现在,"exit_cb: Func})" 实际是一个合法的命令: 保存任何改动到文件 "_cb: Func})"
并退出。为了避免此种错误,Vim9 脚本中在多数命令名和参数间必须有空白。 E1144
不过,不能识别作为命令参数的命令里的续行。例如 "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]
- 有些情况 Vim 解析命令有困难,尤其是一个命令用作另一个命令的参数的时候,如
:windo 。这些情况续行必须使用反斜杠。
空白
E1004 E1068 E1069 E1074 E1127 E1202
Vim9 脚本强制空白的正确使用。以下不再允许:
var name=234 # 出错!
var name= 234 # 出错!
var name =234 # 出错!
"=" 前后必须有空白:
var name = 234 # 正确
命令之后开始注释的 # 之前必须有空白字符:
var name = 234# 出错!
var name = 234 # 正确
多数操作符需要用空白包围。
子列表 (切片) 表达式中的 ":" 两边需要空白,出现在开始和结束处的除外:
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 # 正确
)
E1205
:set 命令在选项名和后续的 "&"、"!"、"<"、"="、"+="、"-=" 或 "^=" 之间不能有
空白。
没有花括号扩展
不能使用 curly-braces-names 。
不忽略命令修饰符
E1176
不接受命令修饰符的命令如果用上命令修饰符,会报错。
E1082
另外,使用了命令修饰符但后面不跟命令,现在也是错误。
字典常量
vim9-literal-dict E1014
传统上 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} # 空键
E1139
如果键需要表达式,可用方括号,就像 JavaScript 那样:
var dict = {["key" .. nr]: value}
键类型可以是字符串、数值、布尔型或者浮点数。其它类型会报错。不带 [] 时直接用作
字符串值,保留引导的零。而用 [] 的表达式会先经过计算再转换为字符串。这样引导的
零就会被丢弃:
var dict = {000123: 'without', [000456]: 'with'}
echo dict
{'456': 'with', '000123': 'without'}
浮点数只能在 [] 里工作,因为如果不是的话,句号不被接受:
var dict = {[00.013]: 'float'}
echo dict
{'0.013': 'float'}
没有 :xit、:t、:k、:append、:change 或 :insert
E1100
这些命令很容易会和局部变量名混淆。
:x 或 :xit 可用 :exit 替代。
:t 可用 :copy 替代。
:k 可用 :mark 替代。
比较符
字符串的比较符不考虑 'ignorecase' 选项。
因此 "=~" 和 "=~#" 一样。
"is" 和 "isnot" ( expr-is 和 expr-isnot ) 用于字符串时,现在总是返回假值。老
式脚本里它们只是比较字符串内容,而在 Vim9 脚本里它们比较是否同一,因为字符串
使用时会先复制,因此两个字符串永不会等价 (有一天字符串不是复制而采用引用计数,
这可能会改变)。
错误后中断
老式脚本里,遇到错误时,Vim 会继续执行后续行。这会引起一长串错误,需要 CTRL-C
才能停止。Vim9 脚本的命令执行会在第一个错误处停止。例如:
vim9script
var x = 不-存-在
echo '不会执行'
For 循环
E1254
循环变量不能事先声明:
var i = 1
for i in [1, 2, 3] # 出错!
但可用全局变量:
g:i = 1
for g:i in [1, 2, 3]
echo g:i
endfor
老式 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
一般而言,不应改变正在遍历的列表。如果需要的话,先建立备份。
遍历列表的列表时,可以修改内层的列表。循环变量是 "final",它不能改变,但它的值
可以改变。
E1306
循环深度,即 :for 和 :while 循环加起来,不能超过 10 层。
条件和表达式
vim9-boolean
多数情况下,条件和表达式就像其它语言里那样的用法。有些值和老式 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 null_blob null_channel
null_dict null_function null_job null_list
null_partial null_string E1034
Vim9 脚本可以使用如下预定义值:
true
false
null
null_blob
null_channel
null_dict
null_function
null_job
null_list
null_partial
null_string
true 等价于 v:true 、 false 等价于 v:false 而 null 等价于 v:null 。
null 类型为 "special",而其它的 "null_" 值可根据其名推定类型。null 值和空值
处理方式通常相同,但不绝对。这些值可用于清除局部于脚本的变量,因为它们不能用
:unlet 删除。例如:
var theJob = job_start(...)
# 让作业自己做事
theJob = null_job
这些值也可用于参数的缺省值:
def MyFunc(b: blob = null_blob)
if b == null_blob
# 没给出 b 参数
可以把 null 和任何值比较,不会给出类型错误。但把 null 和数值、浮点或布尔型
比较总返回 false 。这和老式脚本不同,那里把 null 和零或 false 比较会返回
true 。
转换布尔型为字符串时使用 false 和 true ,而不像老式脚本那样用 v:false 和
v:true 。 v:none 没有对应的 none
替代,在其他语言中它没有类似的结构。
字符串用 [idx]
索引 或 [idx : idx] 取切片时使用的是字符索引而非字节索引。组合
字符包含在内。例如:
echo 'bár'[1]
在老式脚本里,这会得到字符 0xc3 (一个非法字节),在 Vim9 脚本里这会得到字符串
'á'。
负索引从结尾处开始计算,"[-1]" 代表末字符。要排除末字符,用 slice() 。
如果要组合字符分别计算,可用 strcharpart() 。
索引超出范围时,返回空字符串。
老式脚本中,接受 "++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 # :global 命令
要避免 :global 或 :substitute 命令和表达式或赋值相混淆,当这些命令缩写为单
字母时,不能使用若干分隔符: ':'、'-' 和 '.'。
g:pattern:cmd # 非法命令 - 出错
s:pattern:repl # 非法命令 - 出错
g-pattern-cmd # 非法命令 - 出错
s-pattern-repl # 非法命令 - 出错
g.pattern.cmd # 非法命令 - 出错
s.pattern.repl # 非法命令 - 出错
另外,命令和分隔符之间也不能有空格:
g /pattern/cmd # 非法命令 - 出错
s /pattern/repl # 非法命令 - 出错
: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
块里,带上计算值为假值的常量表达式:
def Maybe()
if has('feature')
use-feature
endif
enddef
为此也可用 exists_compiled() 函数。
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' 选项值。
以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的:
https://github.com/lacygoill/wiki/blob/master/vim/vim9.md
:++ :--
新增了 ++ 和 -- 命令,类似于加一或减一:
++var
var += 1
--var
var -= 1
还不支持表达式中使用 ++var 或 --var。
3. 新风格函数 fast-functions
:def E1028
:def[!] {name}
([arguments]
)[: {return-type}
]
定义名为 {name}
的新函数。在下面的行给出函数体,直到配
对的 :enddef 为止。 E1073
E1011
{name}
必须少于 100 字节长。
E1003 E1027 E1056 E1059
:return 所用的值类型必须匹配 {return-type}
。
{return-type}
如果省略或为 "void",不期待函数返回任何
值。
E1077 E1123
{arguments}
是零或多个参数声明的序列。有以下三种形式:
{name}
: {type}
{name}
= {value}
{name}
: {type}
= {value}
第一种形式是必选参数,调用者必须提供。
第二三种形式是可选参数。如果调用者省略参数,使用
{value}
值。
此函数在实际调用、使用 :disassemble 或 :defcompile
时被编译为指令序列。那时才会报告语法和类型错误。
在 :def 或 :function 里可以嵌套另一个 :def ,最多
可达 50 层。
E1117
[!] 的用法同 :function 。注意
Vim9 脚本里局部于脚本的
函数不能删除或在之后重定义。只能通过重新载入相同的脚本
来移除。
:enddef E1057 E1152 E1173
:enddef 结束 :def 定义的函数。必须单独起一行。
以下 wiki 可能对您有用。这是 Vim9 脚本的一位早期试用者编写的:
https://github.com/lacygoill/wiki/blob/master/vim/vim9.md
如果函数定义所在的是 Vim9 脚本,可不经 "s:" 前缀直接访问局部于脚本的变量。这些
变量必须在函数编译之前定义。如果函数定义是在老式脚本里,那么编译时不存在的脚本
局部变量必须通过 "s:" 前缀才能访问。
E1269
Vim9 脚本里的脚本局部变量必须在脚本级别声明。不能在函数里创建,包括老式函数
也不行。
:defc :defcompile
:defc[ompile] 编译尚未编译的在当前脚本中定义的函数。
编译中如有任何错误,会报错。
:defc[ompile] {func}
:defc[ompile] debug {func}
:defc[ompile] profile {func}
编译函数 {func}
,如有需要。"debug" 和 "profile" 指定编
译模式。
编译中如有任何错误,会报错。
:disa :disassemble
:disa[ssemble] {func}
显示 {func}
生成的指令序列。用于调试和测试。 E1061
注意
{func}
的命令行补全可通过前加 "s:" 来查找局部于脚
本的函数。
:disa[ssemble] profile {func}
类似于 :disassemble 但指令用于刨视。
:disa[ssemble] debug {func}
类似于 :disassemble 但指令用于调试。
局限
计算字符串表达式时,局部变量不可见。例如:
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
对于未编译的命令,如 :edit ,可用反引号扩展,这时局部作用域可用。例如:
def Replace()
var fname = 'blah.txt'
edit `=fname`
enddef
同一循环里定义的所有闭包会分享相同的上下文。例如:
var flist: list<func>
for i in range(5)
var inloop = i
flist[i] = () => inloop
endfor
echo range(5)->map((i, _) => flist[i]())
# 返回: [4, 4, 4, 4, 4]
E1271
闭包必须在其定义所在的上下文里进行编译,以便找到此上下文里的变量。大多数情况下
没有问题,但函数在编译后用 :breakadd 标记进行调试时除外。请在编译外层函数前就
定义好断点。
"inloop" 变量只存在一次,所有放入列表的闭包都会指向相同的实例,最后该值为 4。
这样是高效率的,即使循环很多次也是。如果你确实要为每个闭包提供单独的上下文,调
用函数来进行定义:
def GetClosure(i: number): func
var infunc = i
return () => infunc
enddef
var flist: list<func>
for i in range(5)
flist[i] = GetClosure(i)
endfor
echo range(5)->map((i, _) => flist[i]())
# 返回: [0, 1, 2, 3, 4]
有些情况下,特别是从老式上下文里调用 Vim9 闭包时,计算会失败。 E1248
注意
在脚本级别,循环变量在循环之后会变非法,这包括在其后会调用的闭包中的引
用,如用计时器的时候。以下会出错 E1302 :
for n in range(4)
timer_start(500 * n, (_) => {
echowin n
})
endfor
需要定义块并在其中定义变量,并在闭包中使用该变量:
for n in range(4)
{
var nr = n
timer_start(500 * n, (_) => {
echowin nr
})
}
endfor
定时器中 :echowindow 很有用,它使得信息在弹出中出现,触发时不会干扰用户正在
做的事情。
把老式函数转换为 Vim9
convert_legacy_function_to_vim9
这里包括了把老式函数转换为 Vim9 函数要做的主要改变:
- 把 func
或 function
改为 def
。
- 把 endfunc
或 endfunction
改为 enddef
。
- 为函数参数加上类型。
- 如果函数有返回,加上返回类型。
- 注释以 # 开始,而不是 "。
例如,老式函数:
func MyFunc(text)
" 函数体
endfunc
变成了:
def MyFunc(text: string): number
# 函数体
enddef
- 删除参数使用的 "a:"。例如:
return len(a:text)
变成了:
return len(text)
- 把用于声明变量的 let
改为 var
。
- 删除用于给变量赋值的 let
。这包括已经声明过的局部变量和 b: w: g: 和 t: 变
量。
例如,老式函数:
let lnum = 1
let lnum += 3
let b:result = 42
变成了:
var lnum = 1
lnum += 3
b:result = 42
- 表达式中有必要时添加空白。
- 把用于连接的 "." 改为 ".."。
例如,老式函数:
echo line(1).line(2)
变成了:
echo line(1) .. line(2)
- 续行不总是需要反斜杠:
echo ['one',
\ 'two',
\ 'three'
\ ]
变成了:
echo ['one',
'two',
'three'
]
表达式选项调用函数
expr-option-function
若干选项如 'foldexpr' 的值是一个表达式,先经过计算再得到值。这里的计算会有相当
负担。一个尽量减少负担的方法,同时也使选项值保持简单,是先定义一个编译过的函
数,然后设置选项来无参数地调用该函数。例如:
vim9script
def MyFoldFunc(): any
... compute fold level for line v:lnum
return level
enddef
set foldexpr=s:MyFoldFunc()
4. 类型 vim9-types
E1008 E1009 E1010 E1012
E1013 E1029 E1030
支持以下内建类型:
bool
number
float
string
blob
list<{type}
>
dict<{type}
>
job
channel
func
func: {type}
func({type}
, ...)
func({type}
, ...): {type}
void
尚未支持:
tuple<a: {type}
, b: {type}
, ...>
以下类型可用于声明,但不会有简单值真有 "void" 类型。试图使用 void (如没有返回
值的函数) 会报错。 E1031 E1186 。
没有 array 类型,用 list<{type}
> 代替。list 常量使用了有效实现,以避免许多小片
内存的分配。
E1005 E1007
可以用具体程度不同的方式来声明偏函数和函数:
func 任何类型的函数引用,不检查参数和返回值类型
func: void 任何数量和类型的参数,无返回值
func: {type}
任何数量和类型的参数,特定返回类型
func() 无参数的函数,无返回值
func(): void 同上
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 E1104
为避免此种情况,可用类型转换:
var l: list<number> = [1, <number>g:two]
编译后的代码只会检查 "g:two" 是否为数值,如果不是会报错。这叫类型转换。
类型转换的语法是: "<" {type}
">"。"<" 之后或 ">" 之前不能有空格 (为避免和小于
号大于号操作符混淆)。
其语义为,有必要时执行运行时类型检查。这里值并不改变。如果要改变类型,如转换为
字符串型,可用 string() 函数。或用 str2nr() 把字符串转换为数值。
如果给出类型但与期待不符,报错 E1272 。
类型推论
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>
函数引用的公共类型,如果参数数目不尽相同,用 "(...)" 来指明参数数目不确定。例
如:
def Foo(x: bool)
enddef
def Bar(x: bool, y: bool)
enddef
var funclist = [Foo, Bar]
echo funclist->typename()
返回:
list<func(...)>
Vim9 脚本里局部于脚本的变量会进行类型检查,即使对在老式函数里声明的变量也会。
项目类型声明后,和列表或字典捆绑。其后,试图改变项目类型的表达式会报错:
var ll: list<number> = [1, 2, 3]
ll->extend(['x']) # 报错,'x' 不是 number
如果项目类型没有声明,则之后允许改变类型:
[1, 2, 3]->extend(['x']) # 返回: [1, 2, 3, 'x']
对变量声明而言,推导所得的类型依然是重要的:
var ll = [1, 2, 3]
ll->extend(['x']) # 报错,'x' 不是 number
这是因为看来这是数值的列表,因此它和下面的声明等价:
var ll: list<number> = [1, 2, 3]
如果要接纳度更高的列表,需要如此声明类型:
var ll: list<any> = [1, 2, 3]
ll->extend(['x']) # 正确
更严格的类型检查
type-checking
在老式 Vim 脚本中,在期待数值的地方,字符串会自动转换为数值。这方便实际为数值
的情形如 "123",但对不以数值开始的字符串会出现奇怪的问题 (且不报错)。这通常会
导致很难发现的漏洞。例如:
echo 123 == '123'
1
如果不小心多了一个空格:
echo 123 == ' 123'
0
E1206 E1210 E1212
Vim9 脚本中这里会更严格。只要使用的值匹配期待的类型,绝大多数地方和以前一样工
作。有时会报错,破坏后向兼容性。例如:
- 期待布尔型时使用非 0 或 1 的数值。 E1023
- 设置数值选项时使用字符串值。
- 期待字符串时使用数值。 E1024 E1105
一个后果是,如果项目类型已声明,传递给 map() 的列表或字典里的项目类型不能改
变。Vim9 脚本里以下会报错:
var mylist: list<number> = [1, 2, 3]
echo map(mylist, (i, v) => 'item ' .. i)
E1012: Type mismatch; expected number but got string in map()
可用 mapnew() 代替。创建新列表:
var mylist: list<number> = [1, 2, 3]
echo mapnew(mylist, (i, v) => 'item ' .. i)
['item 0', 'item 1', 'item 2']
如果项目类型未声明或确定为 "any",可以变换为更专门的类型。例如,使用混合类型的
列表可以变为数值的列表:
var mylist = [1, 2.0, '3']
# typename(mylist) == "list<any>"
map(mylist, (i, v) => 'item ' .. i)
# typename(mylist) == "list<string>",没有出错
直接使用列表常量和通过变量声明有一个细微的区别。因为类型推导的缘故,列表常量初
始化变量的同时也设置了声明的类型:
var mylist = [1, 2, 3]
# typename(mylist) == "list<number>"
echo map(mylist, (i, v) => 'item ' .. i) # 出错!
如果直接使用列表变量,类型是未声明的,可以修改:
echo map([1, 2, 3], (i, v) => 'item ' .. i) # 正确
背后的原因是类型声明后,在列表被传递和修改时,其声明必须保持不变。这样你才能依
靠类型来匹配已声明的类型。对常量而言这非必要。
E1158
extend() 也是如此,可用 extendnew() 代替,还有 flatten() 也是,可用
flattennew() 代替。因为 flatten() 的目的就是改变类型,它不能用于 Vim9 脚
本。
E1211 E1217 E1218 E1219 E1220 E1221
E1222 E1223 E1224 E1225 E1226 E1227
E1228 E1238 E1250 E1251 E1252 E1253
E1256 E1297 E1298 E1301
大多数内建函数检查类型,以便查找错误。
5. 命名风格、导入和导出
vim9script vim9-export vim9-import
可编写 Vim9 脚本,被其它脚本导入。具体地说,有意地导出若干项目,使之被其它脚本
所用。导出项目的脚本被其它脚本导入,后者就可以使用那些导出项目了。导出脚本的其
余项目依然局部于脚本而不能被导入脚本访问。
此机制的存在是为了编写被其它脚本执行 (导入) 的脚本,同时保证其它脚本只能访问你
要它们访问的项目。这也避免了使用全局命名空间,后者有名字冲突的风险。比如当有两
个功能相近的插件的时候。
可显式使用全局命名空间来作弊。这只应该用于真正需要全局的情况。
命名空间
vim9-namespace
为了识别可导入的文件,文件出现的第一个语句必须是 vim9script 语句 ( vim9-mix
介绍一个例外)。它告知 Vim 脚本在自己的命名空间而不是全局命名空间里被解释。如果
文件这样开始:
vim9script
var myvar = 'yes'
那么 "myvar" 只存在于此文件中。如果没有 vim9script ,其它脚本和函数可用
g:myvar
进行访问。
E1101
文件级别的变量和老式脚本里的局部 "s:" 变量非常类似,但省略 "s:"。而且不能被删
除。
和以前一样,Vim9 脚本中仍然可用全局 "g:" 命名空间。还有 "w:"、"b:" 和 "t:" 命
名空间。它们的共同点是变量不声明,没有特定类型且可以删除。 E1304
:vim9script 一个副作用是 'cpoptions' 选项设为 Vim 缺省值,类似于:
:set cpo&vim
其中一个效果是 line-continuation 总是打开。
脚本结束时则会恢复 'cpoptions' 原先的值,在脚本里增加或删除的标志位也会从原先
的值里进行相应的增删,以达到相同效果。标志位的顺序可能会变化。
这不适用于启动时执行的 vimrc 文件。
vim9-mix
有一个办法可以在一个脚本文件里同时使用老式和 Vim9 语法:
" 这里可以提供注释
if !has('vim9script')
" 这里是老式脚本命令
finish
endif
vim9script
# 这里是 Vim9 脚本命令
这样就可以编写脚本,在可能的情况下利用 Vim9 脚本的语法,但也支持旧的 Vim 版
本。
只能有两种方法工作:
1. "if" 语句计算为假,跳过直到 endif
部分为止的命令, vim9script 是第一个真
正执行的命令。
2. "if" 语句计算为真,执行直到 endif
部分为止的命令,而 finish
在到达
vim9script 前退出。
导出
:export :exp
可以这样导出项目:
export const EXPORTED_CONST = 1234
export var someValue = ...
export final someValue = ...
export const someValue = ...
export def MyFunc() ...
export class MyClass ...
export interface MyClass ...
E1043 E1044
就像这里暗示的,只能导出常数、变量、 :def 函数和类。{尚未实现: 类、界面}
E1042
:export 只能用于 Vim9 脚本的脚本级别。
导入
:import :imp E1094 E1047 E1262
E1048 E1049 E1053 E1071 E1088 E1236
导出项目可在另一个脚本里被导入。导入语法有两种形式。简单形式:
import {filename}
{filename}
是计算结果为字符串的表达式。此形式里的文件名必须以 ".vim" 结尾,
".vim" 之前的部分会成为该命名空间的脚本局部名。例如:
import "myscript.vim"
这使得 "myscript.vim" 里的所有导出项目都可用 "myscript.item" 的形式访问。
:import-as E1257 E1261
如果名字太长或有二义性,以下形式可用于指定别名:
import {longfilename} as {name}
这里 {name}
成为代表被导入命名空间的特定的脚本局部名。因此 {name}
必须由字母、
数位和 "_" 组成,就像 internal-variables 那样。{longfilename}
表达式的计算结
果必须为文件名。例如:
import "thatscript.vim.v2" as that
E1060 E1258 E1259 E1260
这样你就可用 "that.item" 等等。可以自由选择名字 "that"。请用可以识别导入脚本的
名字。避免命令名、命令修饰符和内建函数名,因为你选的名字会使那些被隐藏。最好不
要用大写字母开头的名字,因为那样也可能会隐藏全局用户命令和函数。此外,也不能和
脚本其它项目,如函数或变量名,重名。
如果不想要带句号的名字,可为函数提供本地引用:
var LongFunc = that.LongFuncName
对常量这样也可以:
const MAXLEN = that.MAX_LEN_OF_NAME
对变量不可以,因为值会被复制一次,而修改变量会改变备份,而不是原先的变量。所
以,只能用带句号的全名。
:import 不能在函数内使用。导入项目应该出现在脚本级别,且只能导入一次。
import
之后的脚本名可以是:
- 相对路径,以 "." 或 ".." 开始。这会找到相对于脚本文件自身所在位置的文件。可
用于把大型插件分割为几个文件。
- 绝对路径,Unix 上以 "/" 开始,或 MS-Windows 上以 "D:/" 开始。很少用到。
- 既非相对也不是绝对的路径。会在 'runtimepath' 项目的 "import" 子目录中寻找。
名字通常较长且唯一,以避免载入错误的文件。
- 注意
不使用 "after/import"。
如果名字不以 ".vim" 结尾,必须使用 "as name" 的形式。
vim9 脚本文件一旦导入,结果会被缓冲,下次导入相同脚本时,会使用缓冲而不会再次
读取文件。
不能导入同一脚本两次,即使用两个不同的 "as" 名字也不行。
使用导入名时,句号和项目名必须在同一行,中间不能断行:
echo that.
name # 出错!
echo that
.name # 出错!
import-map
vim9 脚本从另一个脚本导入函数时,用 <SID> 前缀可在映射里引用导入的函数:
noremap <silent> ,a :call <SID>name.Function()<CR>
映射在定义时, "<SID>
name." 会被替换为 <SNR>
和导入脚本的脚本 ID。
更简单的方案是用 <ScriptCmd> :
noremap ,a <ScriptCmd>name.Function()<CR>
注意
这只适用于函数,不适用于变量。
import-legacy legacy-import
:import 也可用于老式 Vim 脚本。即使未给出 "s:" 前缀,导入命名空间仍然是脚本
局部的。例如:
import "myfile.vim"
call s:myfile.MyFunc()
使用 "as name" 形式:
import "otherfile.vim9script" as that
call s:that.OtherFunc()
不过,不能直接解析命名空间本身:
import "that.vim"
echo s:that
" ERROR: E1060: Expected dot after name: s:that
这也影响老式映射上下文里 <SID> 的使用。因为 <SID> 只是函数的合法前缀而 不
是命名空间的,你不能用之在局部于脚本的命名空间里局限函数的作用域。因此,不要用
<SID> 函数前缀,而应用 <ScriptCmd> 。例如:
noremap ,a <ScriptCmd>:call s:that.OtherFunc()<CR>
:import-cycle
import
命令在见到时就会执行。如果脚本 A 导入脚本 B,而 B 已经 (直接或间接地)
导入了 A,会跳过前者。A 在 "import B" 之后的项目此时尚处于未处理和未定义状态。
所以,循环导入可以存在且不会直接报错,但 A 在 "import B" 之后的项目可能会因为
未定义而出错。这不适用于自动载入导入,见下一小节。
在自动载入脚本中导入
vim9-autoload import-autoload
要有最佳的启动速度,应该尽量延迟脚本的载入直到实际需要为止。建议使用自动载入机
制:
E1264
1. 在插件中定义指向自动载入脚本导入的项目的用户命令、函数和/或映射。
import autoload 'for/search.vim'
command -nargs=1 SearchForStuff search.Stuff(<f-args>)
应放在 .../plugin/anyname.vim。 "anyname.vim" 可自由选择其名字。
现在就可用 "SearchForStff" 命令了。
:import 的 "autoload" 参数意味着直到其中项目被实际用到,脚本暂缓载入。这
类脚本可在 'runtimepath' 的 "autoload" 目录下找到,而不是 "import" 目录。此
处,也可用相对或绝对名字,见下。
2. 主要代码放在自动载入脚本。
vim9script
export def Stuff(arg: string)
...
放在 .../autoload/for/search.vim 里。
"search.vim" 脚本放在 "/autoload/for/" 目录下的效果是 "for#search#" 会加在
每个导出的项目之前。前缀由文件名获取,就像老式自动载入脚本里你会手动做的一
样。因而,上例中的导出函数可由 "for#search#Stuff" 获取,但通常会用
`import autoload` 而不是直接用前缀 (在函数的编译过程中时如果遇到此函数,后
者有载入自动载入脚本的副作用)。
自动载入脚本中,可以根据需要分割功能和导入其它文件。这样可以在插件间分享代
码。
在 'runtimepath' 里的所有项目里搜索自动载入脚本颇耗时。如果插件知道脚本的位置
的话,使用相对路径是常见的。这样可以避免搜索,会快很多。另一个优点是脚本名不需
要是唯一的。也可用绝对路径。例如:
import autoload '../lib/implement.vim'
import autoload MyScriptsDir .. '/lib/implement.vim'
使用导入的自动载入脚本的映射在定义时,可用特殊键 <ScriptCmd> 。这使映射中的命
令可以使用映射定义所在脚本的上下文。
编译 :def 函数时,如果遇到自动载入脚本中的函数,不载入该脚本,直到 :def 函
数被调用时才载入。这意味着只有在运行时才看到错误,因为参数和返回类型此时尚未
知。但如果直接用带 '#' 字符的名字,那么 确实会
载入自动载入脚本。
小心不要意外触发自动载入脚本的载入。例如,设置选项时如果用到函数名,要用字符串
而非函数引用:
import autoload 'qftf.vim'
&quickfixtextfunc = 'qftf.Func' # _不载入_自动载入脚本
&quickfixtextfunc = qftf.Func # _载入_自动载入脚本
另一方面,需要报告错误的时候,应早点载入脚本。
为测试用, test_override() 函数使 `import autoload` 立即载入脚本,这样可以马
上检查项目和类型,而无须等待实际使用它们的时候再检查:
test_override('autoload', 1)
之后要复位的话:
test_override('autoload', 0)
或者:
test_override('ALL', 0)
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 识别带 && 或 || 的表达式,并可以把结果用作布尔型。新增
了 falsy-operator 支持使用缺省值的机制。
- TypeScript 把空串当作假值,而空列表或字典当作真值。这不统一。Vim 中空列表和
字典也都当作假值。
- TypeScript 有若干 "只读" 类型,其用途有限,因为类型转换可抹除其不可更改的性
质。Vim 则对值进行锁定,这更灵活,但只在运行时进行检查。
- TypeScript 有复杂的 "import" 语句,这和 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 脚本时,只能使用全局定义的项目,而不是导出的
项目。考虑过以下备选方案:
- 所有导出项目成为局部于脚本的项目。这样没法控制什么项目有定义,有可能很快就会
有问题。
- 使用导出项目成为全局项目。缺点是这样就不能避免全局空间的名字冲突。
- 完全禁止 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:tw=78:ts=8:noet:ft=help:norl: