使用腳本編寫 Vim 編輯器,第 2 部分: 用戶定義函數

用戶定義函數

Haskell 或 Scheme 程序員會告訴您,函數對於任何嚴肅的編程語言來說都是最重要的特性。對於 C 或 Perl 程序員,他們也會告訴您完全相同的觀點。

函數爲嚴肅的程序員提供了兩個基本優勢:

  1. 它們能夠將複雜的計算任務細分爲足夠小的部分,從而能夠容易地被人類理解。
  2. 它們允許這些細分後的部分具有邏輯的和可理解的名稱,這樣就十分適合由人類處理。

Vimscript 是一種嚴肅的編程語言,因此它天生就支持創建用戶定義函數。事實上,它確實提供了比 Scheme、C 或 Perl 更加優秀的 用戶定義函數支持。本文探究了 Vimscript 函數的各種特性,並展示瞭如何使用這些函數以可維護的方式增強並擴展 Vim 的內置函數。

聲明函數

Vimscript 中的函數使用 function 關鍵字定義,後跟函數名,然後是參數列表(這是強制的,即使該函數沒有參數)。函數體然後從下一行開始,一直連續下去,直到遇到一個匹配的 endfunction 關鍵字。例如:

清單 1. 具有正確結構的函數
functionExpurgateText (text)
    let expurgated_text = a:text

    for expletive in [ 'cagal', 'frak', 'gorram', 'mebs', 'zarking']
        let expurgated_text
        \   = substitute(expurgated_text, expletive, '[DELETED]', 'g')
    endfor

    return expurgated_text
endfunction

函數返回值使用 return 語句指定。可以根據需要指定任意數量的單獨 return 語句。如果函數被用作一個過程,並且沒有任何有用的返回值,那麼可以不包含 return 語句。然而,Vimscript 函數始終 返回一個值,因此如果沒有指定任何 return,那麼函數將自動返回 0。

Vimscript 中的函數名必須以大寫字母開頭:

清單 2. 以大寫字母開頭的函數名
function SaveBackup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call SaveBackup()<CR>

這個例子定義了一個函數,它將遞增當前緩衝區的 b:backup_count 變量的值(或初始化爲 1,如果尚不存在的話)。函數隨後獲取當前文件(getline(1,'$'))中的每一行並調用內置的 writefile() 函數來將它們寫入到磁盤中。writefile() 的第二個參數是將要寫入的新文件的名稱;在本例中,爲當前文件(bufname('%'))的名稱附加上計數器的新值。返回的值爲對 writefile() 調用的 success/failure 值。最後,nmap 設置 CTRL-B 以調用函數來創建對當前文件的有限備份。

Vimscript 函數沒有使用前導大寫字母,相反,可以使用顯式的範圍前綴聲明函數(類似變量,如 第 1 部分 所述)。最常見的選擇是 s:,它表示函數對於當前腳本文件是本地函數。如果函數使用這種方式確定範圍,那麼它的名稱就不需要以大寫開頭;它可以是任意有效標識符。然而,顯式確定範圍的函數必須始終使用範圍前綴進行調用。比如:

清單 3. 使用範圍前綴調用函數
" Function scoped to current script file...
function s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call s:save_backup()<CR>

可重新聲明的函數

Vimscript 中的函數聲明爲運行時語句,因此如果一個腳本被加載兩次,那麼該腳本中的任何函數聲明都將被執行兩次,因此將重新創建相應的函數。

重新聲明函數被看作一種致命的錯誤(這樣做是爲了防止發生兩個不同腳本同時聲明函數的衝突)。這使得很難在需要反覆加載的腳本中創建函數,比如自定義的語法突出顯示腳本。

因此 Vimscript 提供了一個關鍵字修飾符(function!),允許在需要時指出某個函數聲明可以被安全地重載:

清單 4. 表示某個函數聲明可以被安全地重載
function! s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

對於使用這個修飾過的關鍵字定義的函數,沒有執行任何重新聲明檢查,因此非常適合用於顯式確定範圍的函數(在這種情況下,範圍已經確保函數不會和其他腳本中的函數發生衝突)。

調用函數

要調用函數並使用它的返回值作爲語言表達式的一部分,只需要命名它並附加一個使用圓括號括起的參數列表:

