縮排::Vim進階索引[8]

helloxchen發表於2010-10-22

縮排可以使用文字結構更清晰易讀。在Vi中,這透過是使用專用的外部程式(如:indent或c beautifier類的程式)實現的。Vim除保留了原有外部程式支援外更增加了一些內部的支援。包括了插入模式下的互動進行的縮排與'='指令的縮排操作。

1 基礎知識

:h indent.txt
:h =
:h 'equalprg'
:h indentkeys

Vim中縮排有三種基本的使用方式。一是在普通(正常)模式下使用'='指令。可以圈選範圍後使用可以在指令後加上移動的指令。使用的方式與其他編輯指令是一樣的(比如'd')。'=='表示對當前行進行縮排。看下面的例子:

=ip
對當前段落縮排
=G
將由當前行至文章末尾的範圍縮排
30==
縮排由當前行開始的30行

二是在編輯的過程(插入模式)使用某些鍵觸發。比如,使用'autoindent'時,在插入模式中輸入回車(即按Enter鍵)時Vim自動對新行應用縮排規則。
三是貼上文字時,使用']p'指令對貼上文字強制運用縮排。詳見::h ]p

此外,'gq'或ex命令':left'也能用來縮排文字。由於它們屬於文字格式化的內容,這裡不作討論。

注意:當'equalprg'不為空時,'='/'gq'總是使用equalprg中設定的外部工具。除此之外其他的縮排操作不影響。

在equalprg中使用的外部程式通常是整理(過濾)文字的工具,很少是單獨用於縮排的工具。此外,如果透過外部程式實現縮排,有一些缺點不可避免:

  • 使用上不方便。如果要實現互動方式的縮排(即邊輸入邊根據輸入實現縮排),要不斷執行外部程式,執行效率低。透過cinkeys/indentkeys的設定Vim可以在輸入時計算縮排。
  • 對大多數的一般應用而言,使用者只需要最基本的縮排支援——如autoindent。而你卻很難找到這樣的程式。
  • 許多工具不跨平臺。
  • 不夠靈活。你其實不想為一些簡單的縮排而寫新的程式。

Vim應用縮排的過程如下:

  1. 依據設定使用縮排規則計算縮排寬度。

    在不同的縮排規則同時開啟時只能有一個起作用。在所有開啟的縮排項中只有優先順序最高的起作用。它們的優先順序排列如下:
    indentexpr > cindent > smartindent > ai

    縮排寬度:以一個半形字元的寬度為基本單位計算的總縮排的量。縮排時,Vim會在行首增加相應寬度的空格或製表符。舉例而言,如果縮排寬度為4,則Vim在行首增加4個半形空格;如果縮排寬度為8,Vim在行首增加一個製表符。

  2. 刪除目標行首的製表符與空格。
  3. 根據expandtab與tabstop的設定及縮排寬度新增相應的空格或製表符。

    製表符的寬度與'tabstop'的設定有關。預設值是8,所以8個半形空格(或其他字元)的寬度與一個製表符一樣。如果將'tabstop'設為4,那麼如果縮排寬度為9則Vim在行首增加2個製表符與1個半形空格。如果不想使用製表符可以:se noet

如果要實驗各種縮排方式的話,建議定義如下的快捷鍵以便隨時按檢視縮排的設定:

map  :se autoindent? smartindent? cindent? lisp? indentexpr? equalprg? paste? cpoptions?

有些選項,如'paste'會影響縮排,所以需要檢視這個設定項的情況。各個設定項的情況可以見各自的文件。

2 預設規則

為了方便使用者Vim提供了一些預置的縮排規則:自動縮排(autoindent)、智慧縮排(smartindent)、c縮排(cindent)、lisp縮排(lisp)。

2.1 autoindent

autoindent的縮排規則是最簡單的。它使用與上一行一樣的縮排量。換言之如果你為當前行加了3個空格的縮排,則開始下一行時Vim會自動新增3個空格的縮排。寫python指令碼時,使用這種縮排就夠了。

使用autoindent,只要開啟相應的選項::se autoindent 或 :se ai

注意:indentexpr、lisp、cindent、smartindent中的任一項開啟都會覆蓋autoindent的設定。

2.2 smartindent,cindent

:h C-indenting
:h smartindent
:h cindent
:h cinkeys
:h cinwords
:h cinoptions
:h cinkeys-format

smartindent的縮排規則可應用於與c語法類似的語言如AWK、JavaScript等,當然也可以用在c語言。它的規則是將{}塊內的語句縮排一定寬度。巢狀的{}塊內的語句則相對於上一層語句縮排一定寬度。

