在上一章里我們用Vim的`indent`折疊方式,在Potion文件中增加了一些快捷而骯臟的折疊。
打開`factorial.pn`并用`zM`關閉所有的折疊。文件現在看起來就像這樣:
~~~
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
~~~
這真不錯,但我個人喜歡依照內容來折疊每個塊的第一行。 在本章中我們將寫下一些自定義的折疊代碼,并在最后實現這樣的效果:
~~~
factorial = (n):
total = 1
+--- 3 lines: n to 1 (i):
total.
+-- 5 lines: 10 times (i):
~~~
這將更為緊湊,而且(對我來說)更容易閱讀。 如果你更喜歡`indent`也不是不行,不過最好學習本章來對Vim中實現折疊的代碼的更深入的了解。
## 折疊原理
為了寫好自定義的折疊,我們需要了解Vim對待("thinks")折疊的方式。簡明扼要地講解下規則:
* 文件中的每行代碼都有一個"foldlevel"。它不是為零就是一個正整數。
* foldlevel為零的行_不會_被折疊。
* 有同等級的相鄰行會被折疊到一起。
* 如果一個等級X的折疊被關閉了,任何在里面的、foldlevel不小于X的行都會一起被折疊,直到有一行的等級小于X。
通過一個例子,我們可以加深理解。打開一個Vim窗口然后粘貼下面的文本進去。
~~~
a
b
c
d
e
f
g
~~~
執行下面的命令來設置`indent`折疊:
~~~
:setlocal foldmethod=indent
~~~
花上一分鐘玩一下折疊,觀察它是怎么工作的。
現在執行下面的命令來看看第一行的foldlevel:
~~~
:echom foldlevel(1)
~~~
Vim顯示`0`。現在看看第二行的:
~~~
: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
~~~
重讀這一部分開頭的幾條規則。打開或關閉每個折疊,觀察foldlevel,并確保你理解了為什么會這樣折疊。
一旦你已經自信地認為你理解了每行的foldlevel是怎么影響折疊結構的,繼續看下一部分。
## 首先:做一個規劃
在我們埋頭敲鍵盤之前,先為我們的折疊功能規劃出幾條大概的規則。
首先,同等縮進的行應該要折疊到一塊。我們也希望_上_一行也一并折疊,達到這樣的效果:
~~~
hello = (name):
'Hello, ' print
name print.
~~~
將折疊成這樣:
~~~
+-- 3 lines: hello = (name):
~~~
空行應該算入_下_一行,因此折疊底部的空行不會包括進去。這意味著類似這樣的內容:
~~~
hello = (name):
'Hello, ' print
name print.
hello('Steve')
~~~
將折疊成這樣:
~~~
+-- 3 lines: hello = ():
hello('Steve')
~~~
而_不是_這樣:
~~~
+-- 4 lines: hello = ():
hello('Steve')
~~~
這當然是屬于個人偏好的問題,但現在我們就這么定了。
## 開始
現在開始寫我們的自定義折疊代碼吧。 打開Vim,分出兩個分割,一個是`ftplugin/potion/folding.vim`,另一個是示例代碼`factorial.pn`。
在上一章我們關閉并重新打開Vim來使得`folding.vim`生效,但其實還有更簡單的方法。
不要忘記每當設置一個緩沖區的`filetype`為`potion`的時候,在`ftplugin/potion/`下的所有文件都會被執行。 這意味著僅需在`factorial.pn`的分割下執行`:set ft=potion`,Vim將重新加載折疊代碼!
這比每次都關閉并重新打開文件要快多了。 唯一需要銘記的是,你得_保存_`folding.vim`到硬盤上,否則未保存的改變不會起作用。
## Expr折疊
為了獲取折疊上的無限自由,我們將使用Vim的`expr`折疊。
我們可以繼續并從`folding.vim`移除`foldignore`,因為它只在使用`indent`的時候生效。 我們也打算讓Vim使用`expr`折疊,所以把`folding.vim`改成這樣:
~~~
setlocal foldmethod=expr
setlocal foldexpr=GetPotionFold(v:lnum)
function! GetPotionFold(lnum)
return '0'
endfunction
~~~
第一行只是告訴Vim使用`expr`折疊。
第二行定義了Vim用來計算每一行的foldlevel的表達式。 當Vim執行某個表達式,它會設置`v:lnum`為它需要的對應行的行號。 我們的表達式將把這個數字作為自定義函數的參數。
最后我們定義一個對任意行均返回`0`的占位(dummy)函數。 注意它返回的是一個字符串而不是一個整數。等會我們就知道為什么這么做。
繼續并重新加載折疊代碼(保存`folding.vim`并對`factorial.pn`執行`:set ft=potion`)。 我們的函數對任意行均返回`0`,所以Vim將不會進行任何折疊。
## 空行
讓我們先解決空行的特殊情況。修改`GetPotionFold`函數成這樣:
~~~
function! GetPotionFold(lnum)
if getline(a:lnum) =~? '\v^\s*$'
return '-1'
endif
return '0'
endfunction
~~~
我們增加了一個`if`語句來處理空行。它是怎么起效的?
首先,我們使用`getline(a:lnum)`來以字符串形式獲取當前行的內容。
我們把結果跟正則表達式`\v^\s*$`比較。記得`\v`表示"very magic"(我的意思是,正常的)模式。 這個正則表達式將匹配"行的開頭,任何空白字符,行的結尾"。
比較是用大小寫不敏感比較符`=~?`完成的。 技術上我們不用擔心大小寫,畢竟我們只匹配空白,但是我偏好在比較字符串時使用更清晰的方式。 如果你喜歡,可以使用`=~`代替。
如果需要喚起Vim中的正則表達式的回憶,你應該回頭重讀"基本正則表達式"和"Grep Operator"這兩部分。
如果當前行包括一些非空白字符,它將不會匹配,我們將如前返回`0`。
如果當前行_匹配_正則表達式(i.e. 比如它是空的或者只有空格),就返回字符串`'-1'`。
之前我說過一行的foldlevel可以為0或者正整數,所以這會發生什么?
## 特殊折疊
你自定義的表達式可以直接返回一個foldlevel,或者返回一個"特殊字符串"來告訴Vim如何折疊這一行。
`'-1'`正是其中一種特殊字符串。它告知Vim,這一行的foldlevel為"undefined"。 Vim將把它理解為"該行的foldlevel等于其上一行或下一行的較小的那個foldlevel"。
這不是我們計劃中的_最終_結果,但我們可以看到,它已經足夠接近了,而且必將達到我們的目標。
Vim可以把undefined的行串在一起,所以假設你有三個undefined的行和接下來的一個level1的行, 它將設置最后一行為1,接著是倒數第二行為1,然后是第一行為1。
在寫自定義的折疊代碼時,你經常會發現有幾種行你可以容易地設置好它們的foldlevel。 然后你就可以使用`'-1'`(或我們等會會看到的其他特殊foldlevel)來"瀑布般地"設置好剩余的行的foldlevel。
如果你重新加載了`factorial.pn`的折疊代碼,Vim_依然_不會折疊任何行。 這是因為所有的行的foldlevel要不是為0,就是為"undefined"。 等級為0的行將影響undefined的行,最終導致所有的行的foldlevel都是`0`。
## 縮進等級輔助函數
為了處理非空行,我們需要知道它們的縮進等級,所以讓我們來創建一個輔助函數替我們計算它。 在`GetPotionFold`之上加上下面的函數:
~~~
function! IndentLevel(lnum)
return indent(a:lnum) / &shiftwidth
endfunction
~~~
重新加載折疊代碼。在`factorial.pn`緩沖區執行下面的命令來測試你的函數:
~~~
:echom IndentLevel(1)
~~~
Vim顯示`0`,因為第一行沒有縮進。現在在第二行試試看:
~~~
:echom IndentLevel(2)
~~~
這次Vim顯示`1`。第二行開頭有四個空格,而`shiftwidth`設置為4,所以4除以4得1。
我們用它除以緩沖區的`shiftwidth`來得到縮進等級。
為什么我們使用`&shiftwidth`而不是直接除以4? 如果有人偏好使用2個空格縮進他們的Potion代碼,除以4將導致不正確的結果。 使用`shiftwidth`可以允許任何縮進的空格數。
## 再來一個輔助函數
下一步的方向尚未明朗。讓我們停下來想想為了確定折疊非空行,還需要什么信息。
我們需要知道每一行的縮進等級。我們已經通過`IndentLevel`函數得到了,所以這個條件已經滿足了。
我們也需要知道_下一個非空行_的縮進等級,因為我們希望折疊段頭行到對應的縮進段中去。
讓我們寫一個輔助函數來得到給定行的下一個非空行的foldlevel。在`IndentLevel`上面加入下面的函數:
~~~
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
~~~
這個函數有點長,不過很簡單。讓我們逐個部分分析它。
首先我們用`line('$')`得到文件的總行數。查查文檔來了解`line()`。
接著我們設變量`current`為下一行的行號。
然后我們開始一個會遍歷文件中每一行的循環。
如果某一行匹配正則表達式`\v\S`,表示匹配"有一個_非_空白字符",它就是非空行,所以返回它的行號。
如果某一行不匹配,我們就循環到下一行。
如果循環到達文件尾行而沒有任何返回,這就說明當前行之后_沒有_非空行! 我們返回`-2`來指明這種情況。`-2`不是一個有效的行號,所以用來簡單地表示"抱歉,沒有有效的結果"。
我們可以返回`-1`,因為它也是一個無效的行號。 我甚至可以選擇`0`,因為Vim中的行號從`1`開始! 所以為何我選擇`-2`這個看上去奇怪的選項?
我選擇`-2`是因為我們正處理著折疊代碼,而`'-1'`(和`'0'`)是特殊的Vim foldlevel字符串。
當眼睛正掃過代碼時,看到`-1`,腦子里會立刻浮現起"undefined foldlevel"。 這對于`0`也差不多。 我在這里選擇`-2`,就是為了突出它_不是_foldlevel,而是表示一個"錯誤"。
如果你覺得這不可理喻,你可以安心地替換`-2`為`-1`或`0`。 這只是代碼風格問題。
## 完成折疊函數
本章已經顯得比較冗長了,所以現在把折疊函數包裝起來(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
~~~
這里的新代碼真多!讓我們分開一步步來看。
### 空行
首先我們檢查空行。這里沒有改動。
如果不是空行,我們就準備好處理非空行的情況了。
### 獲取縮進等級
接下來我們使用兩個輔助函數來獲取當前行和下一個非空行的折疊等級。
你可能會疑惑萬一`NextNonBlankLine`返回錯誤碼`-2`該怎么辦。 如果這發生了,`indent(-2)`還會繼續工作。對一個不存在的行號執行`indent()`將返回`-1`。 你可以試試`:echom indent(-2)`看看。
`-1`除以任意大于1的`shiftwidth`將返回`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
~~~
首先我們檢查這兩行是否有同樣的縮進等級。如果相等,我們就直接把縮進等級當作foldlevel返回!
舉個例子:
~~~
a
b
c
d
e
~~~
假設我們正處理包含`c`的那一行,它的縮進等級為1。 下一個非空行("d")的縮進等級也是一樣的,所以返回`1`作為foldlevel。
假設我們正處理"a",它的縮進等級為0。這跟下一非空行("b")的等級是一樣的,所以返回`0`作為foldlevel。
在這個簡單的示例中,可以分出兩個foldlevel。
~~~
a 0
b ?
c 1
d ?
e ?
~~~
純粹出于運氣,這種情況也處理了在最后一行對特殊的"error"情況。 記得我們說過,如果我們的輔助函數返回`-2`,`next_indent`將會是`0`。
在這個例子中,行"e"的縮進等級為`0`,而`next_indent`也被設為`0`,所以匹配這種情況并返回`0`。 現在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`的第二部分檢查下一行的縮進等級是否比當前行_小_。就像是例子中行"d"的情況。
如果符合,將再一次返回當前行的縮進等級。
現在我們的例子看起來像這樣:
~~~
a 0
b ?
c 1
d 1
e 0
~~~
當然,你可以用`||`把兩種情況連接起來,但是我偏好分開來寫以顯得更清晰。 你的想法可能不同。這只是風格問題。
又一次,純粹出于運氣,這種情況處理了其他來自輔助函數的"error"狀態。設想我們有一個文件像這樣:
~~~
a
b
c
~~~
第一種情況處理行"b":
~~~
a ?
b 1
c ?
~~~
行"c"為最后一行,有著縮進等級1。由于我們的輔助函數,`next_indent`將設為`0`。 這匹配`if`語句的第二部分,所以foldlevel設為當前縮進等級,也即是`1`。
~~~
a ?
b 1
c 1
~~~
結果如我們所愿,"b"和"c"折疊到一塊去了。
### 更高的縮進等級
現在還剩下最后一個`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
~~~
而我們的例子現在是:
~~~
a 0
b ?
c 1
d 1
e 0
~~~
只剩下行"b"我們還不知道它的foldlevel,因為:
* "b"的縮進等級為`0`。
* "c"的縮進等級為`1`。
* 1既不等于0,又不小于0。
最后一種情況檢查下一行的縮進等級是否_大于_當前行。
這種情況下Vim的`indent`折疊并不理想,也是為什么我們一開始打算寫自定義的折疊代碼的原因!
最后的情況表示,當下一行的縮進比當前行多,它將返回一個以`>`開頭和_下一行_的縮進等級構成的字符串。 這是什么意思呢?
從折疊表達式中返回的,類似`>1`的字符串表示Vim的特殊foldlevel中的一種。 它告訴Vim當前行需要_展開_一個給定level的折疊。
在這個簡單的例子中,我們可以簡單返回表示縮進等級的數字,但我們很快將看到為什么要這么做。
這種情況下"b"將展開level1的折疊,使我們的例子變成這樣:
~~~
a 0
b >1
c 1
d 1
e 0
~~~
這就是我們想要的!萬歲!
## 復習
如果你一步步做到了這里,你應該為自己感到驕傲。即使像這樣的簡單折疊代碼,也會是令人絞盡腦汁的。
在我們結束之前,讓我們重溫最初的`factorial.pn`代碼,看看我們的折疊表達式是怎么處理每一行的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都將設為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.
~~~
在下一行的縮進比當前行_更少_的情況下,也是同樣的處理:
~~~
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
~~~
最后的情況是下一行的縮進比當前行更多。如果這樣,那就設當前行的折疊等級為展開下一行的折疊:
~~~
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
~~~
現在我們已經得到了文件中每一行的foldlevel。剩下的就是由Vim來解決未定義(undefined)的行。
不久前我說過undefined的行將選擇相鄰行中較小的那個foldlevel。
Vim手冊是這么講的,但不是十分地確切。 如果真是這樣的,我們的文件中的空行的foldlevel為1,因為它相鄰兩行的foldlevel都為1。
事實上,空行的foldlevel將被設定成0!
這就是為什么我們不直接設置`10 times(i):`的foldlevel為1。我們告訴Vim該行_展開_一個level1的折疊。 Vim能夠意識到這意味著undefined的行應該設置成`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`。注意你的表達式可以返回的所有特殊字符串。
閱讀`:help getline`。
閱讀`:help indent()`。
閱讀`:help line()`。
想想為什么我們用`.`連接`>`和我們折疊函數給出的數字。如果我們使用的是`+`會怎樣?
我們在全局空間中定義了輔助函數,但這不是好的做法。把它改到腳本本地的命名空間中。
放下本書,出去玩一下,讓你的大腦從本章中清醒清醒。
- 前言
- 鳴謝
- 預備知識
- 打印信息
- 設置選項
- 基本映射
- 模式映射
- 精確映射
- Leaders
- 編輯你的Vimrc文件
- Abbreviations
- 更多的Mappings
- 鍛煉你的手指
- 本地緩沖區的選項設置和映射
- 自動命令
- 本地緩沖區縮寫
- 自動命令組
- Operator-Pending映射
- 更多Operator-Pending映射
- 狀態條
- 負責任的編碼
- 變量
- 變量作用域
- 條件語句
- 比較
- 函數
- 函數參數
- 數字
- 字符串
- 字符串函數
- Execute命令
- Normal命令
- 執行normal!
- 基本的正則表達式
- 實例研究:Grep 運算符(Operator),第一部分
- 實例研究:Grep運算符(Operator),第二部分
- 實例研究:Grep運算符(Operator),第三部分
- 列表
- 循環
- 字典
- 切換
- 函數式編程
- 路徑
- 創建一個完整的插件
- 舊社會下的插件配置方式
- 新希望:用Pathogen配置插件
- 檢測文件類型
- 基本語法高亮
- 高級語法高亮
- 更高級的語法高亮
- 基本折疊
- 高級折疊
- 段移動原理
- Potion段移動
- 外部命令
- 自動加載
- 文檔
- 發布
- 還剩下什么?