既然知道了段移動(dòng)的工作原理,讓我們重新映射這些命令來使得它們對(duì)于Potion文件起作用。
首先我們要決定Potion文件中"段"的意義。 有兩對(duì)段移動(dòng)命令,所以我們可以總結(jié)出兩套組合,我們的用戶可以選擇自己喜歡的一個(gè)。
讓我們使用下面兩個(gè)組合來決定哪里是Potion中的段:
稍微拓展我們的factorial.pn
例子,這就是那些規(guī)則當(dāng)作段頭的地方:
# factorial.pn 1
# Print some factorials, just for fun.
factorial = (n): 1 2
total = 1
n to 1 (i):
total *= i.
total.
print_line = (): 1 2
"-=-=-=-=-=-=-=-\n" print.
print_factorial = (i): 1 2
i string print
'! is: ' print
factorial (i) string print
"\n" print.
"Here are some factorials:\n\n" print 1
print_line () 1
10 times (i):
print_factorial (i).
print_line ()
我們的第一個(gè)定義更加自由。它定義一個(gè)段為一個(gè)"頂級(jí)的文本塊"。
第二個(gè)定義則嚴(yán)格一點(diǎn)。它定義一個(gè)段為一個(gè)函數(shù)定義。
在你的插件的repo中創(chuàng)建ftplugin/potion/sections.vim
。 這將是我們放置段移動(dòng)代碼的地方。記得一旦一個(gè)緩沖區(qū)的filetype
設(shè)置為potion
,這里的代碼就會(huì)執(zhí)行。
我們將重新映射全部四個(gè)段移動(dòng)命令,所以繼續(xù)并創(chuàng)建一個(gè)骨架:
noremap <script> <buffer> <silent> [[ <nop>
noremap <script> <buffer> <silent> ]] <nop>
noremap <script> <buffer> <silent> [] <nop>
noremap <script> <buffer> <silent> ][ <nop>
Notice that we use?noremap
?commands instead of?nnoremap
, because we want these to work in operator-pending mode too. That way you'll be able to do things like?d]]
?to "delete from here to the next section". 注意我們使用noremap
而不是nnoremap
,因?yàn)槲覀兿胍@些命令也能在operator-pending模式下工作。 這樣你就能使用d]]
命令來刪除從這到下一段之間的內(nèi)容。
我們?cè)O(shè)置映射生效于buffer-local,所以它們只對(duì)Potion文件起作用,不會(huì)替換全局選項(xiàng)。
我們也設(shè)置了silent,因?yàn)橛脩舨粦?yīng)關(guān)心我們實(shí)現(xiàn)段移動(dòng)的細(xì)節(jié)。
每個(gè)命令中實(shí)現(xiàn)段移動(dòng)的代碼會(huì)是非常相似的,所以讓我們把它抽象出供映射調(diào)用的一個(gè)函數(shù)。
你將在那些創(chuàng)建了一些相似的映射的Vim插件中頻繁看到這種策略。 比起把所有的功能堆砌于各個(gè)映射中,這樣做不僅更易讀,而且更易維護(hù)。
在sections.vim
文件中加上下面內(nèi)容:
function! s:NextSection(type, backwards)
endfunction
noremap <script> <buffer> <silent> ]]
\ :call <SID>NextSection(1, 0)<cr>
noremap <script> <buffer> <silent> [[
\ :call <SID>NextSection(1, 1)<cr>
noremap <script> <buffer> <silent> ][
\ :call <SID>NextSection(2, 0)<cr>
noremap <script> <buffer> <silent> []
\ :call <SID>NextSection(2, 1)<cr>
這里我用到了Vimscript的斷行特性,因?yàn)槲也幌肟吹接珠L又臭的代碼。 注意反斜杠是放在第二行前面進(jìn)行轉(zhuǎn)義的。閱讀:help line-continuation
以了解更多。
注意我們使用<SID>
和一個(gè)腳本本地命名空間內(nèi)定義的函數(shù)來避免污染全局空間。
每個(gè)映射簡單地以適當(dāng)參數(shù)調(diào)用NextSection
實(shí)現(xiàn)對(duì)應(yīng)的移動(dòng)。 現(xiàn)在我們可以開始實(shí)現(xiàn)NextSection
了。
讓我們考慮下我們的函數(shù)需要做什么。 我們想要移動(dòng)光標(biāo)到下一段,而移動(dòng)光標(biāo),有一個(gè)簡單的辦法就是利用/
和?
命令。
編輯NextSection
成這樣:
function! s:NextSection(type, backwards)
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . 'foo' . "\r"
endfunction
現(xiàn)在這個(gè)函數(shù)使用我們之前見過的execute normal!
來執(zhí)行/foo
或?foo
,取決于backwards
的值。 這將是個(gè)好的開始。
繼續(xù)前進(jìn),我們顯然需要搜索foo
以外的東西,是什么則取決于用的是段頭的第一個(gè)還是第二個(gè)定義。
把NextSection
改成這樣:
function! s:NextSection(type, backwards)
if a:type == 1
let pattern = 'one'
elseif a:type == 2
let pattern = 'two'
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . "\r"
endfunction
現(xiàn)在只需要補(bǔ)上匹配的模式了(pattern),讓我們繼續(xù)完成它吧。
用下面一行替換掉第一個(gè)let pattern = '...'
:
let pattern = '\v(\n\n^\S|%^)'
如果不理解這個(gè)正則表達(dá)式是干什么的,請(qǐng)回憶我們正在實(shí)現(xiàn)的"段"的定義。
任何在空行之后的,第一個(gè)字符為非空字符的行,以及文件首行。
開頭的\v
強(qiáng)制切換為"very magic"模式,一如之前的幾次。
剩下的正則表達(dá)式由兩個(gè)選項(xiàng)組成。第一個(gè),\n\n^\S
,搜索"兩個(gè)換行符,接著之后是一個(gè)非空字符"。 這正好是我們的定義中的第一種情況。
另一個(gè)是%^
,在Vim中,這是一個(gè)代表文件開頭的特殊正則符號(hào)。
我們現(xiàn)在已經(jīng)到了嘗試前兩個(gè)映射的時(shí)機(jī)了。 保存ftplugin/potion/sections.vim
并在你的Potion例子緩沖區(qū)中執(zhí)行:set filetype=potion
。?[[
和]]
命令應(yīng)該可以工作,但會(huì)顯得古怪。
你大概注意到了,在段之間移動(dòng)時(shí)光標(biāo)會(huì)位于真正想要移動(dòng)到的地方上方的空行。 在繼續(xù)閱讀之前,先想想為什么會(huì)這樣。
問題在于我們使用/
(或?
)進(jìn)行搜索,而在默認(rèn)情況下Vim會(huì)把光標(biāo)移動(dòng)到匹配開始處。 舉個(gè)例子,當(dāng)你執(zhí)行/foo
光標(biāo)會(huì)位于foo
中的f
。
為了讓Vim把光標(biāo)移動(dòng)到匹配結(jié)束處而不是開始處,我們可以使用搜索標(biāo)記(search flag)。 試試在Potion文件中這么搜索:
/factorial/e
Vim將找到factorial
并帶你到那。按下幾次n
來在匹配處之間移動(dòng)。?e
標(biāo)記將使得Vim把光標(biāo)移動(dòng)到到匹配結(jié)束處而不是開始處。在另一個(gè)方向也試試:
?factorial?e
讓我們來修改我們的函數(shù),用搜索標(biāo)記來放置光標(biāo)到匹配的段頭的另一端。
function! s:NextSection(type, backwards)
if a:type == 1
let pattern = '\v(\n\n^\S|%^)'
let flags = 'e'
elseif a:type == 2
let pattern = 'two'
let flags = ''
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction
我們這里改動(dòng)了兩處。首先,我們依照段移動(dòng)的類型設(shè)置flags
變量的值。 現(xiàn)在我們僅需處理第一種情況,所以設(shè)置了標(biāo)記e
。
其次,我們?cè)谒阉髯址羞B接dir
和flags
。這將依照我們搜索的方向加入?e
或/e
。
保存文件,切換回Potion示例文件,并執(zhí)行:set ft=potion
來讓改動(dòng)生效。 現(xiàn)在嘗試[[
和]]
來看看我們的成果吧!
是時(shí)候處理我們對(duì)"段"的第二個(gè)定義了,幸運(yùn)的是這個(gè)比起第一個(gè)簡單多了。 重新說一下我們需要實(shí)現(xiàn)的定義:
任何第一個(gè)字符為非空字符,包括一個(gè)等于號(hào),并以冒號(hào)結(jié)尾的行。
我們可以使用一個(gè)簡單的正則表達(dá)式來查找這樣的行。 修改函數(shù)中第二個(gè)let pattern = '...'
成這樣:
let pattern = '\v^\S.*\=.*:$'
這個(gè)正則表達(dá)式比上一個(gè)沒那么嚇人多了。我把指出它是怎么工作的任務(wù)作為你的練習(xí) -- 它只是我們的定義的一個(gè)直白的翻譯。
保存文件,在factorial.pn
處執(zhí)行:set filetype=potion
,然后試試新的][
和[]
映射。它們應(yīng)該能如期工作。
在這里我們不需要搜索標(biāo)記,因?yàn)槟J(rèn)的移動(dòng)到匹配處開頭正是我們想要的。
我們的段移動(dòng)命令在normal模式下一切正常,但要讓它們也能在visual模式下工作,我們還需要增加一些東西。 首先,把函數(shù)改成這樣:
function! s:NextSection(type, backwards, visual)
if a:visual
normal! gv
endif
if a:type == 1
let pattern = '\v(\n\n^\S|%^)'
let flags = 'e'
elseif a:type == 2
let pattern = '\v^\S.*\=.*:$'
let flags = ''
endif
if a:backwards
let dir = '?'
else
let dir = '/'
endif
execute 'silent normal! ' . dir . pattern . dir . flags . "\r"
endfunction
Two things have changed. First, the function takes an extra argument so it knows whether it's being called from visual mode or not. Second, if it's called from visual mode we run?gv
?to restore the visual selection. 兩個(gè)地方改變了。首先,函數(shù)接受的參數(shù)多了一個(gè),這樣它能知道自己是否是在visual模式下調(diào)用的。 其次,如果它是在visual模式下調(diào)用的,我們執(zhí)行gv
來恢復(fù)可視選擇區(qū)域。
為什么我們要這么做?來,讓我展示給你看。 在visual模式下隨意選擇一些文本并執(zhí)行下面命令:
:echom "hello"
Vim將顯示hello
,但可視模式下選擇的范圍也隨之清空!
當(dāng)用:
執(zhí)行一個(gè)ex模式下的命令,可視選擇的范圍總會(huì)被清空。?gv
命令重新選擇之前的可視選擇范圍,相當(dāng)于撤銷了清空。 這是個(gè)有用的命令,你會(huì)在日常工作中因此受益的。
現(xiàn)在我們需要更新前面的映射,傳遞0
給新的visual
參數(shù):
noremap <script> <buffer> <silent> ]]
\ :call <SID>NextSection(1, 0, 0)<cr>
noremap <script> <buffer> <silent> [[
\ :call <SID>NextSection(1, 1, 0)<cr>
noremap <script> <buffer> <silent> ][
\ :call <SID>NextSection(2, 0, 0)<cr>
noremap <script> <buffer> <silent> []
\ :call <SID>NextSection(2, 1, 0)<cr>
這里沒什么是過于復(fù)雜的?,F(xiàn)在讓我們加上visual模式映射,作為最后一塊拼圖。
vnoremap <script> <buffer> <silent> ]]
\ :<c-u>call <SID>NextSection(1, 0, 1)<cr>
vnoremap <script> <buffer> <silent> [[
\ :<c-u>call <SID>NextSection(1, 1, 1)<cr>
vnoremap <script> <buffer> <silent> ][
\ :<c-u>call <SID>NextSection(2, 0, 1)<cr>
vnoremap <script> <buffer> <silent> []
\ :<c-u>call <SID>NextSection(2, 1, 1)<cr>
這些映射都設(shè)置visual
參數(shù)的值為1
,來告訴Vim在移動(dòng)之前重新選擇上一次的選擇范圍。 這里也用到了我們?cè)贕rep Operator那幾章學(xué)到的<c-u>
技巧。
保存文件,在Potion文件中set ft=potion
,大功告成!嘗試一下你的新映射吧。 像v]]
和d[]
這樣的命令現(xiàn)在應(yīng)該可以正常地工作了。
這是冗長的一章,盡管我們只實(shí)現(xiàn)了一些看上去簡單的功能,但是你學(xué)到了(并充分地練習(xí)了)下列有用的知識(shí):
noremap
而不是nnoremap
來創(chuàng)建可以作為移動(dòng)和動(dòng)作使用的命令。%^
(文件開頭) 。堅(jiān)持下并完成練習(xí)(不過是閱讀一些文檔),然后賞自己一些冰激凌。你值得擁有!
閱讀:help search()
。這是一個(gè)值得了解的函數(shù),不過你也可以使用跟/
和?
列在一起的標(biāo)記。
閱讀:help ordinary-atom
來認(rèn)識(shí)能在搜索模式(pattern)中用到的更多有趣的東西。
更多建議: