Vimscript 高級折疊

2018-02-24 16:03 更新

在上一章里我們用Vim的indent折疊方式,在Potion文件中增加了一些快捷而骯臟的折疊。

打開factorial.pn并用zM關(guān)閉所有的折疊。文件現(xiàn)在看起來就像這樣:

factorial = (n):
+--  5 lines: total = 1

10 times (i):
+--  4 lines: i string print

展開第一個折疊,它看上去會是這樣:

factorial = (n):
    total = 1
    n to 1 (i):
+---  2 lines: # Multiply the running total.
    total.

10 times (i):
+--  4 lines: i string print

這真不錯,但我個人喜歡依照內(nèi)容來折疊每個塊的第一行。 在本章中我們將寫下一些自定義的折疊代碼,并在最后實(shí)現(xiàn)這樣的效果:

factorial = (n):
    total = 1
+---  3 lines: n to 1 (i):
    total.

+--  5 lines: 10 times (i):

這將更為緊湊,而且(對我來說)更容易閱讀。 如果你更喜歡indent也不是不行,不過最好學(xué)習(xí)本章來對Vim中實(shí)現(xiàn)折疊的代碼的更深入的了解。

折疊原理

為了寫好自定義的折疊,我們需要了解Vim對待("thinks")折疊的方式。簡明扼要地講解下規(guī)則:

  • 文件中的每行代碼都有一個"foldlevel"。它不是為零就是一個正整數(shù)。
  • foldlevel為零的行_不會_被折疊。
  • 有同等級的相鄰行會被折疊到一起。
  • 如果一個等級X的折疊被關(guān)閉了,任何在里面的、foldlevel不小于X的行都會一起被折疊,直到有一行的等級小于X。

通過一個例子,我們可以加深理解。打開一個Vim窗口然后粘貼下面的文本進(jìn)去。

a
    b
    c
        d
        e
    f
g

執(zhí)行下面的命令來設(shè)置indent折疊:

:setlocal foldmethod=indent

花上一分鐘玩一下折疊,觀察它是怎么工作的。

現(xiàn)在執(zhí)行下面的命令來看看第一行的foldlevel:

:echom foldlevel(1)

Vim顯示0?,F(xiàn)在看看第二行的:

:echom foldlevel(2)

Vim顯示1。試一下第三行:

:echom foldlevel(3)

Vim再次顯示1。這意味著第2,3行都屬于一個level1的折疊。

這是每一行的foldlevel:

a           0
    b       1
    c       1
        d   2
        e   2
    f       1
g           0

重讀這一部分開頭的幾條規(guī)則。打開或關(guān)閉每個折疊,觀察foldlevel,并確保你理解了為什么會這樣折疊。

一旦你已經(jīng)自信地認(rèn)為你理解了每行的foldlevel是怎么影響折疊結(jié)構(gòu)的,繼續(xù)看下一部分。

首先:做一個規(guī)劃

在我們埋頭敲鍵盤之前,先為我們的折疊功能規(guī)劃出幾條大概的規(guī)則。

首先,同等縮進(jìn)的行應(yīng)該要折疊到一塊。我們也希望_上_一行也一并折疊,達(dá)到這樣的效果:

hello = (name):
    'Hello, ' print
    name print.

將折疊成這樣:

+--  3 lines: hello = (name):

空行應(yīng)該算入_下_一行,因此折疊底部的空行不會包括進(jìn)去。這意味著類似這樣的內(nèi)容:

hello = (name):
    'Hello, ' print
    name print.

hello('Steve')

將折疊成這樣:

+--  3 lines: hello = ():

hello('Steve')

而_不是_這樣:

+--  4 lines: hello = ():
hello('Steve')

這當(dāng)然是屬于個人偏好的問題,但現(xiàn)在我們就這么定了。

開始

現(xiàn)在開始寫我們的自定義折疊代碼吧。 打開Vim,分出兩個分割,一個是ftplugin/potion/folding.vim,另一個是示例代碼factorial.pn。

在上一章我們關(guān)閉并重新打開Vim來使得folding.vim生效,但其實(shí)還有更簡單的方法。

不要忘記每當(dāng)設(shè)置一個緩沖區(qū)的filetypepotion的時候,在ftplugin/potion/下的所有文件都會被執(zhí)行。 這意味著僅需在factorial.pn的分割下執(zhí)行:set ft=potion,Vim將重新加載折疊代碼!