cindent的縮排規則是專門用於c語言的縮排。與smartindent相比,cindent除了更嚴格地對應c語言的語法外,還增加了風格選項——為了適合不同的c語言風格,Vim提供了相當多的定置項改變cindent的縮排方式。設定項包括了:

cinkeys
這個選項定義了一組可以觸發縮排的按鍵。在遇到這些按鍵是Vim會根據縮排規則重新計算當前行的縮排。定義按鍵的格式可以見:h cinkeys-format
cinwords
定義了一組讓下一行相對對當前行增加縮排的關鍵字。在遇到定義在cinwords中的字時,Vim為接下來一行增加縮排。
cinoptions
縮排風格選項。參考::h cinoptions-value、

2.3 lisp

:h lisp
:h lispwords

根據lisp語法縮排,我懂得很少,所以——詳見幫助。

3 進階規則(indentexpr)

:h cpo
:h indentexpr
:h indentkeys

與摺疊一樣,縮排也支援使用表示式定義縮排。這個表示式可以是任意表示數值的表示式也可以是返回數值的自定義/內建函式,這個數值將做為縮排的寬度。也與摺疊一樣Vim使用v:lnum表示目標行的行號。其它常用的函式包括了indent()、getline()、prevnonblank()、nextnonblank()等等。與摺疊不一樣的是使用縮排表示式不用另外指定縮排方式,只要賦於indentexpr項一個值,就會覆蓋autoindent或smartindent/cindent的設定。

Vim的縮排表示式要比摺疊表示式直觀得多。我們直接透過例子瞭解縮排表示式的使用。

3.1 簡單縮排

先看幾個簡單的縮排表示式:

" 縮排寬度總為4
:se indentexpr=4
" 不使用表示式縮排
:se indentexpr=
	
" 將縮排寬度設為與&sw設定一致
:se inde=&shiftwidth
	
" 逐漸增加縮排
:se inde=v:lnum

這一組表示式還是比較容易理解的,都是直接將一某個數值(不需要什麼計算)作為縮排量。 此外,三元條件表示式在摺疊篇中也已經看了不少:

" 偶數行縮排4格
:se indentexpr=v:lnum%2?0:4
	
" 取消註釋行的縮排
:se inde=getline(v:lnum)=~'^s*#'?0:indent(v:lnum)
	
" 行首縮排:
:se inde=(getline(v:lnum-1)=~'^s*$')?4:0
" 懸掛縮排:
:se inde=(getline(v:lnum-1)=~'S')?4:0
	
" 相對於上一行縮排行首帶著-號的行
:se inde=getline(v:lnum)=~'^s*-'?indent(v:lnum-1)+4:0
	

這一小節的的最後一個例子是個常用到的縮排:根據編號縮排。

1. statement
1.1. substatement
2. statement
2.1. substatement
2.1.1. subsubstatement
2.2. substatement

在看需求文件時幾乎每一行都是編號的。程式設計師從不同的渠道獲得這些文件,可能是從某個需求管理系統,電子郵件或者SKYPE。它們有不一樣的縮排,有一些甚至沒縮排。自動縮排工具此時顯得特別有用。

對於寫需求文件的人來講他們除了要能智慧的縮排他們可能還需要一個可以自動編號(根據縮排或者行首的*字元的個數)的編輯器。當然Vim使用者是不需要再花時間找這樣的工具的!

要將這些編號可以用以下的指令碼:

" 根據編號縮排
:se inde=len(split(substitute(getline(v:lnum),'^[ t]*([0-9.]+).*','1',''),'.'))*&sw

將函式寫成單行形式的最大挑戰是要加上非常多的轉義符。而且記住:使用單引號,不要用雙引號。具體的原因See 附錄的解釋.

如果不想記轉義規則可以用函式將它包裝起來。當然,這樣也就沒辦法在模式行中使用了:

func! GetIndent(lnum)
  let ind=len(split(substitute(
        getline(a:lnum),'^[ t]*([0-9.]+).*','1',''),'.'))
  return ind
endfunc
se inde=GetIndent(v:lnum)*&sw

3.2 indentkeys

在定義了縮排表示式後,我們可以在文字輸入完成後使用'='或'=='進行縮排。如果要在編輯的過程中實時地縮排,我們需要定義合適的'indentkeys'。考慮下面的表示式:

" 將字串長小於20的句子右對齊
" 這條命令實際等價於:right 20
se inde=20-len(substitute(getline(v:lnum),'^[t ]+','',''))

因為預設的indentkeys中包含了o,O,所以在開啟新行時,Vim就已經計算了縮排——但這時我們的輸入還沒完成,所以縮排寬度是錯的。我們需要讓Vim在句子輸入完成後再計算縮排寬度。也就是在我們按下回車後先計算並應用縮排再插入換行符。同時還需要定義一個在插入模式中可以使用的縮排命令,以隨時強制Vim計算縮排。就像所有Vim的其他功能一樣,Vim也為這個功能提供了設定項,這次是indentkeys,

