Potion 段落移动

既然已经了解了段落移动是如何工作的, 那就让我们在 Potion 文件中把这些命令以某种方式添加到映射中来。

首先,我们需要决定什么是 Potion 文件中的一个“段落”。 现在有两对段落移动命令,所以我们可以提供两种“方案”,而用户可以随意使用一种自己喜欢的。

我们用以下两种方案来定义 Potion 段落开始的位置:

  1. 任何在空行下面、并且首字符不是空白符的行,或者文件的第一行。
  2. 任何首字符不是空白符、包含一个等号、并且以冒号结尾的行。

以下是稍微扩充后的 factorial.pn 文件,并且展示了这些规则会把哪些行作为段落头部:

# factorial.pn                              1
# Print some factorials, just for fun.

factorial = (n):                            1 2
    total = 1

    n to 1 (i):
        total *= i.

    total.

print_line = ():                            1 2
    "-=-=-=-=-=-=-=-\n" print.

print_factorial = (i):                      1 2
    i string print
    '! is: ' print
    factorial (i) string print
    "\n" print.

"Here are some factorials:\n\n" print       1

print_line ()                               1
10 times (i):
    print_factorial (i).
print_line ()

第一种定义更宽松。它把段落大致定义为一段“最高层级的文本块”。

第二种定义更严格。它把段落定义为(实际上就是)函数的定义。

自定义映射

在插件仓库中创建 ftplugin/potion/sections.vim 文件。 我们将把段落移动相关的代码放在里面。 记住,当缓冲的 filetype 被设为 potion 时,这些代码就会被运行。

我们要把所有4个段落移动命令都重新映射,所以,先创建一个“纲要”文件:

noremap <script> <buffer> <silent> [[ <nop>
noremap <script> <buffer> <silent> ]] <nop>

noremap <script> <buffer> <silent> [] <nop>
noremap <script> <buffer> <silent> ][ <nop>

注意,我们用了 noremap 命令,而不是 nnoremap,因为我们想让它们在操作符等待模式下也能使用。 这样就可以用类似 d]] 的操作来“删除当前位置到下一个段落之间的内容”。

我们设置的是本地缓冲映射,所以只会应用在 Potion 文件中,而不会取代全局的。

我们也设置了静默选项,因为用户并不关心我们是如何在段落间移动的。

使用函数

这些不同段落移动命令的代码实现基本类似,所以,我们把它抽象成一个函数,以供映射调用。

在很多 Vim 插件中,你都能看到创建很多类似的映射,用的都是这种策略。 比起把所有功能都塞进映射的代码中,这种策略更易于阅读和维护。

sections.vim 文件改成如下:

function! s:NextSection(type, backwards)
endfunction

noremap <script> <buffer> <silent> ]]
        \ :call <SID>NextSection(1, 0)<cr>