清單 5. 使用函數的返回值
"Clean up the current line...
let success = setline('.', ExpurgateText(getline('.')) )

但是要注意,與 C 或 Perl 不同,Vimscript 並不 允許您在未使用的情況下拋出函數的返回值。因此,如果打算使用函數作爲過程或子例程並忽略它的返回值,那麼必須使用 call 命令爲調用添加前綴:

清單 6. 在未使用返回值的情況下使用函數
"Checkpoint the text...
call SaveBackup()

否則,Vimscript 將假設該函數調用實際上是一個內置的 Vim 命令,並且很可能會發出報警,指出並不存在這類命令。我們將在本系列的後續文章中查看函數和命令之間的區別。

參數列表

Vimscript 允許您定義顯式參數 和可變參數列表,甚至可以將兩者結合起來。

在聲明瞭子例程的名稱後,您可以立即指定最多 20 個顯式命名的參數。指定參數後,通過將 a: 前綴添加到參數名,可以在函數內部訪問當前調用的相應參數值:

清單 7. 在函數內部訪問參數值
function PrintDetails(name, title, email)
    echo 'Name:   '  a:title  a:name
    echo 'Contact:'  a:email
endfunction

如果您不清楚一個函數具有多少個參數,那麼可以指定一個可變的參數列表,使用省略號(...)而不是命名參數。在本例中,函數可以使用任意數量的參數調用,並且這些值被收集到一個單一變量中:一個名爲 a:000 的數組。爲單個參數也提供了位置參數名:a:1a:2a:3,等等。參數的數量可以是 a:0。例如:

清單 8. 指定並使用一個可變的參數列表
function Average(...)
    let sum = 0.0

    for nextval in a:000"a:000 is the list of arguments
        let sum += nextval
    endfor

    return sum / a:0"a:0 is the number of arguments
endfunction

注意,在本例中,sum 必須被初始化爲一個顯式的浮點值;否則,所有後續計算都將使用整數運算計算。

結合命名參數和可變參數

可以在同一個函數中同時使用命名參數和可變參數,只需要將可變參數的省略號放在命名參數列表之後

例如,假設您希望創建一個 CommentBlock() 函數,它將接收一個字符串並針對不同的編程語言將其格式化爲相應的註釋塊。這類函數始終需要調用者爲其提供一個字符串來進行格式化,因此應當使用命名參數。但是,您可能希望註釋導入器(introducer)、“boxing” 字符和註釋的寬度全部是可選的(在被省略時具有合理的默認值)。那麼應當像下面這樣調用:

清單 9. 一個簡單的 CommentBlock 函數調用
call CommentBlock("This is a comment")

並且將返回一個多行字符串包含:

清單 10. CommentBlock 返回
//*******************
// This is a comment
//*******************

然而,如果提供額外的參數,那麼將爲註釋導入器、“boxing” 字符和註釋寬度指定非默認值。因此這個調用將爲:

清單 11. 更加複雜的 CommentBlock 函數調用
call CommentBlock("This is a comment", '#', '=', 40)

would return the string:

清單 12. CommentBlock 返回
#========================================
# This is a comment
#========================================

這類函數的可能的實現方式爲:

清單 13. CommentBlock 實現
function CommentBlock(comment, ...)
    "If 1 or more optional args, first optional arg is introducer...
    let introducer =  a:0 >= 1  ?  a:1  :  "//"

    "If 2 or more optional args, second optional arg is boxing character...
    let box_char   =  a:0 >= 2  ?  a:2  :  "*"

    "If 3 or more optional args, third optional arg is comment width...
    let width      =  a:0 >= 3  ?  a:3  :  strlen(a:comment) + 2

    " Build the comment box and put the comment inside it...
    return introducer . repeat(box_char,width) . "\<CR>"
    \    . introducer . " " . a:comment        . "\<CR>"
    \    . introducer . repeat(box_char,width) . "\<CR>"
endfunction