se indentkeys=*,!^F

*表示在插入模式下按Enter鍵時,先重新計算縮排再加入換行符。如果只有則Vim會先加入換行符再計算縮排——這時新增行成了目標行。
!^F表示在插入模式下按Ctrl-F時,重新計算縮排但不插入字元。關於*和!在indentkeys(及cinkeys)中的意義可以見::h indentkeys-format

4 縮排進階

在處理縮排時經常會遇到巢狀的格式文字,幸好它們都大同小異。考慮下面的文字巢狀結構:

[marker]
  block
  [marker]
    block
  [end marker]
[end marker]

這種型別的檔案很常見xml(block),C程式碼({block}),opera書籤檔案(opera6.adr)等1

下面我們將一起為兩個使用這種結構的文字的寫縮排指令碼。

4.1 例2

在一些情況下'marker'與'end marker'不那麼明顯。下面是一個文字目錄樹:

+ item1
- item2
  + subitem
  - subitem
    * subsubitem
  -
-
* item3

這裡的marker是跟減號跟隨文字,end marker則是一個減號(後面沒有文字)。事實上將這個end marker改成一個空行,在處理上也不會有什麼不同。2

但無論是哪種形式只是對marker的判別方式有一些區別,其結構並無本質區別。

狀態及對應的處理方式;

  • 當前條目如果只有一個減號(^s*-s*$),則當前條目相對上一條目減少縮排量。
  • 上一條加號或星號開始(^s*[+*]) 當前條目與上一條目的縮排一樣
  • 上一條如果只有一個減號(^s*-s*$),則當前條目減少縮排量。
  • 上一條如果由一個減號開始(^s*-s*S+$),則當前條目增加縮排量。

這樣指令碼就很清楚了,

func! MyIndent(lnum)
  let lastline=getline(a:lnum-1)
  if a:lnum==1 | return 0 | endif
	
  if getline(a:lnum)=~'^s*-s*$'
    let diff=-1
  elseif lastline=~'^s*[*+]'
    let diff=0
  elseif lastline=~'^s*-s*$'
    let diff=-1
  elseif lastline=~'^s*-s*S'
    let diff=1
  endif 
	
  return indent(a:lnum-1)+diff*&sw
endfunc
	
se inde=MyIndent(v:lnum)

4.2 例2

最後是一個完整的例子仍是巢狀的文字塊,看一下下面的文字,

[
outer block
[[
inner block
]
]
[
[inner block]
]
]

這個仍然是marker與end marker的格式。[是marker,]是end marker。我們要寫一個使之能正確縮排的指令碼,其中的關鍵在於判斷巢狀的深度來決定縮排寬度。我們可以使用一個buffer變數儲存巢狀深度,遇到[增加,遇到]減少深度。但因為=命令可以多次不連續地對不同文字塊使用,所有變數的存在可能會導致不正常的結果。為此,仍像前面的例子一樣我們將根據前一行的狀態判斷縮排深度。根據上一行的marker,計算當前行的縮排寬度。描述如下,

  • 如果上一行是[,當前行增加寬度
  • 如果上一行是],當前行減少寬度
  • 否則,保持上一行的寬度

還要考慮到一點,同一行可能數量不等的多個]或[——事實上這是這個例子與上一個例子唯一的不同之處。因為一對[]的縮排剛好可以抵消,我們可以透過它們的差決定縮排的寬度。另外,如果當前行有[或]還要相應增加或減少當前行的縮排。所以改進後的描述如下,

  • 將上一行[的數量減去]的數量,得到初始的縮排寬度
    • 結果為正,則為當前行增加相應數量的縮排
    • 否則,為當前行減少相應數量的縮排
  • 在前面計算的基礎上計算當前行的[與]的差,得到縮排的增量。
    • 結果為正,則為當前行增加相應數量的縮排
    • 否則,為當前行減少相應數量的縮排

現在我們可以寫指令碼了,

" 其中,根據[]數量計算寬度這一段是重複的,
" 我們可以寫成一個單獨的函式
	
func! IndentSum(lnum,incre)
" 兩個引數分別表示目標行行號與縮排的初始量
    let line=getline(a:lnum)
    " 透過'['與']'的數量計算縮排寬度
    " 每多一個[則增加一個單位的縮排
    " 每多一個]則減少一個單位的縮排
    " 沒有]或[的行使用與上一行一樣的縮排
    let in=len(split('x'.line.'x','['))-1
    let ou=len(split('x'.line.'x',']'))-1
    " [的數量減去]的數量
    return &sw*(in-ou)+a:incre
endfunc
	
func! BIndent(lnum)
    " 上一非空行的行號
    let llnum=prevnonblank(a:lnum-1)
    if llnum==0 | return 0 | endif
    " 由上一行得到初始的縮排寬度
    let ind=IndentSum(llnum,indent(llnum))
    " 計算當前行的的縮排增量
    let ind=IndentSum(a:lnum,ind)
    return ind
endfunc
	
se inde=BIndent(v:lnum)

現在,你已經可以寫c縮排的指令碼了(將上面指令碼中的[]換成{} :) )。這種縮排的計算方式幾乎是一個套路了。基於同樣的模式,同樣的工作流程的一個xml的縮排的例子可以見Vim安裝目錄中indent/xml.vim。