這比每次都關(guān)閉并重新打開文件要快多了。 唯一需要銘記的是,你得保存folding.vim到硬盤上,否則未保存的改變不會起作用。

Expr折疊

為了獲取折疊上的無限自由,我們將使用Vim的expr折疊。

我們可以繼續(xù)并從folding.vim移除foldignore,因?yàn)樗辉谑褂?code>indent的時候生效。 我們也打算讓Vim使用expr折疊,所以把folding.vim改成這樣:

setlocal foldmethod=expr
setlocal foldexpr=GetPotionFold(v:lnum)

function! GetPotionFold(lnum)
    return '0'
endfunction

第一行只是告訴Vim使用expr折疊。

第二行定義了Vim用來計算每一行的foldlevel的表達(dá)式。 當(dāng)Vim執(zhí)行某個表達(dá)式,它會設(shè)置v:lnum為它需要的對應(yīng)行的行號。 我們的表達(dá)式將把這個數(shù)字作為自定義函數(shù)的參數(shù)。

最后我們定義一個對任意行均返回0的占位(dummy)函數(shù)。 注意它返回的是一個字符串而不是一個整數(shù)。等會我們就知道為什么這么做。

繼續(xù)并重新加載折疊代碼(保存folding.vim并對factorial.pn執(zhí)行:set ft=potion)。 我們的函數(shù)對任意行均返回0,所以Vim將不會進(jìn)行任何折疊。

空行

讓我們先解決空行的特殊情況。修改GetPotionFold函數(shù)成這樣:

function! GetPotionFold(lnum)
    if getline(a:lnum) =~? '\v^\s*$'
        return '-1'
    endif

    return '0'
endfunction

我們增加了一個if語句來處理空行。它是怎么起效的?

首先,我們使用getline(a:lnum)來以字符串形式獲取當(dāng)前行的內(nèi)容。

我們把結(jié)果跟正則表達(dá)式\v^\s*$比較。記得\v表示"very magic"(我的意思是,正常的)模式。 這個正則表達(dá)式將匹配"行的開頭,任何空白字符,行的結(jié)尾"。

比較是用大小寫不敏感比較符=~?完成的。 技術(shù)上我們不用擔(dān)心大小寫,畢竟我們只匹配空白,但是我偏好在比較字符串時使用更清晰的方式。 如果你喜歡,可以使用=~代替。

如果需要喚起Vim中的正則表達(dá)式的回憶,你應(yīng)該回頭重讀"基本正則表達(dá)式"和"Grep Operator"這兩部分。

如果當(dāng)前行包括一些非空白字符,它將不會匹配,我們將如前返回0

如果當(dāng)前行_匹配_正則表達(dá)式(i.e. 比如它是空的或者只有空格),就返回字符串'-1'

之前我說過一行的foldlevel可以為0或者正整數(shù),所以這會發(fā)生什么?

特殊折疊

你自定義的表達(dá)式可以直接返回一個foldlevel,或者返回一個"特殊字符串"來告訴Vim如何折疊這一行。

'-1'正是其中一種特殊字符串。它告知Vim,這一行的foldlevel為"undefined"。 Vim將把它理解為"該行的foldlevel等于其上一行或下一行的較小的那個foldlevel"。

這不是我們計劃中的_最終_結(jié)果,但我們可以看到,它已經(jīng)足夠接近了,而且必將達(dá)到我們的目標(biāo)。

Vim可以把undefined的行串在一起,所以假設(shè)你有三個undefined的行和接下來的一個level1的行, 它將設(shè)置最后一行為1,接著是倒數(shù)第二行為1,然后是第一行為1。

在寫自定義的折疊代碼時,你經(jīng)常會發(fā)現(xiàn)有幾種行你可以容易地設(shè)置好它們的foldlevel。 然后你就可以使用'-1'(或我們等會會看到的其他特殊foldlevel)來"瀑布般地"設(shè)置好剩余的行的foldlevel。

如果你重新加載了factorial.pn的折疊代碼,Vim_依然_不會折疊任何行。 這是因?yàn)樗械男械膄oldlevel要不是為0,就是為"undefined"。 等級為0的行將影響undefined的行,最終導(dǎo)致所有的行的foldlevel都是0。

