使用脚本编写 Vim 编辑器,第 5 部分: 事件驱动的脚本编写和自动化

Vim 的事件模型

Vim 编辑功能的运行方式是事件驱动的。但由于性能上的原因,实际的实现要远比这个复杂,还需要进行许多事件处理优化或者处理事件循环下面的几层,但是您仍然可以将编辑器看成一个简单循环,响应一系列的编辑事件。

无论您何时开始一个 Vim 会话,打开一个文件,编辑一个缓冲区,修改您的编辑模式,切换窗口,或者和周围的文件系统交互,您正在有效地排列 Vim 能迅速接受和处理的事件。

例如,如果您启动 Vim,编辑一个名为 demo.txt 的文件,切换到 Insert 模式,输入一段文档,保存文件,然后退出,您的 Vim 对话就接收到一系列的事件,如清单 1 所示。

清单 1. 一个简单 Vim 编辑对话中的事件序列
> vim
    BufWinEnter     (create a default window)
    BufEnter        (create a default buffer)
    VimEnter        (start the Vim session):edit example.txt
    BufNew          (create a new buffer to contain demo.txt)
    BufAdd          (add that new buffer to the session’s buffer list)
    BufLeave        (exit the default buffer)
    BufWinLeave     (exit the default window)
    BufUnload       (remove the default buffer from the buffer list)
    BufDelete       (deallocate the default buffer)
    BufReadCmd      (read the contexts of demo.txt into the new buffer)
    BufEnter        (activate the new buffer)
    BufWinEnter     (activate the new buffer's window)i
    InsertEnter     (swap into Insert mode)
Hello
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    InsertLeave     (swap back to Normal mode):wq
    BufWriteCmd     (save the buffer contents back to disk)
    BufWinLeave     (exit the buffer's window)
    BufUnload       (remove the buffer from the buffer list)
    VimLeavePre     (get ready to quit Vim)
    VimLeave        (quit Vim)

更有趣的是,Vim 提供允许您拦截任何此类编辑事件的 “挂钩”。这样,您就可以创建一个特殊的 Vimscript 指令或者函数,在一个特定事件发生时,进行执行:每次 Vim 启动时,每次加载文件时,每次退出 Insert 模式……甚至是每次您移动指针时。这样就可以在整个编辑器中的任何地方添加自动的行为。

Vim 提供 78 种编辑事件的通知,分为八大类:对话开始和清理事件,文件读取事件,文件编辑事件,缓冲区修改事件,选项设置事件,窗口相关事件,用户交互事件,以及异步通知。

要查看此类事件的全部清单,在 Vim 命令行输入 :help autocmd-events。要查看各个事件的详细描述,请看 :help autocmd-events-abc

本文解释了事件在 Vim 中如何运行,然后介绍了一系列自动编辑事件和行为的脚本。

使用自动命令处理事件

Vim 中用于拦截事件的机制就是 自动命令。每个自动命令都指定拦截某种类型的事件,拦截此类事件中被编辑的文件名,当它们被检测到时,命令行模式就会采取行动。所有这些的关键字就是 autocmd(通常缩写为 au)。常用的语法是:

autocmd  EventName  filename_pattern   :command

事件名称是 78 种有效 Vim 事件名称(如 :help autocmd-events 所列示)之一。文件名称模式的语法和普通的 shell 模式(详见 :help autocmd-patterns)很相似 —— 但又有所区别。该命令可以是任何有效的 Vim 命令,包括 Vimscript 函数的调用。命令开始处的冒号是可选的,但是最好加上;这么做能够让命令更轻松地在一个(往往很复杂的) autocmd 参数清单中定位。

例如,您可以放下最后的顾虑,将下述内容添加到您的 .vimrc 文件中,来指定一个 FocusGained 事件的事件处理器:

autocmd  FocusGained  *.txt   :echo 'Welcome back, ' . $USER . '! You look great!'

当一个 Vim 窗口变为窗口系统的输入焦点时,FocusGained 事件就会进行排列。所以现在无论您何时切换回您的 Vim 对话,如果您当前正在编辑任何名称符合文件名称模式 *.txt 的文件,那么 Vim 将会自动执行指定的 echo 命令。

您可以按照自己的意愿对同一个事件设置任意数量的处理器,所有这些处理器都会按照其最初的指定顺序来执行。例如,FocusGained 事件的一个非常有用的的自动化可能是:无论您何时切换回您正在编辑的对话,都会使 Vim 简洁地强调指针所在行,如清单 2 所示。

清单 2. FocusGained 事件的一个有用的自动化
autocmd  FocusGained  *.txt   :set cursorline
autocmd  FocusGained  *.txt   :redraw
autocmd  FocusGained  *.txt   :sleep 1
autocmd  FocusGained  *.txt   :set nocursorline

这 4 个自动命令使 Vim 自动地强调包含指针的行(set cursorline),指示高亮强调点(redraw),等待一秒钟(sleep 1),然后取消强调(set nocursorline)。

您可以按这种方法来使用任何系列的命令;您甚至可以使用多个自动命令来分解一个单一的控制结构。例如,您可以设置一个全局变量(g:autosave_on_focus_change)来控制一个 “自动保存” 机制,它可以自动地写入任何修改过的 .txt 文件,无论用户何时从 Vim 切换到其它窗口(调用一个 FocusLost 事件进行排列):

清单 3. 退出一个编辑器窗口时自动保存的自动命令
autocmd  FocusLost  *.txt   :    if &modified && g:autosave_on_focus_change
autocmd  FocusLost  *.txt   :    write
autocmd  FocusLost  *.txt   :    echo "Autosaved file while you were absent" 
autocmd  FocusLost  *.txt   :    endif

像这样的多行自动命令需要您多次重复关键的事件选择规范(例如,FocusLost *.txt)。这样一来,它们通常就不易维护,也更容易出错。更简单、更安全的方法是,将任意控制结构,或者其它命令队列,析出到一个独立的函数,然后再用一个单独的自动命令来调用这个函数。例如:

清单 4. 更简洁的处理多行自动命令的方法
function! Highlight_cursor ()
    set cursorline
    redraw
    sleep 1
    set nocursorline
endfunction
function! Autosave ()
   if &modified && g:autosave_on_focus_change
       write
       echo "Autosaved file while you were absent" 
   endif
endfunction

autocmd  FocusGained  *.txt   :call Highlight_cursor()
autocmd  FocusLost    *.txt   :call Autosave() 

通用自动命令和单文件适用的自动命令

目前为止,所有列出的例子都局限在处理符合模式 *.txt 的事件。很显然,这就意味着您可以使用任何文件通配符模式来指定一个特殊自动命令所适用的文件。例如,您只需简单地使用通用文件匹配模式 * 作为文件名过滤器,就可以将之前的指针强调 FocusGained 自动应用到任何文件:

" Cursor-highlight any file when context-switching ...
autocmd  FocusGained  *          :call Highlight_cursor()

您也可以有选择地将命令限制在一个单一文件中:

" Only cursor-highlight for my .vimrc ...
autocmd  FocusGained  ~/.vimrc   :call Highlight_cursor()

要注意的是,这也意味着您可以对同一个事件指定不同的行为,这取决于当前正在编辑哪个文件。例如,当用户把注意力放在其它地方时,您可以选择自动地保存文本文件,或者使 Perl 或者 Python 脚本自检, 构建一个文档文件来对当前段落重新格式化,如清单 5 所示。

清单 5. 当用户的注意力在其它地方时可以做什么
autocmd  FocusLost  *.txt   :call Autosave()
autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
autocmd  FocusLost  *.doc  :call Reformat_current_para()

自动命令组

自动命令有一个相关的命名机制,允许它们集成一个自动命令组,这样就可以对它们进行集体操作。

为了指定一个自动命令组,您可以使用 augroup 命令。该命令的通用语法是:

augroup GROUPNAME
 " autocommand specifications here ...
augroup END

这个组的名称可以是一系列非空白的字符,除了 “end” 或者 “END”,这是保留用于指定一个组的结束的。

使用自动命令组,您就可以一次实施任意数量的自动命令。特别是,您可以将响应同一事件的命令集成在一个组中,如清单 6 所示。

清单 6. 定义响应 FocusLost 事件的自动命令组
augroup Defocus
    autocmd  FocusLost  *.txt   :call Autosave()
    autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
    autocmd  FocusLost  *.doc   :call Reformat_current_para()
augroup END

或者您可以把和某种单独文件类型相关的自动命令集成,例如:

清单 7. 定义处理文本文件的自动命令组
augroup TextEvents 
    autocmd  FocusGained  *.txt   :call Highlight_cursor()
    autocmd  FocusLost    *.txt   :call Autosave()
augroup END

停用自动命令

您可以使用 autocmd! 命令(即,使用一个感叹号),删除指定事件的处理器。该命令通用的语法是:

autocmd!  [group]  [EventName [filename_pattern]]

为了删除一个单独的事件处理器,需要指定所有的三个参数。例如,要从 Unfocussed 组中删除 .txt 文件的处理器,使用:

autocmd!  Unfocussed  FocusLost  *.txt

若不使用一个指定事件名称,您可以使用一个星号来表示从特殊组和文件名模式中要删除的事件。如果您想要删除 Unfocussed 组里 .txt 文件中的全部事件,可以使用:

autocmd!  Unfocussed      *      *.txt

如果您离开文件名模式,那么指定事件类型的各个处理器都会被删除。您可以像这样删除 Unfocussed 组的所有 FocusLost 处理器:

autocmd!  Unfocussed  FocusLost

如果您还遗漏了事件名称,那么在指定组中的每个事件处理器也会被删除。这样就能关闭 Unfocussed 组中指定的所有事件处理:

autocmd!  Unfocussed

最后,如果您省略了组名称,自动命令删除就会适用当前激活状态的组中。这个选项的典型应用就是在设置一系列自动命令之前,在组内 “清理桌面”。例如,Unfocussed 组最好可以像这样进行指定:

清单 8. 在添加新自动命令之前保证这个组是空的
augroup Unfocussed
    autocmd!

    autocmd  FocusLost  *.txt   :call Autosave()
    autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
    autocmd  FocusLost  *.doc   :call Reformat_current_para()
augroup END

在每个组开始的地方添加一个 autocmd! 是非常重要的,因为自动命令不会静态地声明事件的处理器;它们是动态地创建处理器。如果您两次执行相同的 autocmd,您就可以获得两个事件处理器,这两个处理器将会由那一点上相同的事件和文件名组合分别激活。通过用 autocmd! 开始每个自动命令组,您就可以清除组内所有现存的处理器,这样队列 autocmd 语句会替换任何现存的处理器,而不是对它们进行参数设置。反过来,这就意味着您的脚本可以按需要执行任意次(或者是您的 .vimrc 可以反复被 source ),无需增加不必要的事件处理实体。

一些实践例子

适当使用自动命令可以使您的编辑工作容易很多。让我们看一些您使用自动命令简化编辑过程,消除现有问题的例子。

管理同步编辑

Vim 最有用的特性之一就是当您打算编辑一个当前正在由其它 Vim 实例编辑的文件时,它会进行自动检测。这经常发生在一个多窗口环境中,在这个环境中,您正在一个终端编辑一个文件;或者在一个多用户设置中,在这个环境下他人正在一个共享文件上工作。当 Vim 检测到第二个编辑某特殊文件的企图时,您可以得到以下请求:

Swap file ".filename.swp" already exists!
[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: _

根据您工作的环境,无需您的仔细思考,您的手指可能每次就会自动地点击这些选项。例如,如果您很少在共享文件上工作,您可能只用点击 q来终止会话,然后开始搜索您正在编辑的文件的终端窗口。另一方面,如果您常常编辑共享资源,或许您的手指会训练有素地立刻点击 来选择默认选项,打开只读文件。

然而,使用自动命令,通过简单地自动响应触发它的 SwapExists 事件,您完全无需查看、识别和响应那个消息。例如,如果您从不想编辑一个正在其它地方进行编辑的文件,您可以添加以下命令到您的 .vimrc

清单 9. 自动退出同步编辑
augroup NoSimultaneousEdits
    autocmd!
    autocmd  SwapExists  *  :let v:swapchoice = 'q'
augroup END

这会设置一个自动命令组,并删除所有之前的处理器(通过 autocmd! 命令)。然后它会在任意文件上安装一个 SwapExists 事件的处理器(使用通用文件模式:*)。那个处理器会简单地分配 'q' 来响应指定的 v:swapchoice 变量。Vim 会提前查询这个变量,显示 “swapfile exists” 消息。如果这个变量已经被设置过,它就使用这个值作为自动响应,而不显示这个消息。那么现在您就永远不会看到 swapfile 消息;您的 Vim 会话将会自动退出,如果您试图编辑一个正在其它地方进行编辑的文件。

换一种情况,如果您想要在只读模式打开已编辑的文件,您可以简单地改变 NoSimultaneousEdits 组:

清单 10. 自动只读访问现有文件
augroup NoSimultaneousEdits
    autocmd!
   autocmd  SwapExists  *  :let v:swapchoice = 'o'
augroup END

更有趣的是,您可以根据当前正在考虑的文件的位置,安排在这两个可替换的选项间选择。例如,您可以选择在您自己的子目录中自动退出文件,但是在 /dev/shared/ 下,以只读的方式打开共享文件。您可以按如下进行操作:

清单 11. 自动返回环境敏感响应
augroup NoSimultaneousEdits
    autocmd!
    autocmd  SwapExists  ~/*            :let v:swapchoice = 'q'
    autocmd  SwapExists  /dev/shared/*  :let v:swapchoice = 'o'
augroup END

即:如果文件全名以主目录开始,以后内容不限(~/*),那么预选 “退出” 行为;但是如果文件全名以共享目录开头(/dev/shared/*),那么就预选 “只读” 行为。

统一自动格式化代码

Vim 很好地支持编辑时自动代码布局(见 :help indent.txt 和 :help filter)。例如,您可以选择 'autoindent' 和'smartindent' 选项,然后根据您的输入自动地使 Vim 重新缩进您的代码块。或者您可以设置 'equalprg' 选项,将您特定语言的代码重新格式化程序连接到标准 = 命令。

不幸的是,Vim 没有一个选项或者命令来处理最常见的代码格式化程序:您就不得不阅读其他人难懂的 malformatted 代码。尤其是,没有内置的选项来告诉 Vim 自动地对您打开的任何代码文件的格式程序进行杀毒。

这是没有问题的,因为不用这种方法,而是设置一个自动命令来实现它是非常简单的。

例如,您可以将以下的自动命令组添加到您的 .vimrc,这样当您打开相应类型的文件时,C、Python、Perl,和 XML 文件就能自动地在适合的代码格式程序中运行,如清单 12 所示。

清单 12. 自动命令上的完美代码
augroup CodeFormatters
    autocmd!

    autocmd  BufReadPost,FileReadPost   *.py    :silent %!PythonTidy.py
    autocmd  BufReadPost,FileReadPost   *.p[lm] :silent %!perltidy -q
    autocmd  BufReadPost,FileReadPost   *.xml   :silent %!xmlpp –t –c –n
    autocmd  BufReadPost,FileReadPost   *.[ch]  :silent %!indent
augroup END

组中的所有自动命令在结构上是相同的,只是在它们适用的文件名扩展和它们相应激活的美化打印机方面有所不同。

要注意的是,自动命令不会命名一个单一的待处理事件。相反地,它都会指定一个事件清单。任何 autocmd 都可以由一个用逗号分隔的事件类型清单指定,这里所列出来的任一事件都能调用处理器。

在这种情况下,每个处理器列出的事件都是 BufReadPost(它在当前文件被加载到新的缓冲区时进行排列)和 FileReadPost (它在任何:read 命令执行之后进行排列)。这两个事件往往同时被指定,因为在两者之间,它们覆盖了将当前文件的内容加载到一个缓冲区的最常用方法。

在事件清单之后,每个自动命令指定它适用的文件后缀:Python 的 .py,Perl 的 .pl 和 .pm,XML 的 .xml,或者 C 的 .c 和 .h 文件。要注意的是,通过这些事件,这些文件名模式也可以被指定为一个逗号分隔的清单,而不是一个单一模式。例如,Perl 处理器可以这样写:

autocmd  BufReadPost,FileReadPost   *.pl,*.pm   :silent %!perltidy -q

或者 C 的处理器也可以被扩展到处理常用的 C++ 变体(.C.cc.cxx,等等。),像这样:

autocmd  BufReadPost,FileReadPost   *.[chCH],*.cc,*.hh,*.[ch]xx  :silent %!indent

通常,每个自动命令的最后组件是需要执行的命令。在各种情况下,它是一个全局过滤器命令(%!filter_program),它会接收文件的全部内容(%),把它排出(!)到指定的外部程序(PythonTidy.pyperltidyxmlpp,或者 indent 之一)。然后将每个程序的输出粘贴回缓冲区,替换原有的内容。

一般情况下,当使用像这样的过滤器命令时,Vim 会在命令完成后,自动地显示一个通知,像这样:

42 lines filtered
Press ENTER or type command to continue_

为了避免这种烦恼,每个自动命令用一个 :silent 来对其行动添加前缀,它会中和任何普通的信息消息,但仍允许显示错误消息。

随机对代码自动格式化

Vim 对自动地格式化您所输入的 C 代码有良好的支持,但它并未对其它语言提供支持。那并不完全是 Vim 的错误;一部分语言 —— 对,Perl,我就在说它 —— 很难实时正确地格式化。

如果 Vim 没有为用您喜欢语言编写的源代码提供足够的支持,您可以简单地用您的编辑器调用一个外部工具来为您完成这个任务。

最简单的方法就是利用 InsertLeave 事件。无论您何时退出 Insert 模式(最常见的是在您敲击  键之后),这个事件都会被排列。您可以轻松地设置一个处理器,它可以在您完成其添加之后重新格式化您的代码,就像这样:

清单 13. 在每个编辑之后激活 PerlTidy
function! TidyAndResetCursor ()
    let cursor_pos = getpos('.')
    %!perltidy -q
    call setpos('.', cursor_pos)
endfunction

augroup PerlTidy
    autocmd!
    autocmd InsertLeave *.p[lm]  :call TidyAndResetCursor()
augroup END

TidyAndResetCursor() 函数可以通过存储在变量 cursor_pos 中内置的 getpos() 返回的指针信息,来记录当前指针的位置。 然后它在整个文件上(%!perltidy -q)运行外部 perltidy 工具,最后通过把存储的指针信息发送到内置的 setpos() 函数,把指针恢复到原来的位置。

在 PerlTidy 组内,每次用户离开任意 Perl 文件中的 Insert 模式时,您只需要设置一个单一调用 TidyAndResetCursor() 的自动命令。

每当您插入文本时,这个相同的代码模式可以适用于执行任何适当的行为。例如,如果您正在一个非常不可靠的系统中工作,希望能最大化自身的能力来恢复文件(见 :help usr_11.txt),如果出现故障,每次您离开 Insert 模式,可以安排 Vim 来升级它的交换文件,像这样:

augroup UpdateSwap
    autocmd!
    autocmd  InsertLeave  *  :preserve
augroup END

时间戳文件

另一组有用的事件是 BufWritePreFileWritePre,和 FileAppendPre。在您的 Vim 会话把一个缓冲区写回磁盘之前(作为一个命令的结果,例如 :write:update,或者 :saveas),就会对这些 “Pre” 事件进行排列。一个 BufWritePre 事件发生在整个缓冲区被写入之前,一个 FileWritePre 发生在部分缓冲区被写入之前(即,当您指定写入的行范围::1,10write)。一个 FileAppendPre 发生在一个:write 命令被用于追加而不是替换之前;例如:

:write >> logfile.log).

对所有这三类事件,Vim 在正在被编写的代码行范围内,设置了特殊的行编号别名 '[ and ']。那么这些别名可以在任意序列命令的范围指定符中使用,保证自动命令行为只应用于相关行。

通常,您可以设置一个单一的覆盖所有三种预写事件的处理器。例如,每当一个文件被写入(或者追加)到磁盘,您可以使 Vim 自动地升级一个内部的时间戳,如清单 14 所示。

清单 14. 当文件保存时自动更新内部时间戳
function! UpdateTimestamp ()
    '[,']s/^This file last updated: \zs.*/\= strftime("%c") /
endfunction

augroup TimeStamping
    autocmd!

    autocmd BufWritePre,FileWritePre,FileAppendPre  *  :call UpdateTimestamp()
augroup END

UpdateTimestamp() 函数,通过专门将替换的范围限制在 '[ 和 '] 之间,在每个正在被编写的代码行上执行一个替代(s/.../.../),像这样:'[,']s/.../.../。这个替代会查找以 “This file last updated:” 开始的代码行,无论其后内容是什么(.*)。在 .* 之前的 \zs 会导致这个替代看起来只是从冒号后开始匹配,所以只有实际的时间戳被替换。

为了更新时间戳,这个替代在替换的文本中使用特殊的 \= escape 序列。这个转义序列告诉 Vim 把替换文本作为一个 Vimscript 表达式来处理,对它进行评估来获取实际的替换字符串。在这种情况下,这个表达式调用内置的 strftime() 函数,这个函数会返回一个标准时间戳字符串:“Fri Oct 23 14:51:01 2009”。这个字符串然后被替代命令写回时间戳。

剩下的工作就是设置在任意文件(*)中的所有三种事件类型(BufWritePreFileWritePreFileAppendPre)的事件处理器(autocmd),然后让它调用适合的时间戳函数(:call UpdateTimestamp())。现在,无论何时编写文件,被保存代码行的时间戳都会被更新到当前时间。

要注意的是,Vim 还提供了另外两组您可以用来修改写操行为的事件。要让写操作之后的某些行为自动化,您可以使用BufWritePostFileWritePost 和 FileAppendPost。要在您的脚本中完全替换标准的写操作,您可以使用BufWriteCmdFileWriteCmd 和 FileAppendCmd(但是首先要查询 :help Cmd-event 来获得一些重要的提示)。

表格驱动的时间戳

当然,您也可以创建更巧妙的机制来处理有不同时间戳约定的文件。例如,您可能喜欢指定各种时间戳签名和它们在一个 Vim 字典中的替换(见该系列的上一篇文章),然后在各对中循环决定时间戳应该怎样升级。这个方法如清单 15 所示。

清单 15. 表格驱动的自动时间戳
let s:timestamps = {
\  'This file last updated: \zs.*'             :  'strftime("%c")',
\  'Last modification: \zs.*'                  :  'strftime("%Y%m%d.%H%M%S")',
\  'Copyright (c) .\{-}, \d\d\d\d-\zs\d\d\d\d' :  'strftime("%Y")',
\}

function! UpdateTimestamp ()
    for [signature, replacement] in items(s:timestamps)
        silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'
    endfor
endfunction

这里,这个 for 循环会遍历 s:timestamps 字典中的每个时间戳的签名/替换对,就像这样:

for [signature, replacement] in items(s:timestamps)

然后它产生一个包含相应替代命令的字符串。以下的替代命令在结构上和之前例子中的一样,但是在这里它是由篡改字符串的签名/替换对来构建的:

"'[,']s/" . signature . '/\= ' . replacement . '/'

最后,它以静默方式执行产生的命令:

silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'

silent! 的使用是很重要的,因为它保证了任何不匹配的替换不会导致令人讨厌的 Pattern not found 错误消息。

需要注意的是,s:timestamps 中的最后条目是一个极为有用的例子:它会自动地更新任何嵌入式版权声明的年份范围,只要包含它们的文件被编写。

文件名驱动的时间戳

您可能喜欢用参数表示 UpdateTimestamp() 函数,然后对不同文件类型创建一系列不同的 autocmds,而不是列出一个表格中所有可能的时间戳格式,如清单 16 所示。

清单 16. 不同文件类型的环境敏感时间戳

点击查看代码清单

在这个版本中,签名和替换组件被明确地传递给 UpdateTimestamp(),然后它可以产生一个包含相应替换命令的字符串,并执行该字符串。在 Timestamping 组中,您可以为每个所需的文件类型设置独立的自动命令,分别为它们传递适合的时间戳签名和替换文本。

请求目录

在您开始编辑之前,自动命令也是很有用的。例如,当您开始编辑一个新文档时,您可能偶尔会看到这样的信息:

"dir/subdir/filename" [New DIRECTORY]

这就表示您所指定的文件(这里是 filename)不存在,它所应该在的目录(这里是 dir/subdir )也不存在。

Vim 将很乐意允许您忽略这个警告(很多用户甚至没有意识到这是一个警告),继续编辑文件。但是当您想要保存它时,您会遇到以下没有帮助的错误信息:

"dir/subdir/filename" E212: Can't open file for writing.

现在,为了保存您的工作,您不得不在编写文件写入之前,创建一个缺失的目录。您可以在 Vim 中这么做:

:write
"dir/subdir/filename" E212: Can't open file for writing.
:call mkdir(expand("%:h"),"p")
:write

这里,内置的 expand() 函数调用应用于 "%:h",其中 % 表示当前文件路径(这里是 dir/subdir/filename ),并且 :h 只提取了这个路径的 “头”,删除了文件名称,留下了预期目录(dir/subdir)。然后,对 Vim 的内置 mkdir() 的调用读取这个目录路径,按这个路径创建所有临时目录(按第二参数要求,"p")。

事实上,虽然大多数 Vim 用户更愿意简单地转到 shell 来创建所需的目录。例如:

:write
"dir/subdir/filename" E212: Can't open file for writing.
:! mkdir -p dir/subdir/
:write

任何一种方法都很麻烦。如果您最终不得不创建缺失的目录,那么为什么不提前让 Vim 通知它不存在,然后在您开始之前简单完成创建?这样,您就永远不会遇到意味不明的 [New DIRECTORY] 提示;您的工作流程稍后也不会被一个同样神秘的 E212 错误所中断。

要让 Vim 负责提前创建不存在的目录,您可以将一个处理器连接到 BufNewFile 事件,当您开始编辑一个不存在的文档时,它就会进行排列。清单 17 显示了您需要添加到 .vimrc 文件来实现此功能的代码。

清单 17. 无条件自动创建不存在的目录
augroup AutoMkdir
    autocmd!
    autocmd  BufNewFile  *  :call EnsureDirExists()
augroup END
function! EnsureDirExists ()
    let required_dir = expand("%:h")
    if !isdirectory(required_dir)
        call mkdir(required_dir, 'p')
    endif
endfunction

AutoMkdir 组设置了任意种类文件上 BufNewFile 事件的单一自动命令,当编辑一个新文件时,调用 EnsureDirExists() 函数。EnsureDirExists() 首先通过展开当前路径的 “头” 来确定正被请求的目录:expand("%:h")。然后它使用内置的 isdirectory() 函数来检查请求的路径是否存在。如果不存在,它就会试着使用 Vim 内置的 mkdir() 创建目录。

要注意的是,如果 mkdir() 调用由于某种原因不能创建请求的目录,它就会生成一个更准确,信息更丰富的错误信息:

E739: Cannot create directory: dir/subdir

更谨慎地请求目录

这个解决方案唯一的问题就是,有时,自动创建不存在的子目录真是最不应该做的事情。例如,假设您进行如下请求:

> vim /share/sites/corporate/root/.htaccess

您打算在现存的子目录 /share/corporate/website/root/ 中创建一个新的访问控制文件。当然,由于您弄错了路径,您实际所作是在之前不存在的子目录 /share/website/corporate/root/ 中创建一个新的访问控制文件。因为那是自动发生的,没有任何警告,至少在误用的访问控制导致某些在线灾难之前,您甚至可能没有意识到这个错误。

为了避免这样的问题,您或许希望 Vim 在自动创建缺失的目录方面不要那么有用。清单 18 显示了 EnsureDirExists() 一个更细致的版本,它会继续检测缺失的目录,但现在询问用户要对其进行何种操作。要注意的是,自动命令设置和清单 17 中的一摸一样;只有EnsureDirExists() 函数发生了改变。

清单 18. 有条件地自动创建不存在的目录
augroup AutoMkdir
    autocmd!
    autocmd  BufNewFile  *  :call EnsureDirExists()
augroup END
function! EnsureDirExists ()
    let required_dir = expand("%:h")
    if !isdirectory(required_dir)
        call AskQuit("Directory '" . required_dir . "' doesn't exist.", "&Create it?")

        try
            call mkdir( required_dir, 'p' )
        catch
            call AskQuit("Can't create '" . required_dir . "'", "&Continue anyway?")
        endtry
    endif
endfunction

function! AskQuit (msg, proposed_action)
    if confirm(a:msg, "&Quit?\n" . a:proposed_action) == 1
        exit
    endif
endfunction

EnsureDirExists() 函数的这个版本像之前一样,对所需的目录进行定位,并检测它是否存在。然而,如果目录不存在,EnsureDirExists() 现在就调用一个助手函数:AskQuit()。这个函数使用内置的 confirm() 函数来询问您是否想要退出会话或者自动创建目录。"Quit?" 作为第一选项显示,这也使它成为默认值,如果您只是敲击  键的话。

如果您选择了 "Quit?" 选项,助手函数就立即终止 Vim 会话。否则,助手函数就简单返回。在这种情况下,EnsureDirExists() 继续执行,并试着调用 mkdir()

然而,要注意的是,mkdir() 调用现在在一个 try...endtry 结构中。这就是 —— 如您所期待的 —— 一个异常处理器,它现在将会捕捉如果 mkdir() 不能创建请求的目录而被抛出的 E739 错误。

当那个错误被抛出,catch 块将会拦截它,并再次调用 AskQuit(),通知您不能创建目录,并询问您是否依然想要继续。想要更详细地了解 Vim 的大量异常处理机制,请看::help exception-handling

EnsureDirExists() 第二版的总体效果是强调不存在、但是需要您明确请求创建的目录(通过在提示时输入一个 ‘c’ 创建)。如果不能创建目录,您就会被再次警告,并让您选择是否无论如何都要继续会话(当被询问时再次输入一个 'c')。这也相当轻松地避免了错误编辑(只需在任意提示时敲击  键来选择默认的 "Quit?" 选项)。

当然,您或许喜欢将继续作为默认值,在这种情况下,您只需要改变 AskQuit() 的第一行:

if confirm(a:msg, a:proposed_action . "\n&Quit?") == 2

这样,您提议的行为就会是第一选择,也就是默认行为。要注意,"Quit?" 现在是第二选择,所以这个响应现在需要和值 2 进行比较。

展望未来

通过自动化重复操作,您就不必亲力亲为,自动命令能够帮您节省大量的努力,避免大量的错误。着手开始的一个有效方法就是在编辑时心理上退后一步,注意通过使用 Vim 的事件处理机制,让使用的重复模式适当地自动化。将这些模式脚本化到自动命令或许需要一些预先的准备,但是自动化的行为每天都会回报您的投资。通过自动化每天的操作,您将会节省时间和精力,避免错误,使工作流程更顺畅,清除琐碎的压力,从而提高您的效率。

虽然您的自动命令开始时可能只能进行一些简单的单行自动操作,随着您思考使用 Vim 更好地完成更多繁琐的工作,您很快就会发现您能重新设计和定制它们。通过这种方式,您的事件处理器逐步会变得更智能、更安全、更完美地适应您的工作方式。

然而,随着这些类似的 Vim 脚本变得更复杂,您还需要更好的工具来管理它们。每当您设计一个聪明的新键盘映射或者自动命令,在您的.vimrc 中添加 10 至 20 行代码将会最终产生一个长达千行……完全不可维护的配置文件。

所以,在这个系列的下一篇文章中,我们会探讨 Vim 的简单插件架构,它允许您析出 .vimrc 的一部分,并将其隔离在单独的模块中。我们将会了解那个插件系统如何通过开发一个独立模块来运行,改善使用 XML 的一些担心。

参考资料

学习

  • 从本系列第一篇文章:“使用脚本编写 Vim 编辑器,第 1 部分:变量、值和表达式”(developerWorks,2009 年 5 月)开始学习 Vimscript 和扩展 Vim 编辑器的嵌入式语言。
  • 查看以下资源继续学习有关 Vim 编辑器和它的很多命令:
    • Vim 主页
    • 在线图书 A Byte of Vim
    • 关于 Vim 的各种图书
    • Vim 手册
    • Steve Oualline 的 Vim Cookbook
  • 要获得 Vimscript 脚本的大量例子,请查看:
    • Vim Tips wiki
    • Vimscript 归档
  • from: https://www.ibm.com/developerworks/cn/linux/l-vim-script-5/

你可能感兴趣的:(Vim/Emacs,脚本编写,Vim,编辑器,事件驱动,教程,开发)