如果至少有一個可選參數(a:0 >= 1),那麼導入器參數將指定給第一個選項(即 a:1);否則,將指定一個默認值 "//"。類似地,如果有兩個或多個可選參數(a:0 >= 2),那麼 box_char 變量被分配給第二個選項(a:2),或一個默認值 "*"。如果提供了三個或多個可選參數,那麼第三個選項被分配給 width 變量。如果沒有提供寬度參數,那麼將自動根據註釋參數本身計算相應的寬度(strlen(a:comment)+2)。

最後,將所有參數值解析後,將構建註釋框的頂部和底部行:首先是一個註釋導入器,後跟 boxing 字符的重複次數(repeat(box_char,width)),在這兩者之間是註釋文本本身。

當然,要使用這個函數,需要以某種方式調用它。最理想的方法可能是使用一個插入映射:

清單 14. 使用一個插入映射調用函數
"C++/Java/PHP comment...
imap <silent>  ///  <C-R>=CommentBlock(input("Enter comment: "))<CR>

"Ada/Applescript/Eiffel comment...
imap <silent>  ---  <C-R>=CommentBlock(input("Enter comment: "),'--')<CR>

"Perl/Python/Shell comment...
imap <silent>  ###  <C-R>=CommentBlock(input("Enter comment: "),'#','#')<CR>

對於每一個映射,將首先調用內置的 input() 函數來請求註釋文本中的用戶類型。CommentBlock() 函數隨後被調用,以將文本轉換爲一個註釋塊。最後,前導 <C-R>= 插入結果字符串。

注意,第一個映射僅僅傳遞一個單一參數,因此默認使用 // 作爲其註釋標記。第二個和第三個映射傳遞第二個參數來指定 # 或 -- 作爲它們各自的註釋導入器。最後一個映射傳遞第三個參數,使得 “boxing” 字符匹配它的註釋導入器。

函數和行範圍

可以使用一個初始的行範圍調用任何標準的 Vim 命令(包括 call),這將針對範圍中的每一行重複一次命令:

"Delete every line from the current line (.) to the end-of-file ($)...
:.,$delete

"Replace "foo" with "bar" everywhere in lines 1 to 10
:1,10s/foo/bar/

"Center every line from five above the current line to five below it...
:-5,+5center

可以在任何 Vim 會話中輸入 :help cmdline-ranges 來了解更多有關此工具的內容。

對於 call 命令,指定範圍將致使所請求的函數被反覆調用:對範圍中的每一行調用一次。要了解這樣做的原因,考慮一下如何編寫一個函數來將當前行中的任何 “原始的” & 符號轉換爲適當的 XML &amp; 實體,但是這樣做也足夠靈巧,可以忽略任何已經存在於其他實體中的 & 符號。這個功能的實現方式類似如下所示:

清單 15. 轉換 & 符號的函數
function DeAmperfy()
    "Get current line...
    let curr_line   = getline('.')

    "Replace raw ampersands...
    let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')

    "Update current line...
    call setline('.', replacement)
endfunction

DeAmperfy() 中的第一行代碼從編輯器緩衝區獲取當前行(getline('.'))。第二行代碼從當前行中查找其後 跟隨標識符和冒號的 &,使用了否定先行(negative lookahead)模式 '&\(\w\+;\)\@!'(參見 :help \@! 獲得更多細節)。substitute() 調用隨後使用 XML &amp; 實體替換所有此類 “原始” & 符號。最後,DeAmperfy() 中的第三行代碼使用修改後的文本更新當前行。

如果從命令行調用該函數:

:call DeAmperfy()

將只對當前行執行替換。但是如果在 call 之前指定了一個範圍:

:1,$call DeAmperfy()

那麼將針對範圍內的每一行調用一次函數(在本例中,指的是文件中的每一行)。

內部化函數行範圍

這種針對每一行反覆調用函數 的行爲是一種方便的默認行爲。然而,有時希望指定一個範圍,但是隻調用一次函數,然後在函數內部處理範圍語義。這對於 Vimscript 也很簡單。只需要將一個特殊修飾符(range)附加到函數聲明之後:

清單 16. 函數內部的範圍語義
function DeAmperfyAll() range"Step through each line in the range...
    for linenum in range(a:firstline, a:lastline)
        "Replace loose ampersands (as in DeAmperfy())...
        let curr_line   = getline(linenum)
        let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')
        call setline(linenum, replacement)
    endfor

    "Report what was done...
    if a:lastline > a:firstline
        echo "DeAmperfied" (a:lastline - a:firstline + 1) "lines"
    endif