縮進(jìn)等級輔助函數(shù)

為了處理非空行,我們需要知道它們的縮進(jìn)等級,所以讓我們來創(chuàng)建一個輔助函數(shù)替我們計算它。 在GetPotionFold之上加上下面的函數(shù):

function! IndentLevel(lnum)
    return indent(a:lnum) / &shiftwidth
endfunction

重新加載折疊代碼。在factorial.pn緩沖區(qū)執(zhí)行下面的命令來測試你的函數(shù):

:echom IndentLevel(1)

Vim顯示0,因?yàn)榈谝恍袥]有縮進(jìn)?,F(xiàn)在在第二行試試看:

:echom IndentLevel(2)

這次Vim顯示1。第二行開頭有四個空格,而shiftwidth設(shè)置為4,所以4除以4得1。

我們用它除以緩沖區(qū)的shiftwidth來得到縮進(jìn)等級。

為什么我們使用&shiftwidth而不是直接除以4? 如果有人偏好使用2個空格縮進(jìn)他們的Potion代碼,除以4將導(dǎo)致不正確的結(jié)果。 使用shiftwidth可以允許任何縮進(jìn)的空格數(shù)。

再來一個輔助函數(shù)

下一步的方向尚未明朗。讓我們停下來想想為了確定折疊非空行,還需要什么信息。

我們需要知道每一行的縮進(jìn)等級。我們已經(jīng)通過IndentLevel函數(shù)得到了,所以這個條件已經(jīng)滿足了。

我們也需要知道_下一個非空行_的縮進(jìn)等級,因?yàn)槲覀兿M郫B段頭行到對應(yīng)的縮進(jìn)段中去。

讓我們寫一個輔助函數(shù)來得到給定行的下一個非空行的foldlevel。在IndentLevel上面加入下面的函數(shù):

function! NextNonBlankLine(lnum)
    let numlines = line('$')
    let current = a:lnum + 1

    while current <= numlines
        if getline(current) =~? '\v\S'
            return current
        endif

        let current += 1
    endwhile

    return -2
endfunction

這個函數(shù)有點(diǎn)長,不過很簡單。讓我們逐個部分分析它。

首先我們用line('$')得到文件的總行數(shù)。查查文檔來了解line()。

接著我們設(shè)變量current為下一行的行號。

然后我們開始一個會遍歷文件中每一行的循環(huán)。

如果某一行匹配正則表達(dá)式\v\S,表示匹配"有一個_非_空白字符",它就是非空行,所以返回它的行號。

如果某一行不匹配,我們就循環(huán)到下一行。

如果循環(huán)到達(dá)文件尾行而沒有任何返回,這就說明當(dāng)前行之后_沒有_非空行! 我們返回-2來指明這種情況。-2不是一個有效的行號,所以用來簡單地表示"抱歉,沒有有效的結(jié)果"。

我們可以返回-1,因?yàn)樗彩且粋€無效的行號。 我甚至可以選擇0,因?yàn)閂im中的行號從1開始! 所以為何我選擇-2這個看上去奇怪的選項?

我選擇-2是因?yàn)槲覀冋幚碇郫B代碼,而'-1'(和'0')是特殊的Vim foldlevel字符串。

當(dāng)眼睛正掃過代碼時,看到-1,腦子里會立刻浮現(xiàn)起"undefined foldlevel"。 這對于0也差不多。 我在這里選擇-2,就是為了突出它_不是_foldlevel,而是表示一個"錯誤"。

如果你覺得這不可理喻,你可以安心地替換-2-10。 這只是代碼風(fēng)格問題。

完成折疊函數(shù)

本章已經(jīng)顯得比較冗長了,所以現(xiàn)在把折疊函數(shù)包裝起來(wrap up)吧。把GetPotionFold修改成這樣:

function! GetPotionFold(lnum)
    if getline(a:lnum) =~? '\v^\s*$'
        return '-1'
    endif

    let this_indent = IndentLevel(a:lnum)
    let next_indent = IndentLevel(NextNonBlankLine(a:lnum))

    if next_indent == this_indent
        return this_indent
    elseif next_indent < this_indent
        return this_indent
    elseif next_indent > this_indent
        return '>' . next_indent
    endif
endfunction