5 進階提示

這一章是關於縮排的一些零散的內容。

5.1 去除縮排

:h g@
:h operatorfunc

現在你可以用'='進行縮排了你可能還需要一個可以去除縮排的指令。當然你可以用'<

:nnoremap <<< :left
:vnoremap <<< :left

這個宏有兩個主要缺點,一是會使<

func! Deindent(dummy)
  exe 'normal! ' . "'[V']:left"
endfunc
se opfunc=Deindent
	
nnoremap <<< g@
vnoremap <<< g@

現在試一下<<

5.2 縮排與格式化選項

:h gq
:h formatoptions
:h formatprg

縮排與文格式化(gq)緊密相關,你可能會有興趣看一下這一部分的內容。

5.3 縮排與摺疊

在摺疊篇我們知道可以以縮排作為摺疊的規則。因此這實際給我們一種同時定義縮排與摺疊的方式。在學了這一篇之後這個摺疊的縮排規則就能派上用場了!

:se fdm=indent

Appendix A 表示式與沙箱


直接寫表示式跟將表示式包裝在一個函式中有一個最主要的不同是,前一種方式中的及空格需要進行轉義。所以在模式行中要使用很多的。另外要注意的是"與'是不一樣的。

:h expr-quote
:h expr-'

在寫Vim指令碼或命令時"的字串是允許使用跳脫字元的,而'的字串則不進行轉義。如"t"表示的是一個製表符而't'表示的是一個斜槓和一個字母t。't'等價的雙字號字串是"t"。
在指令碼篇我們就講過了一個例子:

echo '|t|'
echo "|t|'

但對於indentexpr及其它在沙箱中計算表示式的設定項來講,數值表示式在進入沙箱中先進行了一次表示式的計算——只是計算字串值。例如,當你執行:se inde=len('abct')時,先計算字串的“安全值”。即在計算函式的值之前,Vim會先計算字串表示式的值,所以函式現在成了,

"len('abct')"

這個字串表示式的值,大家都知道是(透過:echo &inde可以觀察字串表示式的值):

len('abct')

然後,再計算函式的值,結果是4(3個字母加一個製表符)。

在執行=命令時,Vim首先計算了字串表示式的值,再eval字串的值(即執行len('abc')並返回數值)。

注意,在寫表示式時你並不需要在前後加上引號,Vim會自動為你加上雙引號並進行計算字串的值。這個過程中Vim還進行了一些處理以確保值是“安全的”。其中包括移掉未轉義的。可以簡單的記為這些表示式中不能有未轉義的雙引號",空格和斜槓。還是例子比較實在,

命令 字串表示式 字串值
:se inde=len('t') "len('t')" len('t')
:se inde=len('t') "len('t')" len('t') 未轉義斜槓會被忽略掉
:se inde=len(' abc') 空格未被轉義,不合法表示式
:se inde=len('ab"c') "len('ab"c')" len('ab 雙引號"未被轉義,所中間的"後的字串被省略
:se inde=len("t") "len("t")" len("t")

正因為在計算len()之前已經先計算了字串值一次,所以本來,

func! GetIndent(lnum)
	return len(split(substitute(getline(v:lnum),'^[ t]*([0-9.]+).*','1',''),'.'))*&sw
endfunc
se inde=GetIndent(v:lnum)

不用轉義的表示式,直接放到:se inde命令後,就成了:

:se inde=len(split(substitute(getline(v:lnum),'^[ t]*([0-9.]+).*','1',''),'.'))*&sw

可以看到多了一堆的反斜槓('')。不與這些轉義規則打交道的方法是儘量將它們包裝在獨立的函式中(這樣不用使用沙箱)。如果一定要用的話儘量少用空格與雙引號。


Footnotes

[1] 事實上幾乎所有的巢狀結構都是這樣的

[2] 但有個'-'號會比空行直觀一點。

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040224/,如需轉載,請註明出處,否則將追究法律責任。

相關文章