Jade Dungeon

vimscript缓冲区

本地缓冲区缩写(iabbrev)

---替换为Hello,只在当前缓冲区生效:

:iabbrev <buffer> --- Hello

自动命令和缩写

使用本地缓冲区的缩写和自动命令来创建一个简单的snippet系统。

执行下面的命令:

:autocmd FileType python     :iabbrev <buffer> iff if:<left>
:autocmd FileType javascript :iabbrev <buffer> iff if ()<left>

打开一个Javascript文件然后输入iff缩写。 然后再打开一个Python文件试试。Vim会依据文件类型在当前行执行合适的缩写。

自动命令组

自动命令会有意料之外重复执行的问题:

:autocmd BufWrite * :echom "Writing buffer!"

现在使用:write命令将当前缓冲区写入文件,然后执行:messages命令查看消息日志。 你会看到Writing buffer!在消息列表中。

然后将当前缓冲区写入文件,执行:messages查看消息日志。 你会看到Writing buffer!在消息列表中出现了两次。

现在再次执行上面的自动命令:

:autocmd BufWrite * :echom "Writing buffer!"

再次将当前缓冲区写入文件并执行:messages命令。 你会看到Writing buffer!在消息列表中出现了4次。

这是因为当你以上面的方式创建第二个自动命令的时候, Vim没办法知道你是想替换第一个自动命令。在上面的示例中, Vim创建了两个不同的自动命令,并且这两个命令刚好做同样的事情。

问题是当你加载你的~/.vimrc文件的时候,Vim会重新读取整个文件, 包括你所定义的任何自动命令!这就意味着每次你加载你的~/.vimrc文件的时候, Vim都会复制之前的自动命令,这会降低Vim的运行速度, 因为它会一次又一次地执行相同的命令。

有些老用户的~/.vimrc文件可以轻易达到1000行,其中会有很多自动命令。 再加上安装的插件中的自动命令,这肯定会影响Vim的速度。

把自动命令放到组中(augroup)

对于这个问题,Vim有一个解决方案。 这个解决方案的第一步是将相关的自动命令收集起来放到一个已命名的组(groups)中。

新开一个Vim实例,这样可以清除之前所创建的自动命令。然后运行下面的命令:

:augroup testgroup
:    autocmd BufWrite * :echom "Foo"
:    autocmd BufWrite * :echom "Bar"
:augroup END

当你多次使用augroup的时候,Vim每次都会组合那些组。(注意,是组合,不是替换)

现在执行下面的命令:

:augroup testgroup
:    autocmd BufWrite * :echom "Baz"
:augroup END

清除自动命令组

如果你想清除一个组,你可以把autocmd!这个命令包含在组里面。执行下面的命令:

:augroup testgroup
:    autocmd!
:    autocmd BufWrite * :echom "Cats"
:augroup END

现在试试写入文件然后执行:messages查看消息日志。 这次Vim只会输出Cats在消息列表中。

在Vimrc中使用自动命令

可以使用这种方式将自动命令添加到~/.vimrc中, 这样每次加载它的时候就不会复制自动命令了。

添加下面的命令到你的~/.vimrc文件中:

augroup filetype_html
    autocmd!
    autocmd FileType html nnoremap <buffer> <localleader>f Vatzf
augroup END

当进入filetype_html这个组的时候,我们会立即清除这个组,然后定义一个自动命令, 然后退出这个组。当我们再次加载~/.vimrc文件的时候, 清除组命令会阻止Vim添加一个一模一样的自动命令。

更多命令组信息

:help autocmd-groups

Operator-Pending映射

VIM里很多操作都是一个操作(Operator)加一个移动(Movement)两种命令的组合。 比如:

