Vimscript编程指南

外部命令

Vim遵循UNIX的“做好一件事”的哲学。Vim没有把所有的功能都集成到编辑器内部,而是通过把某些合适的功能代理到外部的命令中。

我们添加一些能够和Potion编译器进行互动的插件来让我们能够更好地进行互动。

编译

首先,我们需要添加一个命令来编译和运行当前的Potion文件。实现这个功能有很多种方式,但是我们这里只是简单地使用一个外部命令。

在你的插件的代码库里创建一个potion/ftplugin/potion/running.vm文件。我们会在这个文件里创建用来编译和运行Potion文件的映射。


if !exists("g:potion_command")
    let g:potion_command = "potion"
endif

function! PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

nnoremap  r :call PotionCompileAndRunFile()

第一部分代码创建了一个全局变量来保存用来执行Potion的命令,当然是在这个变量还没有设置的情况下。我们之前有过类似的代码。

这种方式可以允许用户重新设置potion命令的路劲,如果potion不是在默认的$PATH路径下,用户可以在~/.vimrc文件里添加上`g:option_command = "/Users/sjl/src/potion/poiton"来进行覆盖。

最后一行添加了一个缓存本地的映射,它会调用我们上面定义的函数。注意,因为这个文件是房子ftdetect/potion目录下,所以每当文件的filetype被设置成potion的时候,这个脚本文件都会被执行。

实际上的功能实现是在PotionCompileAndRunFile()函数里。保存这个文件,打开factorial.pn文件然后按下<localleader>r来运行这个映射,看看会发生什么。

如果potion是在你的$PATH路径下,这个文件会运行并且你会在终端上看到它的输出结果(如果你使用的时GUI Vim,结果会在窗口的底部出现)。如果你看到一个potion命令找不到的错误,那么你需要在~/.vimrc文件里设置g:option_command的值。

我们来看看PotionCompileAndRunFile()是怎么工作的。

!命令

:!命令(读作“帮”)可以在Vim里运行外部命令,并且输出它们的结果。通过下面的命令来试试这它:


:!ls

Vim会输出这个ls命令的结果,并且会有一个“按回车键或者输入命令继续”的提示。

通过这种方式运行命令,Vim并不会给命令提供任何输入。通过下面的命令来确认这个:


:!cat

输入几行文本,你会看到cat命令会把这些文本再回显回来,就像你在Vim外部运行命令一下。使用Ctrl-D来结束这个命令。

要想运行外部命令时没有Press ENTER or type command to continue这样的提示的花,使用:silent !运行下面的命令:


:slient !echo Hello, world.

如果使用的时MacVim或者gVim这样的图形界面的Vim,你就不会看见Hello, world.这样的输出了。

如果你运行的是终端下的Vim,输出的结果以来于你的配置。在运行完单独的:silent !命令后你需要运行:redraw!来恢复你的屏幕。

注意这个命令式:silent !而不是:silent!(注意空格?)!这两个是不同的命令,并且我们需要的时前面一个!Vim是不是很强大?

我们现在回头看看PotionCompileAndRun()函数:


function! PotionCompileAndRunFile()
    silent !clear
    execute "!" . g:potion_command . " " . bufname("%")
endfunction

首先,我们运行一下silent !clear命令,这样就会清空屏幕,并且不会显示Press ETER...的提示信息。这样就会保证我们只会看到当前运行程序的输出,这对于不停地跑同一个命令是很有帮助的。

接下来的一行我们用之前的execute来动态构建一个命令。它构建的命令和下面有点类似:


!potion factorial.pn

注意这里没有silent,所以用户需要按回车键回到Vim。这使我们的映射所需要的,所以我们的工作完成了。

展示字节码

Potion编译器有个允许你查看生产的字节码的选项。当你需要调试底层代码的时候,这个功能是很有用的。试试在shell里运行下面的命令:


potion -c -V factorial.pn

你会看到很多类似下面的输出:


-- parsed --
code ...
-- compiled --
; function definition: 0x109d6e9c8 ; 108 bytes
; () 3 registers
.local factorial ; 0
.local print_line ; 1
.local print_factorial ; 2
...
[ 2] move     1 0
[ 3] loadk    0 0   ; string
[ 4] bind     0 1
[ 5] loadpn   2 0   ; nil
[ 6] call     0 2
...

我们来添加一个映射,让用户能够在一个Vim窗口里查看当前Potion文件的字节码输出,所以用户能够浏览并且查看它们。

首先,把下面的一行代码添加到ftplugin/potion/running.vm的末尾:


nnoremap  b :call PotionShowBytecode()

这里没有什么特别的 —— 只是一个简单地映射,下面我们简单描述一下我们要实现的函数:


function! PotionShowBytecode()
    " Get the bytecode.

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

现在我们有了大致的框架,我们再来讨论如何来实现它。

system()

我们有很多方式可以实现这个功能,所以我们会选择一个对你而言很实用的方式。

运行下面的命令:


:echo system("ls")
你会在屏幕的下方看到`ls`命令的输出。如果你运行`:message`你也在那儿看到有输出。所以Vim里的`system()`函数会执行一个命令,并且把命令的结果作为字符串进行返回。

你可以传入第二个字符串作为`system()`的参数。运行下面的命令:

:echom system("wc -c","abcdefg")

Vim会输出7(有些填充的空格)。如果你通过上面的方式传入第二个参数,Vim会把它写入到一个临时文件,并且通过管道把它传入到命令的标准输入上。对我们而言实际上不需要这样,但是最好还是了解一下。

回到我们的函数,编辑PotionShowBytecode()用下面的代码来完成框架的第一部分:


function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))
    echom bytecode

    " Open a new split and set it up.

    " Insert the bytecode.

endfunction

保存文件,在factorial.pn里运行:set ft=potion来重新加载它,然后使用<localleader>b映射。Vim会在屏幕的底部输出字节码,一旦你确定这段代码能够正常工作,就可以吧echom这一行给删除了。

草稿分屏

接下来我们要打开一个新的分屏来展示结果。这样的话,用户就能够使用强大的Vim来浏览字节码,而不是在屏幕底部看一次就够了。

要完成这个功能,我们需要创建一个“草稿”分屏:这种分屏包含一个缓存区,但是这个缓冲区的数据不会被保存下来,并且每次运行映射的时候都会被覆盖掉。修改PotionShowBytecode()的内容如下所示:


function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%"))

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.

endfunction

这个新的命令很容易理解。

vsplit创建一个垂直的分屏,它的缓冲区名称叫做__Potion_Bytecode__。这个缓冲区的名称前后都加了下划线用来表示这个文件不是一个常规的文件(它仅仅是用来保持输出结果的一个临时缓冲区)。下划线并没有什么特别之处,这种做法只是一种约定。

接下来我们通过normal! ggG命令来删除这个缓冲区里的所有内容。第一次运行映射的时候这个命令不会做什么,但是接下来的时间里,我们需要重复利用__Potion_Bytecode__这个缓冲区,所以我们需要清空它。

接下来我们需要为这个缓冲区做两个设置。首先,我们把这个缓冲区的文件类型设置为potionbytecode,表面它里面的内容。其次我们把buftype设置成nofile,以此告诉Vim这个缓冲区不和硬盘上的任何文件进行关联,所以不用去写它。

接下来,我们把保存到bytecode变量里的字节码放到这个缓存里。接下来完成我们的函数:


function! PotionShowBytecode()
    " Get the bytecode.
    let bytecode = system(g:potion_command . " -c -V " . bufname("%") . " 2>&1")

    " Open a new split and set it up.
    vsplit __Potion_Bytecode__
    normal! ggdG
    setlocal filetype=potionbytecode
    setlocal buftype=nofile

    " Insert the bytecode.
    call append(0, split(bytecode, '\v\n'))
endfunction

这个append()Vim函数接收两个参数:一个表示从哪一行之后开始添加文本,然后是一个列表的文本行。举个例子,试试下面的命令:


:call append(3, ["foo", "bar"])

这个命令会在当前缓冲区的第三行的下面添加两行内容foobar。这种情况下,会在第0行下面添加文件,也就是“文件的顶部”。

我们需要一个字符串列表来进行添加,但是使用system()函数我们拿到的时一个里面有换行符的单个字符串。我们使用Vim的split()函数来把这一长串文本拆分成一个字符串列表。split()函数使用正则表达式来匹配切分点来切分字符串。这个很简单。

现在我们已经完成了这个函数,我们可以来试试这个映射的功能了。当你在factorial.pn的缓冲区里运行<localleader>b命令的时候,Vim会打开一个新的缓冲区里面包含Potion的字节码。试试改变代码,保存文件,然后运行这个映射来看看它的变化。

练习

  • 阅读:help bufname的内容。
  • 阅读:help buftype的内容。
  • 阅读:help append()的内容。
  • 阅读:help split()的内容。
  • 阅读:help :!的内容。
  • 阅读:help :read:help :read!的内容(虽然我们没有讲这些命令,但是它们是很有用的)。
  • 阅读:help system()的内容。
  • 阅读:help design-not的内容。

现在,我们的映射需要用字自己在运行映射之前先保存文件,这样他们的修改才会有效。撤销的动作很容易实现,所以我们修改函数来帮他们保存当前的文本。

当你在一个有语法错误的文件上来运行字节码映射的函数,会发生什么呢?为什么会是这样的结果呢?

修改PotionShowBytecode()函数,来检测当Potion编译器返回错误呢,把错误信息提示给用户。

额外加分

每次你运行字节码映射时,Vim会创建一个新的垂直分屏,即使用户没有关闭前一个。如果用户没有关闭它们,那么会出现很多窗口堆叠的情况。

修改PotionShowBytecode()函数,来检测是否已经有窗口为__Potion_Bytecode__缓冲区打开,如果有的话,直接切换到已有的窗口,而不是创建一个新的窗口。

对于这个任务,你可能需要阅读以下:help bufwinnr()的内容。

更多额外的加分

记得我们把临时缓冲区的filetype设置成了potionbytecode吗?创建一个syntax/potionbytecode.vim文件,然后定义Potion字节码的语法高亮,使得它们更加易读。