這里的新代碼真多!讓我們分開一步步來看。

空行

首先我們檢查空行。這里沒有改動。

如果不是空行,我們就準(zhǔn)備好處理非空行的情況了。

獲取縮進(jìn)等級

接下來我們使用兩個輔助函數(shù)來獲取當(dāng)前行和下一個非空行的折疊等級。

你可能會疑惑萬一NextNonBlankLine返回錯誤碼-2該怎么辦。 如果這發(fā)生了,indent(-2)還會繼續(xù)工作。對一個不存在的行號執(zhí)行indent()將返回-1。 你可以試試:echom indent(-2)看看。

-1除以任意大于1的shiftwidth將返回0。 這好像有問題,不過它實(shí)際上不會有?,F(xiàn)在暫時不用糾結(jié)于此。

同級縮進(jìn)

既然我們已經(jīng)得到了當(dāng)前行和下一非空行的縮進(jìn)等級,我們可以比較它們并決定如何折疊當(dāng)前行。

這里又是一個if語句:

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

首先我們檢查這兩行是否有同樣的縮進(jìn)等級。如果相等,我們就直接把縮進(jìn)等級當(dāng)作foldlevel返回!

舉個例子:

a
b
    c
    d
e

假設(shè)我們正處理包含c的那一行,它的縮進(jìn)等級為1。 下一個非空行("d")的縮進(jìn)等級也是一樣的,所以返回1作為foldlevel。

假設(shè)我們正處理"a",它的縮進(jìn)等級為0。這跟下一非空行("b")的等級是一樣的,所以返回0作為foldlevel。

在這個簡單的示例中,可以分出兩個foldlevel。

a       0
b       ?
    c   1
    d   ?
e       ?

純粹出于運(yùn)氣,這種情況也處理了在最后一行對特殊的"error"情況。 記得我們說過,如果我們的輔助函數(shù)返回-2,next_indent將會是0。

在這個例子中,行"e"的縮進(jìn)等級為0,而next_indent也被設(shè)為0,所以匹配這種情況并返回0。 現(xiàn)在foldlevels是這樣:

a       0
b       ?
    c   1
    d   ?
e       0

更低的縮進(jìn)等級

我們再來看看那個if語句:

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

if的第二部分檢查下一行的縮進(jìn)等級是否比當(dāng)前行。就像是例子中行"d"的情況。

如果符合,將再一次返回當(dāng)前行的縮進(jìn)等級。

現(xiàn)在我們的例子看起來像這樣:

a       0
b       ?
    c   1
    d   1
e       0

當(dāng)然,你可以用||把兩種情況連接起來,但是我偏好分開來寫以顯得更清晰。 你的想法可能不同。這只是風(fēng)格問題。

又一次,純粹出于運(yùn)氣,這種情況處理了其他來自輔助函數(shù)的"error"狀態(tài)。設(shè)想我們有一個文件像這樣:

a
    b
    c

第一種情況處理行"b":

a       ?
    b   1
    c   ?

行"c"為最后一行,有著縮進(jìn)等級1。由于我們的輔助函數(shù),next_indent將設(shè)為0。 這匹配if語句的第二部分,所以foldlevel設(shè)為當(dāng)前縮進(jìn)等級,也即是1

a       ?
    b   1
    c   1

結(jié)果如我們所愿,"b"和"c"折疊到一塊去了。

更高的縮進(jìn)等級

現(xiàn)在還剩下最后一個if語句:

if next_indent == this_indent
    return this_indent
elseif next_indent < this_indent
    return this_indent
elseif next_indent > this_indent
    return '>' . next_indent
endif

而我們的例子現(xiàn)在是:

a       0
b       ?
    c   1
    d   1
e       0

只剩下行"b"我們還不知道它的foldlevel,因?yàn)椋?/p>

  • "b"的縮進(jìn)等級為0。
  • "c"的縮進(jìn)等級為1
  • 1既不等于0,又不小于0。

最后一種情況檢查下一行的縮進(jìn)等級是否_大于_當(dāng)前行。

這種情況下Vim的indent折疊并不理想,也是為什么我們一開始打算寫自定義的折疊代碼的原因!

最后的情況表示,當(dāng)下一行的縮進(jìn)比當(dāng)前行多,它將返回一個以>開頭和_下一行_的縮進(jìn)等級構(gòu)成的字符串。 這是什么意思呢?