endfunction

在參數列表之後指定了 range 修飾符後,使用如下範圍調用 DeAmperfyAll() 時:

:1,$call DeAmperfyAll()

將只對函數執行一次調用,而兩個特殊參數 a:firstline 和 a:lastline 被設置爲範圍的第一個行號和最後一個行號。如果未指定任何範圍,那麼 a:firstline 和 a:lastline 都將被設置爲當前行號。

函數首先構建一個包含所有相關行號的列表(range(a:firstline, a:lastline))。注意,對內置 range() 函數的調用與在函數聲明中使用range 修飾符一點關係也沒有。range() 函數只是一個 list 構造函數,非常類似於 Python 中的 range() 函數,或者是 Haskell 或 Perl 中的 ..運算符。

確定了將要處理的行號列表後,函數使用 for 循環來遍歷每個行號:

for linenum in range(a:firstline, a:lastline)

然後相應地更新每一行(正如最初的 DeAmperfy() 所做的那樣)。

最後,如果範圍涵蓋了多個行(即 a:lastline > a:firstline),函數將報告被更新的行的數量。

可視範圍

一旦擁有了一個可以操作行範圍的函數調用後,一個特別有用的技巧就是通過 Visual 模式(參見 :help Visual-mode 獲得更多細節)調用該函數。

例如,如果遊標位於文本塊的某個位置,那麼可以使用下面的代碼在周圍的段落中編碼所有 & 號:

Vip:call DeAmperfyAll()

在 Normal 模式下輸入 V 將切換到 Visual 模式。ip 隨後將使 Visual 模式突出顯示您正位於其中的整個段落。之後,: 將您切換到 Command 模式並自動將命令範圍設置爲剛剛從 Visual 模式選擇的行的範圍。此時,調用 DeAmperfyAll() 對所有行執行 deamperfy 操作。

注意,在這個實例中,可以使用下面的代碼獲得相同的效果:

Vip:call DeAmperfy()

惟一的不同之處在於 DeAmperfy() 函數將被反覆調用:針對 Visual 模式下 Vip 中突出顯示的每一行調用一次。

用於編碼的函數

Vimscript 中的大多數用戶定義函數只需要很少的參數,並且通常情況下根本不需要參數。這是因爲它們常常直接從當前編輯器緩衝區和上下文信息(比如當前遊標位置、當前段落大小、當前窗口大小或當前行的內容)中獲得數據。

此外,如果函數通過上下文而不是參數列表包含數據,那麼往往更加有用和方便。例如,維護源代碼的一個常見問題就是賦值運算符在聚集起來後無法對齊,這將損害代碼的可讀性:

清單 16. 無法對齊的賦值運算符
let applicants_name = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative = 'sister'
let fathers_occupation = 'Sith'

在每次添加新語句時手動重新對齊它們將十分費力:

清單 17. 手動重新對齊賦值運算符
let applicants_name     = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative    = 'sister'
let fathers_occupation  = 'Sith'

要讓日常編程任務沒那麼乏味,可以創建一個鍵映射(比如 ;=),它可以選擇當前代碼塊、定位具有賦值運算符的任何行,並自動對齊這些運算符。如下所示:

清單 18. 對齊賦值運算符的函數
function AlignAssignments ()
    "Patterns needed to locate assignment operators...
    let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
    let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

    "Locate block of code to be considered (same indentation, no blanks)
    let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
    let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
    let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
    if lastline < 0
        let lastline = line('$')
    endif

    "Find the column at which the operators should be aligned...
    let max_align_col = 0
    let max_op_width  = 0
    for linetext in getline(firstline, lastline)
        "Does this line have an assignment in it?
        let left_width = match(linetext, '\s*' . ASSIGN_OP)

        "If so, track the maximal assignment column and operator width...
        if left_width >= 0
            let max_align_col = max([max_align_col, left_width])

            let op_width      = strlen(matchstr(linetext, ASSIGN_OP))
            let max_op_width  = max([max_op_width, op_width+1])
         endif
    endfor

    "Code needed to reformat lines so as to align operators...
    let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
    \                                    max_op_width,  submatch(2))'

    " Reformat lines with operators aligned in the appropriate column...
    for linenum in range(firstline, lastline)
        let oldline = getline(linenum)
        let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
        call setline(linenum, newline)
    endfor
