在上一章里我們用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ī)則:
通過一個例子,我們可以加深理解。打開一個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ī)則。
首先,同等縮進(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ū)的filetype
為potion
的時候,在ftplugin/potion/
下的所有文件都會被執(zhí)行。 這意味著僅需在factorial.pn
的分割下執(zhí)行:set ft=potion
,Vim將重新加載折疊代碼!
這比每次都關(guān)閉并重新打開文件要快多了。 唯一需要銘記的是,你得保存folding.vim
到硬盤上,否則未保存的改變不會起作用。
為了獲取折疊上的無限自由,我們將使用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)等級,所以讓我們來創(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ù)。
下一步的方向尚未明朗。讓我們停下來想想為了確定折疊非空行,還需要什么信息。
我們需要知道每一行的縮進(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
為-1
或0
。 這只是代碼風(fēng)格問題。
本章已經(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)備好處理非空行的情況了。
接下來我們使用兩個輔助函數(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ī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
我們再來看看那個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"折疊到一塊去了。
現(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>
0
。1
。最后一種情況檢查下一行的縮進(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
這就是我們想要的!萬歲!
如果你一步步做到了這里,你應(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
中玩玩我們神奇的折疊功能吧!
閱讀:help foldexpr
.
閱讀:help fold-expr
。注意你的表達(dá)式可以返回的所有特殊字符串。
閱讀:help getline
。
閱讀:help indent()
。
閱讀:help line()
。
想想為什么我們用.
連接>
和我們折疊函數(shù)給出的數(shù)字。如果我們使用的是+
會怎樣?
我們在全局空間中定義了輔助函數(shù),但這不是好的做法。把它改到腳本本地的命名空間中。
放下本書,出去玩一下,讓你的大腦從本章中清醒清醒。
更多建議: