0%

学习 vi 和 Vim 编辑器(8):Vim 脚本

我们可以在 .vimrc 中对 Vim 进行配置,但有时我们还需要更 动态 或更 即时 的配置,Vim 脚本可以实现这样的需求,它能让我们完成复杂的任务。其实在 Vim 的配置文件中设置 Vim 选项,就已经在编写 Vim 脚本了,因为所有 Vim 命令和选项都是 Vim 脚本的有效输入。

脚本极简入门

下面通过一个简单示例,来说明 Vim 脚本。Vim 多种自定义配色方案,可以使用 Vimcolorscheme 命令选择某种配色方案,例如 colorscheme desert。将该配置放入 Vim 的配置文件,即可在每次启动 Vim 时设置相应的配色方案。但是如果我们想根据时间早晚而选择不同的配色方案,该如何实现呢?

Vim 提供类似于 C 语言的 if/else 条件语句,而 Vim 的内置函数 strftime() 可以返回时间信息。因此将如下代码放入 .vimrc 文件之后,Vim 在每次启动时都会根据当前的时间自动设置配色方案。

1
2
3
4
5
6
7
8
9
10
11
" choose color scheme
if strftime("%H") < 6 + 0
colorscheme morning
echo "set colorscheme to morning"
elseif strftime("%H") < 12 + 0
colorscheme shine
echo "set colorscheme to shine"
else
color evening
echo "set colorscheme to evening"
endif

上述代码中还使用了一个 Vim 脚本命令:echoecho 将消息显示在 Vim 命令行状态窗口里(或者以对话框的形式出现,取决于它在启动序列中的位置)。在使用 echo 显示字符串时,需要用双引号括起字符串,否则 echo 认为其为表达式或函数。

变量

可以使用变量对上述代码进行改进:并不需要在每个条件分支里都调用 strftime() 函数,相反,我们可以只调用一次并将结果保存到 Vim 变量里。Vim 有一套定义变量作用域的惯例,依赖于变量名称的前缀。前缀包括:

  • b:在单一 Vim 缓冲区里被辨识的变量
  • w:在单一 Vim 窗口里被辨识的变量
  • t:在单一 Vim 分页里被辨识的变量
  • g:全局变量,能在任何地方被辨识
  • l:在函数内被辨识的变量(局部变量)
  • s:在来源的 Vim 脚本里被辨识的变量
  • a:函数的参数
  • v:Vim 变量,由 Vim控制,也是全局变量

如果在定义 Vim 变量时没有指定作用域,则

  • 当变量定义在函数外时,其默认为全局变量
  • 定义在函数内时,其默认为局部变量

使用 let 命令指派变量的值:let var = "value"。对上述代码进行修改,得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
" choose color scheme
let currentHour = strftime("%H")
if currentHour < 6 + 0
let colorScheme = "morning"
elseif currentHour < 12 + 0
let colorScheme = "shine"
else
let colorScheme = "evening"
endif
echo "set colorscheme to " colorScheme
colorscheme colorScheme

但该脚本执行会出现如下错误:

1
E185: Cannot find color scheme 'colorScheme'

即执行 colorscheme 命令时,没有找到 colorScheme 这种配色方案配色方案。也就说 Vim 将 colorScheme 当成配色方案,而不是将 colorScheme 的值当成配色方案。

execute命令

需要将上述语句改成 execute "colorscheme " colorSchemeexecute 的行为可以这样理解:

  • 对于单纯的词(不加引号),execute 命令将该词视为变量或表达式,并用变量的值代替该变量
  • 用引号括起来的字符串,execute 命令直接将它视为字面字符串,不进行变量替换

定义函数

上述代码只会在 Vim 启动时执行,如果想在 Vim 会话期间的任何时刻执行上述代码,可以创建包含该脚本代码的函数。Vim 使用如下形式定义函数,注意:Vim 的用户自定义函数名必须以大写字母开头。

1
2
3
4
function MyFunction(arg1, arg2, arg3......)
line of code
another line of code
endfunciton

如果我们不调用该函数,其中的代码并不会执行。在 Vim 中使用 call 语句来调用函数,例如 call MyFunction()

使用全局变量转变 Vim 脚本

如果我们想让改变颜色模式的函数被自动调用,一种方法是将该调用放入 statusline 中。由于状态行会自动更新计算,因此其中的函数也会被自动调用,但是它的缺点也很明显:调用次数过于频繁,而且每次调用都会重新设置配色方案,即使设置前后的方案完全相同。

我们使用全局变量 colors_name 来优化该函数,比较 colors_name 变量与 colorScheme,只当有两者不同时,才重新设置配色方案。colorscheme 命令会设置它自身的变量 colors_name(可以通过 echo colors_name查看该变量的值),以下是修改后的脚本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SetColor()
" choose color scheme
let currentHour = strftime("%H")
if currentHour < 6 + 0
let colorScheme = "morning"
elseif currentHour < 12 + 0
let colorScheme = "shine"
else
let colorScheme = "evening"
endif
if g:colors_name != colorScheme
echo "set colorscheme to " colorScheme
execute "colorscheme " colorScheme
endif
endfunction

上述代码有一个需要注意的事项,必须要先定义 colors_name 变量,否则脚本文件会认为没有这个变量。我们可以执行colorscheme defalut 将颜色模式设置为默认值,这样 colors_name 变量自动定义好了(也可以通过 let 命令直接定义 g:colors_name 变量)。

数组