按键   操作       移动          
----   --------   ------------- 
dw     删除       到下一个单词  
ci(    修改       在括号内
yt,    复制       到逗号

Movement映射

Vim允许你创建任何新的movements,这些movements可以跟所有命令一起工作。

比如原来要删除在括号范围内的内容:

di(

现在想要把i(这个代表在括号范畴内的移动操作定义一个快捷映射, 可以用oonremap

:onoremap p i(

应用时,如要要删除以下括号中文字:

test (apple cat doc) test

把光标放到单词cat上,然后敲击dp。Vim会删除括号内的所有文字。 你可以把这个新建的movements当作「参数」。

onoremap命令会告诉Vim当它在等待一个要附加在operator后面的movement的时候, 如果这个movement是p的话,它会把它当作i(。所以当我们在运行dp的时候, 就象是在对Vim说delete parameters,而Vim会把它理解为「在括号内删除」。

回到刚刚的例子:

test (apple cat doc) test

把光标放到单词cat上,然后敲击cp。这次又会发生什么? Vim会删除括号中的所有文字,不过这一次删除之后Vim会处于插入模式, 这是因为你使用的是change,而不是delete

再看一个示例。执行下面的命令:

:onoremap b /return<cr>

现在把下面的文字输入到缓冲区:

def count(i):
    i += 1
    print i

    return foo

把光标放到第二行的i上,然后按下db。会发生生么? Vim把整个函数体中直到return上面的内容都删除了, return就是上面的映射使用Vim的通用查找得到的结果。

当你想搞清楚怎么定义一个新的operator-pending movement的时候, 你可以从下面几个步骤来思考:

  1. 在光标所在的位置开始。
  2. 进入可视模式(charwise)。
  3. ...把映射的按键放到这里...
  4. 所有你想包含在movement中的文字都会被选中。

你所要做的工作就是在第三步中填上合适的按键。

改变开始位置

你可能已经从上面所学的东西中意识到一个了问题。 如果我们定义的movements都是从光标所在的位置开始的话, 那么这就会限制我们做一些我们想使用movement来做的事情。

但是Vim并不会限制你去做你想做的事情,所以对于这个问题肯定有解决办法。 执行下面的命令:

:onoremap in( :<c-u>normal! f(vi(<cr>

这个命令看起来有些复杂,不过我们还是先试试它能干什么。将下面的文字输入缓冲区:

print foo(bar)

把光标放到单词print上面,然后敲击cin(

Vim会删除括号内的内容然后进入插入模式,并且光标会停留在括号的中间。

你可以将这个映射理解为「在下一个括号内」(inside next parentheses)。 它会对当前行光标所在位置的下一个括号内的文本执行operator。

我们再创建一个「在上一个括号内」(inside last parentheses)的movement进行对照。 (在这里使用「前一个」(previous)可能更准确, 但这会覆盖「段落」(paragraph)movement)

:onoremap il( :<c-u>normal! F)vi(<cr>

先试试确保这个命令可以工作。

那么这些映射是怎么工作的呢?首先,<c-u>比较特殊, 可以先不用管(你只需要相信我这个东西可以让这个映射在任何情况下都能正常工作)。 如果我们删除它的话,这个映射会变成这个样子:

:normal! F)vi(<cr>

:normal!会在后面的章节谈到,现在你只需要知道它可以在常用模式下模拟按下按键。 例如,运行:normal! dddd会删除两行,就像按下dddd。 映射后面的<cr>是用来执行:normal!命令的。

那么现在我们可以认为这个映射的关键是运行下面这些按键组成的命令:

F)vi(

这个命令很容易理解:

  • F): 向后移动到最近的)字符。
  • vi(: 进入可视模式选择括号内的所有内容。

这个movement结束在在可视模式下选择中我们想操作的文本, 然后Vim会对选中的文本执行操作,就像通常情况一样。

一般规则

下面两条规则可以让你可以很直观的以多种方式创建operator-pending映射:

  • 如果你的operator-pending映射以在可视模式下选中文本结束,Vim会操作这些文本。
  • 否则,Vim会操作从光标的原始位置到一个新位置之间的文本。

其他

:help omap-info

例:定位Markdown的标题

对于以下的Markdown文本:

Topic One
=========

This is some text about topic one.

It has multiple paragraphs.

Topic Two
=========

This is some text about topic two.  It has only one paragraph.

使用多个=作为「下划线」的行会被Markdown当作标题。我们现在创建一些映射, 这些映射可以让我们通过movements定位到标题。运行下面的命令:

:onoremap ih :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>

这个映射有些复杂。现在把你的光标放到文本中的某个位置(不要放到标题上), 然后敲击cih。Vim会删除光标所在章节的标题,然后保持在插入模式, 这可以称为「修改所在的标题」(change inside heading)。

这里使用了一些我们之前从来没有见过的东西, 所以我们有必要单独分析下每一部分的含义。

  • 这个映射的第一部分, :onoremap ih是映射命令,这个我们很熟悉了,无需多言。
  • <c-u>上一章讲过,我们现在也不深究。

接着看看剩下的部分:

:execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>

normal

:normal命令的后面会跟着一串字符,无论这些字符表示什么含义, :normal命令都会执行它们,就像是在常用模式下敲击这些字符一样。

简单的说明一下。执行下面的命令:

:normal gg

Vim会将光标跳转到文件的顶部。现在执行下面的命令,Vim将缩进当前行:

:normal >>

normal后面的!的功能以后再讨论。

execute

execute命令后面会跟着一个Vim脚本字符串,然后把这个字符串当作一个命令执行。 执行下面的命令:

:execute "write"

Vim会写文件,就像你已经输入了:write<cr>一样。现在执行下面的命令:

:execute "normal! gg"

Vim会执行:normal! gg,这个会将光标跳转到文件的顶部,跟之前一样。

现在问题来了,我们为什么要搞得这么蛋疼,又是execute,又是normal!, 直接执行normal!不就可以搞定么?

看看下面的命令,猜猜它会干啥:

:normal! gg/a<cr>

这个命令似乎会做下面的一些事情:

  • 将光标跳转到文件的顶部。
  • 准备搜索。
  • a当作目标字符串进行搜索。
  • 按下return键执行搜索。

你自己执行一下,Vim会将光标跳转到了文件顶部,然后……没有然后了!

出错的原因是normal!不能识别「特殊字符」,例如这里的<cr>。 这个问题有很多办法可以搞定,最简单的就是使用execute, 另外使用execute也会让脚本更易读。

execute碰到任何你想让它执行的字符串的时候。 它会先替换这个字符串中的所有特殊字符。在这个示例中,\r是一个转义字符, 它表示的是「回车符(carriage return)」。两个反斜线也是一个转义字符, 它会将一个反斜线当作一般字符放到字符串中。

如果我们按照上面的分析替换这个映射中的特殊字符,然后就可以搞清楚这个映射会怎么执行:

:normal! ?^==\+$<cr>:nohlsearch<cr>kvg_
                ^^^^           ^^^^
                 ||             ||
这里的`<cr>`实际上是一个回车符,而不是由4个字符——「左尖括号」,
`c`,`r`和「右尖括号」组成的字符串。

所以现在normal!会执行这些字符,如同我们是在常用模式下敲击了它们一样。 我们以回车符对这些字符进行拆分,然后看看它们是怎么执行的:

?^==\+$
:nohlsearch
kvg_

第一部分?^==\+$会向后搜索任何由两个或多个等号组成的行, 这些行不会包含除等号外的任何其他字符。 这个命令执行后会让光标停留在符合搜索条件的行的行首。

之所以使用向后搜索,是因为当你想「修改所在的标题」(change inside heading) 的时候,而当前光标是位于某一节的文本上,那么你最可能想做的是修改这一节的标题, 而不是下一节的标题。

第二部分是:nohlsearch命令。这个命令只是简单的清除之前的搜索结果的高亮显示, 防止这些高亮显示分散我们的注意。

最后一部分是三个常用模式下的命令的序列:

  • k:向上移动一行。第一部分已经将光标定位到了等号符号组成的行的第一个字符, 所以执行这个命令后光标就会被定位到标题的第一个字符上。
  • v:进入可视模式(characterwise)。
  • g_:移动到当前行的最后一个非空字符上。这里没有使用$, 是因为$会选中换行符号,这不是我们所想要的。

例:删除Markdown的标题

再来看一个映射。执行下面的命令:

:onoremap ah :<c-u>execute "normal! ?^==\\+\r:nohlsearch\rg_vk0"<cr>

把光标放到某一节的文字上,然后敲击cah试试看。这一次Vim不仅会删除这一节的标题, 而且还会删除跟这个标题相连的等号组成的行。 你可以把这个movement当作是「环绕一节的标题」(around this section's heading)。

这个映射有什么不同呢?让我们对照之前的映射看一下:

:onoremap ih :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>
:onoremap ah :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rg_vk0"<cr>

唯一的不同是映射的后面用于选择文本的部分:

inside heading: kvg_
around heading: g_vk0

其他的部分都是一模一样的,所以我们现在从将光标定位到等号组成的行的第一个字符的那个部分开始进行讲解:

  • g_:移动到当前行(译注:等号组成的行)的最后一个非空字符。
  • v:进入可视模式(characterwise)。
  • k:向上移动一行。这会将光标移动到包含标题文字的行上。
  • 0:移动到这一行(译注:标题行)的第一个字符。

这一系列命令的执行结果就是在可视模式下同时选中标题的文字和等号组成的行, 然后Vim会在这两行上执行相应的操作。

其他

  • :help pattern-overview
  • :help normal
  • :help execute
  • :help expr-quote

状态栏

通过设置statusline选项来进行自定义。执行下面的命令:

:set statusline=%f

你可以在状态条上看到当前所编辑文件的路径(相对于当前路径)。再执行这个命令:

:set statusline=%f\ -\ FileType:\ %y
  • %f会被替换为文件名
  • %y会被替换为文件类型
  • 状态条中的空格需要反斜线进行转义

例:与文件名相关:

:set statusline=%f         " 文件的路径
:set statusline+=\ -\      " 分隔符
:set statusline+=FileType: " 标签
:set statusline+=%y        " 文件的类型

例,与行号相关,显示成12/223这个样子:

:set statusline=%l    " 当前行号
:set statusline+=/    " 分隔符
:set statusline+=%L   " 总行数

宽度和边距

可以在%后面添加额外的字符来改变状态条中信息的显示样式。执行下面的命令:

:set statusline=[%4l]

现在状态条中的文件行数会至少显示为4个字符的宽度(例如:[ 12]), 这可以用于防止状态条中的文字总是令人厌烦地跳来跳去。

默认情况下在值的左边添加边距。执行下面的命令:

:set statusline=Current:\ %4l\ Total:\ %4L

你的状态条看来会是这个样子:

Current:   12 Total:  223

你可以使用-将边距添加在右边,而不是左边。执行下面的命令:

:set statusline=Current:\ %-4l\ Total:\ %-4L

现在你的状态条看起来会是这个样子:

Current: 12   Total: 223

这样就好看多了,因为数字值是紧挨着它的标签的。

对于会被显示为数字的代码,你可以让Vim使用0代替空格来填充边距。执行下面的命令:

:set statusline=%04l

现在当光标位于第12行的时候你的状态条会显示0012

最后,你可以设置一个代码所要输出的值的最大宽度。执行下面命令:

:set statusline=%F

%F会显示当前文件的完整路径。现在执行下面的命令改变最大宽度:

:set statusline=%.20F

如果有必要路径会被删简,像下面这样:

<hapters/17.markdown

这可以用于防止路径或者是其他的很长的代码占用整个行。

通用格式

阅读:help statusline查看状态条中代码的通用格式:

%-0{minwid}.{maxwid}{item}

除了%item外其他都是可选的。

分割

我们不会探讨状态条的更多细节 (Vim的文档有非常详细的说明,如果你想学到更多,建议阅读它们), 不过我们会介绍一个简单的代码,这个代码可以立即带来价值。执行下面的命令:

:set statusline=%f         " 文件的路径
:set statusline+=%=        " 切换到右边
:set statusline+=%l        " 当前行
:set statusline+=/         " 分隔符
:set statusline+=%L        " 总行数

现在状态条的左边会包含文件的路径,当前行/总行数会显示在状态条的右边。 %=这个代码告诉Vim所有在此之后要在状态条上显示的信息都应该右对齐 (作为一个整体),而不是左对齐。

练习

浏览:help statusline中的可用代码。先别在意那些你现在理解不了的代码。

编辑你的~/.vimrc文件创建一个自定义的状态条。 确保在set中使用+=来一条一条地定义要显示的代码, 并且每一行的设置添加注释来说明每一条的含义。

尝试使用自动命令和setlocal来为不同的文件定义不同的状态条。 确保使用了自动命令组防止自动命令被重复创建(永远记住)。