從折疊表達(dá)式中返回的,類似>1的字符串表示Vim的特殊foldlevel中的一種。 它告訴Vim當(dāng)前行需要_展開_一個給定level的折疊。

在這個簡單的例子中,我們可以簡單返回表示縮進(jìn)等級的數(shù)字,但我們很快將看到為什么要這么做。

這種情況下"b"將展開level1的折疊,使我們的例子變成這樣:

a       0
b       >1
    c   1
    d   1
e       0

這就是我們想要的!萬歲!

復(fù)習(xí)

如果你一步步做到了這里,你應(yīng)該為自己感到驕傲。即使像這樣的簡單折疊代碼,也會是令人絞盡腦汁的。

在我們結(jié)束之前,讓我們重溫最初的factorial.pn代碼,看看我們的折疊表達(dá)式是怎么處理每一行的foldlevel的。

重新把factorial.pn代碼列在這里:

factorial = (n):
    total = 1
    n to 1 (i):
        # Multiply the running total.
        total *= i.
    total.

10 times (i):
    i string print
    '! is: ' print
    factorial (i) string print
    "\n" print.

首先,所有的空行的foldlevel都將設(shè)為undefined:

factorial = (n):
    total = 1
    n to 1 (i):
        # Multiply the running total.
        total *= i.
    total.
                                         undefined
10 times (i):
    i string print
    '! is: ' print
    factorial (i) string print
    "\n" print.

所有折疊等級跟下一行的_相等_的行,它們的foldlevel等于折疊等級:

factorial = (n):
    total = 1                            1
    n to 1 (i):
        # Multiply the running total.    2
        total *= i.
    total.
                                         undefined
10 times (i):
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.

在下一行的縮進(jìn)比當(dāng)前行_更少_的情況下,也是同樣的處理:

factorial = (n):
    total = 1                            1
    n to 1 (i):
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         undefined
10 times (i):
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.                          1

最后的情況是下一行的縮進(jìn)比當(dāng)前行更多。如果這樣,那就設(shè)當(dāng)前行的折疊等級為展開下一行的折疊:

factorial = (n):                         >1
    total = 1                            1
    n to 1 (i):                          >2
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         undefined
10 times (i):                            >1
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.                          1

現(xiàn)在我們已經(jīng)得到了文件中每一行的foldlevel。剩下的就是由Vim來解決未定義(undefined)的行。

不久前我說過undefined的行將選擇相鄰行中較小的那個foldlevel。

Vim手冊是這么講的,但不是十分地確切。 如果真是這樣的,我們的文件中的空行的foldlevel為1,因?yàn)樗噜弮尚械膄oldlevel都為1。

事實(shí)上,空行的foldlevel將被設(shè)定成0!

這就是為什么我們不直接設(shè)置10 times(i):的foldlevel為1。我們告訴Vim該行_展開_一個level1的折疊。 Vim能夠意識到這意味著undefined的行應(yīng)該設(shè)置成0而不是1。

這樣做背后的理由也許深埋在Vim的源碼里。 通常Vim在處理undefined行時,對待特殊的foldlevel的行為都是很聰明的,所以你總能如愿以償。

一旦Vim處理完undefined行,它會得到一個對每一行的折疊情況的完整描述,看上去像這樣:

factorial = (n):                         1
    total = 1                            1
    n to 1 (i):                          2
        # Multiply the running total.    2
        total *= i.                      2
    total.                               1
                                         0
10 times (i):                            1
    i string print                       1
    '! is: ' print                       1
    factorial (i) string print           1
    "\n" print.                          1

這就是了,我們完成啦!重新加載折疊代碼,在factorial.pn中玩玩我們神奇的折疊功能吧!

練習(xí)

閱讀:help foldexpr.

閱讀:help fold-expr。注意你的表達(dá)式可以返回的所有特殊字符串。

閱讀:help getline。

閱讀:help indent()。

閱讀:help line()。

想想為什么我們用.連接>和我們折疊函數(shù)給出的數(shù)字。如果我們使用的是+會怎樣?

我們在全局空間中定義了輔助函數(shù),但這不是好的做法。把它改到腳本本地的命名空間中。

放下本書,出去玩一下,讓你的大腦從本章中清醒清醒。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號