endfunction

nmap <silent>  ;=  :call AlignAssignments()<CR>

AlignAssignments() 函數首先創建兩個正則表達式(參見 :help pattern 獲得有關 Vim 正則表達式語法的必要細節):

let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

ASSIGN_OP 中的模式匹配任何標準的賦值運算符:=+=-=*=,等等,但是注意不要匹配其他包含 = 的運算符,比如 == 和 =~。如果您喜歡的語言中包含其他賦值運算符(比如 .= 或 ||= 或 ^=),那麼可以擴展 ASSIGN_OP 正則表達式來識別這些運算符。另一種選擇是,可以重新定義 ASSIGN_OP 來識別其他 “可對齊的” 類型,比如註釋導入器或列表及,並對齊它們。

ASSIGN_LINE 中的模式只在行的起始部分(^)開始匹配,首先匹配最小字符數(.\{-}),然後匹配任何空白(\s*),最後匹配賦值運算符。

注意,最初的 “最小字符數” 子模式和運算符子模式都在捕捉圓括號內進行了指定:\(...\)。這兩個正則表達式組件捕獲的子字符串稍後將通過調用內置 submatch() 函數來提取;具體來講,通過調用 submatch(1) 來提取運算符前面的所有內容,然後調用 submatch(2) 來提取運算符本身。

AlignAssignments() 隨後查找它將對其進行操作的行範圍:

let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
    let lastline = line('$')
endif

在此前的例子中,函數依賴於一個顯式的命令範圍或一個 Visual 模式選擇來確定要進行處理的行,但是這個函數則直接計算它自己的行範圍。具體來講,它首先調用內置 matchstr() 函數來確定出現在當前行(getline('.'))起始部分的前導空白('^\s*')。隨後在 indent_pat 中構建一個新的正則表達式,精確匹配任何非空行的起始處的相同序列的空白(即拖尾 '\S')。

AlignAssignments() 隨後調用內置 search() 函數向上搜索(使用標記 'bnW')並定位位於遊標上方的第一個 具有相同縮進的行。向此行號加 1 將得出感興趣的範圍的起始行號,也就是說,具有相同縮進的第一個相鄰行就作爲當前行。

第二個 search() 調用隨後向下搜索('nW')來判斷 lastline:具有相同縮進的最後一個相鄰行。對於這種情況,搜索可能會到達文件的末尾,並且沒有找到具有不同縮進的行,這種情況下 search() 將返回 -1。要正確地處理這種情況,隨後的 if 語句需要顯式地將 lastline 設置爲文件末端的行號(即設置爲由 line('$') 返回的行號)。

這兩個搜索的結果將使 AlignAssignments() 知道緊鄰着當前行的上方或下方、具有與當前行完全相同的縮進的完整行範圍。它使用這些信息來確保只對位於同一代碼塊中相同範圍級別的賦值語句執行對齊。當然,如果代碼的縮進不能正確反映它的範圍,那麼這種情況下必須進行重新格式化。

AlignAssignments() 中的第一個 for 循環判斷其中的賦值運算符應當對齊的列。實現方法是遍歷所選範圍內的行列表(由getline(firstline, lastline) 取回的行)並檢查每個行是否包含賦值運算符(運算符的前面可能包含空格):

let left_width = match(linetext, '\s*' . ASSIGN_OP)

如果該行中沒有運算符,那麼內置 match() 函數將無法找到匹配,因此將返回 -1。對於這種情況,循環將直接跳到下一行。如果存在 運算符,那麼 match() 將返回在其中顯示運算符的(正)指數。if 語句隨後使用內置 max() 函數判斷這個最近的列位置是否比此前找到的運算符更靠右,從而跟蹤所需的最大列位置來對齊範圍內的所有賦值運算符:

let max_align_col = max([max_align_col, left_width])