noremap <script> <buffer> <silent> [[
        \ :call <SID>NextSection(1, 1)<cr>

noremap <script> <buffer> <silent> ][
        \ :call <SID>NextSection(2, 0)<cr>

noremap <script> <buffer> <silent> []
        \ :call <SID>NextSection(2, 1)<cr>

我们使用了 Vimscript 的续行特性,因为这些行对我而言有点太长了。 注意反斜杠是如何在第二行开始处转义的。阅读 :help line-continuation 以了解更多。

注意,我们用了 <SID> 和一个脚本本地的函数来避免我们的辅助函数污染全局命名空间。

每个映射只是简单地结合适当的参数来调用 NextSection 并实现移动。 现在我们可以开始实现 NextSection 了。

基本移动

让我们想想这个函数需要做些什么。 我们要把光标移到下一个“段落”,而用 /? 命令来移动光标是一种简便的方式。

NextSection 编辑成如下这样:

function! s:NextSection(type, backwards)
    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . 'foo' . "\r"
endfunction

这个函数用到了之前见过的 execute normal! 模式来实现 /foo 或者 ?foo,取决于 backwards 的值。 这是个不错的开始。

继续,我们显然需要搜索些其他的内容,而非只是 foo, 而模式具体的内容取决于我们是想用第一种段落头部的定义还是第二种。

NextSection 修改成如下这样:

function! s:NextSection(type, backwards)
    if a:type == 1
        let pattern = 'one'
    elseif a:type == 2
        let pattern = 'two'
    endif

    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . pattern . "\r"
endfunction

现在我们只需要填充模式的内容,让我们继续来完成它。

最高层级文本段落

把第一行 let pattern = '...' 替换成下面的内容:

let pattern = '\v(\n\n^\S|%^)'

要理解这个正则表达式是如果工作的,只要记住要实现的“段落”定义:

任何在空行下面、并且首字符不是空白符的行,或者文件的第一行。

开始处的 \v 只是强制开启“very magic”模式,就像之前多次见过的那样。

剩余的是两个选项组成的一个组。 第一个是 \n\n^\S,搜索“一个换行符,随后再是一个换行符,随后是一个非空字符串”。 这会找到定义中第一种的行。

另一个是 %^,这是 Vim 中一种特殊的原子正则表达式,它表示“文件的开始”。

现在我们可以先试试前面这两个映射。 在 Potion 样例的缓冲中保存 ftplugin/potion/sections.vim:set filetype=potion[[]] 命令应该能工作,但是有点奇怪。

搜索标志

你会发现在段落间移动时,光标会停留在我们实际想去位置上方的空行处。 在继续阅读之前,仔细想想为什么会这样。

答案是我们使用了 /(或者 ?)来搜索,而默认情况下,Vim 会把光标停留在匹配的开始处。 例如,用 /foo 时,光标会停留在 foof 处。

要告诉 Vim 把光标放到匹配的结尾而非开始处,那就要使用一个搜索标志。 试试在 Potion 文件中这样搜索:

/factorial/e

Vim 会找到 factorial 单词,并移动到那。多按几次 n 在匹配间移动。 e 标志会告诉 Vim 把光标放到匹配的结尾处而不是开始处。反方向再试试看:

?factorial?e

我们在函数中添加这个搜索标志,让光标停留在段落匹配的结尾处:

function! s:NextSection(type, backwards)
    if a:type == 1
        let pattern = '\v(\n\n^\S|%^)'
        let flags = 'e'
    elseif a:type == 2
        let pattern = 'two'
        let flags = ''
    endif

    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction

我们修改了两处地方。第一,我们根据段落移动类型设置了一个 flags 变量。 目前,我们只考虑了第一种类型,它需要一个 e 标志。

第二,我们把 dirflags 连结在一起搜索字符串。 这会添加 ?e/e,取决于我们往哪个方向搜索。

保存文件,切换回 Potion 样例文件中,并运行 :set ft=potion 让修改生效。 现在试试 [[]],可以看到它们能正常工作了!

函数的定义

现在是时候来处理第二种“段落”的定义了,而且幸运的是这种比第一种更直接。 回想下需要实现的定义:

任何首字符不是空白符、包含一个等号、并且以冒号结尾的行。

我们可以用一个相当简单的正则表达式来找到这些行。 把函数中第二行 let pattern = '...' 改成这样:

let pattern = '\v^\S.*\=.*:$'

这个正则表达式看上去应该没有上一个可怕。 我把它作为练习留给你去弄明白它是如何工作的 —— 它基本就是我们定义的直译。

保存文件,在 factorial.pn 中运行 :set filetype=potion,并试试新的 ][[] 映射。 它们应该按预期工作。

这里我们并不需要一个搜索标志,因为把光标放在匹配的开始处(默认行为)就挺好的。

可视化模式

我们的段落移动命令在普通模式下能很好的工作,但要在可视化模式中也能正常工作就需要一些额外的代码。 首先,把函数改成如下这样:

function! s:NextSection(type, backwards, visual)
    if a:visual
        normal! gv
    endif

    if a:type == 1
        let pattern = '\v(\n\n^\S|%^)' 
        let flags = 'e'
    elseif a:type == 2
        let pattern = '\v^\S.*\=.*:$'
        let flags = ''
    endif

    if a:backwards
        let dir = '?'
    else
        let dir = '/'
    endif

    execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction

有两处修改。第一,函数接受一个额外的参数,这样才能知道是否是在可视化模式中调用的。 第二,如果是在可视化模式中调用的,运行 gv 来恢复可视化选择。

我们为什么需要这样做?让我们试一个例子就能明白了。 在随意一个缓冲中,可视化选择一些文本,并运行以下命令:

:echom "hello"

Vim 会显示 hello,但可视化选择也会被清除!

在用 : 运行一个命令时,可视化选择总是会被清除。 gv 命令会重新选择之前的可视化选择,所以这会“撤销”之前的清除。 这是个很有用的命令,在日常的工作中也很方便。

现在,我们需要更新现有的映射,为新的 visual 参数传入 0

noremap <script> <buffer> <silent> ]]
        \ :call <SID>NextSection(1, 0, 0)<cr>

noremap <script> <buffer> <silent> [[
        \ :call <SID>NextSection(1, 1, 0)<cr>

noremap <script> <buffer> <silent> ][
        \ :call <SID>NextSection(2, 0, 0)<cr>

noremap <script> <buffer> <silent> []
        \ :call <SID>NextSection(2, 1, 0)<cr>

这并不复杂。然后要添加可视化模式的映射,这是这个难题的最后一部分了:

vnoremap <script> <buffer> <silent> ]]
        \ :<c-u>call <SID>NextSection(1, 0, 1)<cr>

vnoremap <script> <buffer> <silent> [[
        \ :<c-u>call <SID>NextSection(1, 1, 1)<cr>

vnoremap <script> <buffer> <silent> ][
        \ :<c-u>call <SID>NextSection(2, 0, 1)<cr>

vnoremap <script> <buffer> <silent> []
        \ :<c-u>call <SID>NextSection(2, 1, 1)<cr>

这些映射都为 visual 参数传入 1,告诉 Vim 在执行移动前重新选择最后一次的选择。 它们还使用了我们在 grep 操作符章节中学到的 <c-u> 技巧。

保存文件,在 Potion 文件中运行 :set ft=potion,这样你就完成了! 试试这些新映射。类似 v]]d[] 这样的,现在都能正常工作了。

为何如此费劲?

这是一个很长的章节,而只讲到了些看似很小的功能, 但是一路下来,你已经学习并实践了很多有用的东西:

继续做完后面的练习(只是阅读些 :help),然后吃些冰淇淋。这是你应得的!

练习

阅读 :help search()。这是一个很有用的函数,但是你仍然可以使用 /? 命令,并结合文档中列出来的标志。

阅读 :help ordinary-atom 来了解更多在搜索模式中能使用的有趣的东西。

原文地址:http://learnvimscriptthehardway.stevelosh.com/chapters/51.html