利用 Vim 的数组可以进一步改进上述脚本。Vim 中的数组为 方括号中由逗号分隔的一串值,使用下标运算来访问数组中的某个元素,下标从 0 开始。

1
2
3
4
5
6
7
8
9
10
11
function SetColor()
" choose color scheme
let g:colorSchemes = ["morning", "shine", "evening", "evening"]
let colorIndex = (strftime("%H") + 0 ) / 6
let colorScheme = g:colorSchemes[colorIndex]
echo colorScheme
if g:colors_name != colorScheme
echo "set colorscheme to " . colorScheme
execute "colorscheme " colorScheme
endif
endfunction

通过脚本动态配置文件类型

通常编辑新文件时,Vim 根据文件的扩展名来判断并设置 filetype。但并非所有文件都会提供扩展名,事实上,Vim 具有良好的训练,只需要判读文件的内容,也可以辨识出文件类型,但是前提是文件已经提供了足够的内容。接下来我们将编写一个 Vim 脚本,来动态配置文件类型。

自动命令

自动命令包括任何合法的 Vim 命令。Vim 使用事件执行命令,Vim 事件包括:

  • BufNewFile:在 Vim 开始编辑新文件时触发相关联的命令
  • BufReadPre:在 Vim 移向新缓冲区前触发相关联的命令
  • BufRead, BufReadPost:在编辑新文件时触发相关联的命令,但需要在读入文件后
  • BufWrite, BufWirtePre:把缓冲区的内容写入文件前触发相关联的命令
  • FileType:在设置 filetype 后触发相关联的命令
  • vimResized:在改变 Vim 窗口尺寸后触发相关联的命令
  • winEnter, winLeave:分别在进入、离开 Vim 窗口时触发相关联的命令
  • CursorMoved, CursorMovedI:分别在光标进入正常模式、插入模式时触发的命令

Vim 事件还有很多,任何一个事件都可以定义在事件发生时执行的 autocmd,其格式如下:

1
autocmd [group] event pattern [nested] command
  • group:可选的命令组
  • event:触发命令的事件
  • pattern:匹配文件名的模式,用于找出应执行命令的文件
  • nested:如果出现,表示这个自动命令能放在其它自动命令中
  • command:当事件发生时执行的 Vim 命令、函数或用户自定义脚本

例如设计如下自动命令,用于检测文件类型:

1
autocmd CursorMovedI * call CheckFileType()
  • 触发命令的事件为 CursorMovedI,即当处于插入模式时触发命令
  • 匹配文件名的模式为:*,代表辨识任何新打开的文件类型
  • 执行的命令为 call CheckFileType()CheckFileType() 是我们自己编写的用于检测文件类型的函数

CheckFileType 函数中,需要检查 filetype 选项的值,在 Vim 脚本中,通过在选项名称前加入前缀(&)字符,即可获得选项值。以下是 CheckFileType 函数的简易定义:

1
2
3
4
5
function CheckFileType()
if &filetype == ""
filetype detect
endif
endfunction

Vim 命令 filetype detect 是个保存在 $VIMRUNTIME 目录下的 Vim 脚本,它检查许多标准,试着为每个文件指派一个类型。

缓冲区变量

如果我们想在插入模式中将光标移动 20 次之后才开始文件类型的检测,此时需要定义一个缓冲区变量(因为编辑会话中的各个缓冲区应该相互独立)。修改后的函数定义如下:

1
2
3
4
5
6
7
8
9
10
function CheckFileType()
if exists("b:countCheck") == 0
let b:countCheck = 0
endif
let b:countCheck += 1

if &filetype == "" && b:countCheck > 20
filetype detect
endif
endfunction

自动命令与组

vim提供自动命令组的表示法,其语法为:

1
2
3
augroup group_name
.....
augroup END

现在使用如下语句将前面的 autocmd 定义到自动命令组中:

1
2
3
augroup newFileDetection
autocmd CursorMovedI * call CheckFileType()
augroup END

为了高效地实现我们的函数,我们希望函数完成功能后,解除对它的引用。Vim 可以在事件处理函数中删除自动命令:

1
autocmd! [group] [event] [pattern]
  • 感叹号接在 autocmd 后,表示与组、事件或模式相关联的命令将被删除

这样,我们通过如下语句就可删除与 newFileDetection 组相关联的命令:

1
autocmd! newFileDetection

如果想要确认自动命令的定义与删除,可在 Vim 中使用如下命令进行查询:autocomd newFileDetection

如果自动命令组中已经不包含自动命令,就可以使用 augroup! groupname 删除自动命令组。注意删除 自动命令组 不等于删除 已关联的自动命令,如果在 自动命令组 中存在 自动命令 的情况下删除 自动命令组,则 Vim 在每次引用到这些自动命令时都将出现错误状态。

其他注意事项

  • Vim 提供不少扩展组件以及其它脚本语言的编程接口,其中包括 Perl,Python 和 Ruby 这三种最受欢迎的脚本语言
  • 除了 Vim 命令,Vim 也提供大量内置函数,具体的这些内置函数的说明可以查看 Vim 内置的帮助文件 usr_41.txt

这里只是讨论了 Vim 脚本的皮毛,有更多的资源可以让我们更深入学习 Vim 脚本:

  • 在 Vim 主页可以找到大量 Vim 脚本
  • Vim 的运行时目录中也有大量的 Vim 脚本,所有后缀是 .vim 的文件都是脚本
  • Vim 内置的说明文档也是无价之宝,可以参考如下主题:
    • help autocmd
    • help scripts
    • help variables
    • help functions
    • help usr_41.txt