if 中剩下的兩行代碼使用內置 matchstr() 函數檢索實際的運算符,然後使用內置 strlen() 函數判斷行的長度("=" 的長度爲 1,'+=''-=' 的長度爲 2,等等)。max_op_width 變量隨後被用來跟蹤所需的最大寬度,以對範圍內的各種運算符執行對齊:

let op_width     = strlen(matchstr(linetext, ASSIGN_OP))
let max_op_width = max([max_op_width, op_width+1])

一旦確定了賦值區域的位置和寬度,剩下的就是遍歷範圍中的行並相應地執行重新格式化。要執行重新格式化,函數將使用內置的 printf()函數。這個函數十分有用,但是它的命名非常糟糕。它與 C、Perl 或 PHP 中的 printf 函數不同。實際上,它類似於以上這些語言中的sprintf 函數。也就是說,在 Vimscript 中,printf 並不會輸出其數據參數列表的格式化後的版本;它會返回一個字符串,其中包含了數據參數列表的格式化後的版本。

理想情況下,要重新格式化每一行,AlignAssignments() 將使用內置的 substitute() 函數,並使用經過 printf 重新整理後的文本替換運算符之前的所有內容。不幸的是,substitute() 要求使用固定的字符串作爲它的替代值,而不是一個函數調用。

因此,要使用 printf() 來重新格式化每個替換文本,需要使用特殊的嵌入式替換形式:"\=expr"。替換字符串中的前導 \= 要求substitute() 對隨後的表達式求值並使用結果作爲替換文本。注意,這類似於 Insert 模式下的 <C-R>= 機制,惟一不同的是這種奇妙的行爲只針對內置 substitute() 函數的替換字符串(或在標準 :s/.../.../ Vim 命令中)。

在本例中,特殊替換形式對於每一行來說都將是相同的 printf ,因此它將在第二個 for 循環開始之前被預先存儲到 FORMATTER 變量中:

let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
\                                    max_op_width,  submatch(2))'

當最終被 substitute() 調用時,這個內嵌的 printf() 將把運算符左側的所有內容(submatch(1))靠左對齊(使用 %-*s 佔位符)並將結果放到字符寬度爲 max_align_col 的字段中。隨後將運算符本身(submatch(2))右對齊(使用 %*s)到第二個字段,其字符寬度爲max_op_width。參考 :help printf(),瞭解 - 和 * 選項如何修改這裏使用的兩個 %s 格式說明符(specifier)。

有了這個格式化程序後,第二個 for 循環就可以遍歷完整的行號範圍,每次取回一行相應的文本緩衝內容:

for linenum in range(firstline, lastline)
    let oldline = getline(linenum)

循環隨後使用 substitute() 來轉換這些內容,方法是匹配位於任何賦值運算符之前幷包括運算符在內的所有內容(使用 ASSIGN_LINE 中的模式)並使用 printf() 調用的結果替換文本(如 FORMATTER 中指定的那樣):

    let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
    call setline(linenum, newline)
endfor

當 for 循環遍歷了所有行之後,這些行中的賦值運算符將被正確對齊。剩餘的工作是創建一個鍵映射來調用 AlignAssignments(),如下所示:

nmap <silent>  ;=  :call AlignAssignments()<CR>

結束語

爲了處理真實 Vim 編程任務的複雜性,需要將應用程序分解爲正確的、可維護的組件,而函數是實現這個過程的基本工具。

Vimscript 允許您使用固定的或可變的參數列表來定義函數,並使它們通過自動的或用戶控制的方式與編輯器的文本緩衝中的行範圍進行交互。函數可以回調到 Vim 的內置特性(比如,回調到 search() 或 substitute() 文本),並且它們可以直接訪問編輯器狀態信息(比如通過line('.') 確定遊標所在的當前行)或者與當前進行編輯的任何文本緩衝進行交互(通過 getline() 和 setline())。

這無疑提供了非常強大的功能,但是通過編程的方式操作狀態和內容始終受限於數據表示的整潔性和準確性,我們的代碼將對這些數據進行處理。到目前爲止,該 系列文章 一直關注單個標量函數(數值、字符串和布爾值)的使用。在接下來兩篇文章中,我們將探討更強大、更方便的數據結構的應用:有序列表和隨機訪問字典。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章