沒穿方服

索引

顯示╱隱藏內文
顯示具有 Vim 標籤的文章。 顯示所有文章
顯示具有 Vim 標籤的文章。 顯示所有文章

Telescope 官方 README 建議不要追 master 版本,我也真的踩過雷,所以一直停在 0.1.8,18 個月前的東西了。

前幾天終於釋出 0.1.9,變動列出來很長:
Release v0.1.9 · nvim-telescope/telescope.nvim (我也貢獻了一個 PR)
光看標記為 feat 的就要花些時間了,本篇選幾個列出來。

新增 actions.select_tab_drop

選擇檔案後要怎麼開啟,身為分頁使用者當然是用 tab drop 啊。

這個 2022 年就實作的東西一直沒有釋出,我用自己寫的 function 代用很久,終於能換掉了。

除了一些檔案類 picker 能直接設定 mappings 例如 ['<CR>'] = 'select_tab_drop' 這樣,在一些 LSP picker 也能用 jump_type = 'tab drop' 達到相當效果(#2218#2751)。

顯示 * 表示正在搜尋

就是搜尋框後面會多一個顯示,跑完就會消失 (#2637)。

新增 actions.delete_buffer

給 buffers picker 用的,預設按鍵是 <M-d>。 (#3145

新事件 TelescopeResumePost

原來 telescope 有幾個自訂 User 事件 (autocmd),doc 沒寫,只有 README 有提到。
但應該是 plugin 開發者比較有機會用到。 (#2433

插入 <cWORD><cfile>

以前在 picker 輸入框裡想用 <C-R><C-W>(貼上游標文字)或 <C-R>%(目前檔案路徑)會發現貼不出東西,現在特別加了幾個預設 mapping 來做這些事。 (#3134 等)

新增「bcommits_range」 picker

以前雖然有 bcommits picker 可以列出目前檔案的歷史,但沒辦法針對「特定幾行」或「目前這一行」比較精準地追溯,現在新增 bcommits_range 滿足你的願望,#2398

不過預設動作只有 checkout 或者打開 diff,我的習慣還是用 vim-gitdiffall 這老東西:

※這個範例用到我自己寫的 vim function,所以不能真的拿來用,僅供參考
gitdiff = function(bufnr)
  -- Refer to builtin: actions.git_checkout_current_buffer
  local cwd = action_state.get_current_picker(bufnr).cwd
  local selection = action_state.get_selected_entry()
  actions.close(bufnr)

  local fugitive_path = vim.fn.FugitivePath(selection.current_file)
  local display_name = vim.fn.fnamemodify(fugitive_path, ':.')
  local command = string.format("tabnew %s | silent GitDiff @%s", display_name, selection.value)
  vim.cmd(command)
end,

gitdiffall = function(bufnr)
  local cwd = action_state.get_current_picker(bufnr).cwd
  local selection = action_state.get_selected_entry()
  actions.close(bufnr)

  local toplevel = vim.fn.eval("fugitive#repo().tree()")
  vim.fn.TmuxNewWindow({
    text = "gitdiffall @" .. selection.value,
    title = '⎇',
    directory = toplevel
  })
end,

vim-cycle 1.4.0

14 年前發布 vim-cycle 0.1.0貼文)有得到一些迴響,但後來可能宣傳上被同名 zef/vim-cycle 掩蓋,接著又有 switch.vim 打破限制支援 pattern,繼續用 vim-cycle 的理由就不多了。

今年為了清掉古早 issue,幫專案加了 test,體質提昇後就開始加功能,結果變出了不錯的成果。

新增 regex 選項,大致就是跟 switch.vim 一樣的機制,例如 ruby 的 :bar => 可以變成 bar: ,以往是做不到的。 設定上也可以收割 switch.vim 的豐富資產,只要做一些微調(主要是把 vim dictionary 換成 list)即可。

新增 matcher / changer 定義,也就是「什麼東西要 cycle」「要怎麼 cycle」變成可抽換的,這麼一來 regex 也只是一種變體而已。

於是出現了紀年轉換,可以從民國年、日本元号、泰國佛曆、西元年之間轉換。
還有 naming convention 也被獨立出來,變成跟 regex 不太一樣的思維,比較好維護和擴充。

前陣子新增的 select 介面有時也很實用,將選項列出來用選的,而不是一個一個跳,也支援 telescope 等不同 UI。

錄了 demo 影片(影片有聲音):

歡迎多加利用或宣傳,至少讓類似專案的「類似專案」不要老是忽略我。

以前寫 Rails 的時候,有自己寫一套跑測試的 vim 函數,按 \tl 會在右邊切 tmux pane 跑最接近游標的 it block(RSpec 語法),按 \tj 切在下方、\tw 開新視窗,\t. 則是用最後開的視窗或 pane 跑最後一次跑的測試。

多年後才知道有 vim-test 這個專案,但看起來沒有我寫的好用,就沒關心了。

直到最近嘗試 vim-themis 測試框架,無法直接套以前的工具,才想來研究這個還算熱門的專案。

對應多種語言的 :TestNearest 是最大價值

專案 README 第一行寫著: A Vim wrapper for running tests on different granularities.

各種測試框架被定義為 test runner,例如 ruby 的 minitest、rspec,JS 的 jest、vitest、cypress。 能自動辨識處於哪個環境,然後在編輯特定檔案時(不一定要是測試檔本身),能找到對應測試然後執行,即使 runner 不支援 "Nearest"(只跑最接近游標的、最小粒度的測試)也有機會用搜尋湊出來,然後只測這個範圍。

持續收錄大家的貢獻,支援更多語言和框架,讓我們進入新專案時直接可用,就是它最棒的地方。

執行畫面要開在哪裡:strategy

至於跑測試的介面,例如用 :! 開、用 tmux 開、用 vim-dispatch 開,這些被定義為 strategy

Strategy 就比較貧弱了,雖然提供了 34 種,但這個抽象並沒有提供額外能力(有幾個例外,能支援 quickfix 等),從實作 autoload/test/strategy.vim 可以看見,幾乎每個 strategy 都只是呼叫一個開啟指令就結束了。

最後我是直接指明 custom strategy,接到自己寫的 tmux 功能(暫未公開),才能取得「開在右邊、下面、新視窗、重用」這些能力。

let g:test#custom_strategies = {'tmux_window': function('TmuxTestStrategy')}
let g:test#strategy = 'tmux_window'

定義 g:test#strategy 還有一個好處,就是會跳過載入 autoload/test/strategy.vim,如果你只用其中一個 strategy,實在不必每次吃這個檔。

痛點:非制式的測試

現實世界中,很多專案的測試都有客製化,或者用一個 script 包裝過,例如 ./test/run.shnpm run test,這時候用自動偵測的 runner 去跑反而是偏差的。

vim-test 提供的解法,簡單的狀況是指定 test#{runner}#executable(只改執行指令)或 custom transformation(只微調測試指令),再複雜就需要用到 custom runner 才能解決問題。 Custom runner 已經是在實作一個 runner,所以也會建議寫完貢獻回上游專案。 但就是因為需求處於「一次性的設計」和制式標準之間,所以問題就一直重複下去。

去年也有人提出相關議題,其中一步是增加了 generic runner: #820 Add support for generic command runner

後來又有一個想法是單發的 :TestCommand(討論後已 close): #862 Add :TestCommand for one-off tests

目前我還沒想辦法改善,也許要支援一個測試情境本來就難,終究要弄一個 runner 或類似的東西。

實用函數 test#base#nearest_test()

在尋找 Nearest 的實作上,內部函數 test#base#nearest_test() 扮演關鍵角色,也比較難自己刻出來,某方面也是這個 plugin 的精華所在。

維護狀況良好

這個專案已經 11 年了,最初是一位捷克的 ruby 開發者 Janko Marohnić 啟動,現在也有其他人幫忙維護,處理 issue 很快,是健康可靠的專案。

之前鎖版很久,最近才升到後續維護的 smoka7/hop.nvim,發現 2023 年有推出一個叫 match mapping 的功能。

這個提案最早來自原版的 PR 235,看敘述我還真看不懂在幹嘛。

實際上是強化 HopChar1 這個指令,原本是輸入一個字,Hop 就會找出那個字出現的所有位置。
設定了 match_mappings = { 'zh_tc' } 後,會增加中文「拼音」對應,例如按 g 可以找到「乖工幹肛」這些字,鍵位與文字的對應表如 /lua/hop/mappings/zh_tc.lua 內容。

注音對應表

目前 hop 內建的表有波斯文(why!?)和中文,但中文只有拼音,我想改用注音,於是弄了一個
bootleq/hop-extend-match-mappings 專案。 參考全字庫整理出常用字,實作邏輯可參考 gen_tables.py.prompt.md

實際使用例,要找黃偉哲的「ㄨㄟˇ」,就用 HopChar1 然後按 j(鍵盤的位置):

HopChar1 → j (鍵盤ㄨ的位置)

掌管整個 Vim 開發體系的作者 Bram Moolenaar 在 8/3 離開了我們,62 歲。

30 年來處理所有 patch,參與討論,也會回覆新手問題,在 vim_dev 總是一直看見他的頭像。

我大概 13 年前開始用 Vim,讀 help 一篇一篇地學,對那種平實詳盡(例如:說明各種預設按鍵的選用理由)光明正大的風格,深感讚嘆; 每到一個新章節,例如 undoterm,都當作新概念的洗禮,過程欣賞了整個 Vim 的設計,不斷同意細節、獲得巨大滿足。

當時是 Vim 7.3,很多人還是當作陽春編輯器在用,但其實各種新發明、語言支援一直在 patch 版本追加,成熟強大或小巧聰明的 plugin 也接連出現,是一個很甜蜜的時期。

後來 Neovim 聲勢起來,到進入 Vim9 script 時代,開始感到不安;即使到了 9.0.xxx 版本,實際寫起 vim9script 仍然很多規格未定,身為使用者都隱隱感到開發壓力。 不禁也會揣測 Bram 的想法,但當然沒有結果,只能遠遠地誠心祝福。

最後的離開是瀟灑的,去年也一度有住院事件,但我們並不知道是否要擔心健康狀況,或怎麼看待,保留著一個距離。

見證投入一生創造能表達自己的軟體,也感謝給予這個時代最好用的編輯器。


平日樂趣,不定期看 @h_east 整理的新功能摘要,然後編譯最新版;作為紀念的最後版本是:

Build of Bram's last commit

六年前,越獄哀鳳打開 Vim,就好像安心許多、能做很多事了(實際上什麼也不行)

2017/11/17 在公司跟同事借鍵盤

Bram 的旅行相簿 —— www.moolenaar.net/albums.html

事件處理的常見問題,短時間內事件發生太頻繁,接收端處理不了,卡死、噴錯或其他,要用一些手法來減壓。

日常使用 Vim 會碰到嗎?幾乎不會,多半 plugin 會自己照顧自己,其次是症狀不夠嚴重,所以無感;最後是真的爆了,也懶得追怎麼回事,quit 重開復原,那些殺不死我的,我就不修。

然而 autocmd 越寫越多,各種自動化疊來疊去,看似無害的事件(例如 User xxx)也可能在跑某個 script 時(例如 :source $VIMRUNTIME/syntax/hitest.vim)突然爆發——幸好要解決也不難,就是用 timer_* 系列函數。


辨別 debounce / throttle

不就是要抑制事件的觸發嗎?為何變成只有 debounce / throttle 兩個選項呢?

其實根據需求,會有多樣的操作才對,例如 ReactiveX 定義的各種 filter 運算就可以參考(只是借用,概念不盡相同); 而一般應用上,最直觀以「一段時間」為限,降低頻率的作法,就能歸納為 throttle

至於 debounce 就比較難翻譯,或說看字面難以想像,先來考察用語來源:

  • 最早來自電路開關的彈跳 (bounce) 現象,要判斷是實際輸入,還是零件振動的雜訊,解法不只一種。 wikipedia: Switch
  • 承上,一種解法:訊號發生後,短時間內的訊號先不理會,然後就能正確判定了——常見例子是鍵盤。 wikipedia: Keyboard
  • JavaScript 開發者的引用,意思是將多個連續發生的訊號「合併」為一個訊號,見 Debouncing Javascript Methods | John Hann
    Debouncing means to coalesce several temporally close signals into one signal.
    具體實作會設定一段時間,期間內再度發生事件的話,就重新計時,直到超過這段時間都沒有發生,才認定整個事件完成。

我自己看這個詞,在程式領域的用法已經特化了——主要概念仍是消除不穩定,而作法是等,特徵是等的時間可以延長,描述為一個意象的話,是等一個振動的東西穩定下來。

這個認知下,即便 Lodash 的 throttle 是用 debounce 做的(採 maxWait = wait 的設定),也不會說 throttle 是 debounce 的一種,反而應該說不能延長 wait 的用法其實指的是 throttle。


模仿常見 JavaScript 實作

所謂常見的實作,是指 LodashUnderscore.js 等 library 提供的 higher-order function,接受函數為參數,回傳另一個 debounced 版函數的這種設計,以下用 Vim script 實作。

要拿來測試用的隨意 timeout 和 Say 函數:

let g:TEST_TIMEOUT = 5000

function! Say(...)
  echomsg printf('=> Saying: %s  (at %s)', string(a:000), localtime())
endfunction

實際 call Say('my name') 會輸出 => Saying: ['my name'] (at 1675687185)

Vim 的 Throttle

function! Throttle(fn, timeout)
  let o = {}
  let busy = 0

  function o.unlock(id) closure
    let busy = 0
    echomsg 'T: unlocked'
  endfunction

  function o.run(...) closure
    if !busy
      call timer_start(a:timeout, o.unlock)
      let busy = 1
      call call(a:fn, a:000)
    else
      echomsg 'T: throttled'
    endif
  endfunction

  return o.run
endfunction

實際用用看

let ThrottledSay = Throttle('Say', g:TEST_TIMEOUT)

" For manual test
nnoremap <F5> <Cmd>call ThrottledSay('my name', 'Throttle')<CR>

第一次執行會輸出 => Saying: ['my name', 'Throttle'] (at 1675688797)
立刻再執行會輸出 T: throttled
(第一次執行的)五秒後會輸出 T: unlocked
然後回到一開始的狀態。

解釋

  • timer_start({time}, {callback})

    Vim 的 timer 功能,最早在 2016 年的 Vim 7.4.1578 開始支援,會在 time 毫秒之後執行 callback。 (doc)

  • o.unlock

    這邊 callback 特別用 dict function 形式來寫,以避開一個煩人限制,就是函數名稱必須以大寫字母或 s: 開頭,導致很難寫出 scope 只在 function 內的函數變數,解法之一就是這個也蠻怪異的 dictionary function,這裡的 o 只是一個 wrapper dictionary,專門用來放 run 和 unlock 函數而已。

  • function closure

    JavaScript 的版本是用 closure 來保存狀態,這裡也一樣,不過 Vim 的函數要加上 closure 關鍵字才會有 closure 性質。

  • call call

    call() 就是 Vim 的 Function apply()。 (doc)

Vim 的 Debounce

function! Debounce(fn, timeout)
  let o = {}
  let timer = 0

  function o.start(args, id) closure
    call call(a:fn, a:args)
  endfunction

  function! o.run(...) closure
    if timer
      call timer_stop(timer)
      echomsg 'D: debounced'
    else
      echomsg 'D: waiting'
    endif

    let timer = timer_start(a:timeout, function(o.start, [a:000]))
  endfunction

  return o.run
endfunction

實際用用看

let DebouncedSay = Debounce('Say', g:TEST_TIMEOUT)

" For manual test
nnoremap <F6> <Cmd>call DebouncedSay('my name', 'Debounce')<CR>

第一次執行會輸出 D: waiting
立刻再執行會輸出 D: debounced 且五秒重新計算
五秒不動後會輸出 => Saying: ['my name', 'Debounce'] (at 1675688903)
然後回到一開始的狀態。

解釋

  • timer_stop({timer})

    停止之前用 timer_start() 開始的 timer,也就是取消掉,時間到了也不會做任何事。 (doc)

  • function(o.start, [a:000])

    function({name}, {arglist}) 有給第二個參數的話,會形成一個 partial function,類似 functional programming 中 curry 的用法(參考 doc), 這邊用來把使用者給的實際參數 a:000 綁到動態產生的 o.start 上去。

實戰考量

這組函數的優點是用法類似 JavaScript 的常見 API,但本質上是動態生成函數,需要再建立變數存起來(又會碰到變數名稱一定要大寫開頭的問題)才能餵給 autocmd,因為 autocmd 後面只能接 {cmd} 而不能用匿名函數,這是比較麻煩的。

還有就是 vim script 動態建立 function 的效率是不好的,我自己測試生 20,000 個函數大概需要 0.5 秒的時間(通常不會一下建這麼大量就是了)。


其他方案

直接使用 timer 函數改實作

如果只有少數主要功能要處理(例如 Say()),那可以不要用 higher-order function,直接在 Say 裡頭用 timer 函數控制,增加一些變數(例如 busytimers)做狀態管理就好,這樣就能減少建立多餘函數。

Neovim / lua

Neovim API 已經有 uv module(底層是 libuv,跟 nodejs 做到 event loop 用的是一樣的東西),另外也可以用 lua 直接塞匿名函數給 autocmd,所以不理會 Vim 相容性的話,直接用 lua 寫就好了。 有看到一個 gist Neovim throttle & debounce 給了完整範例。

vim9script

不建議,雖然新的 function compile 也許會改善效能,但很多舊的限制都還在,更重要的是目前(vim 9.0.1291)還不是進場時機,語言本身還沒實作完(class、type 等),也會直接失去 Neovim 相容性。如果想用新語言,又想支援 Vim / Neovim,也許研究 denops.vim 還比較實在。

Callbag

這個我沒有深入研究,但看到 vim-lsp 在用 callbag.vim,也同時處理掉 debounce 需求。

Callbag 是一個比較輕量的 reactive / iterable programming 規範,引用 github callbag/callbag

A standard for JS callbacks that enables lightweight observables and iterables

有了按規範實作的 debounceTime() 之後,在 vim-lsp 實際寫起來會像:

(摘自 vim-lsp/documentation.vim
let s:Dispose = lsp#callbag#pipe(
    \ lsp#callbag#merge(
    \   lsp#callbag#pipe(
    \       lsp#callbag#fromEvent('CompleteChanged'),
    \       lsp#callbag#filter({_->g:lsp_completion_documentation_enabled}),
    \       lsp#callbag#map({->copy(v:event)}),
    \       lsp#callbag#debounceTime(g:lsp_completion_documentation_delay),
    \       lsp#callbag#switchMap({event->
    \           lsp#callbag#pipe(
    \               s:resolve_completion(event),
    \               lsp#callbag#tap({managed_user_data->s:show_floating_window(event, managed_user_data)}),
    \               lsp#callbag#takeUntil(lsp#callbag#fromEvent('CompleteDone'))
    \           )
    \       })
    \   ),
    \   lsp#callbag#pipe(
    \       lsp#callbag#fromEvent('CompleteDone'),
    \       lsp#callbag#tap({_->s:close_floating_window(v:false)}),
    \   )
    \ ),
    \ lsp#callbag#subscribe(),
    \ )

其實底層 debounce 的部分也是用 timer 函數去做,但這樣包裝就能用 functional、宣告式的寫法,在大型 plugin 可以考慮一試。

重開電腦才發現 WSLg 終於下放給 Win 10 使用了(見 microsoft devblogs 文章),之前要跑 cypress 還要用 VcXsrv 搞個 X server 實在辛苦,我也是 Windows 支持者卻要受這等待遇。

那麼終於能用 Vim 的 sound_playfile('我的酷音效.wav') 了嗎? (自從 patch 8.1.1502 已經等了三年)

結果是可以發聲,但功能很有限。 我……很失望。

Vim help 舉的例子是播放 ubuntu 的系統音效,位置在 /usr/share/sounds/freedesktop/stereo,下面程式碼最後是能動的:

call sound_playfile('/usr/share/sounds/freedesktop/stereo/bell.oga')
call sound_playevent('bell')

但首先會遇上 wslg/issue #864,因為 WSLg 是在 PulseAudio 做整合而沒有打理 ALSA 底層,而 Vim +sound 看起來是跟 ALSA 溝通,所以會遇到沒有音效卡之類的問題: Vim has serveral errors when playing sound 解法如 issue 說的裝 libasound2 然後在 ~/.asoundrc 加入

pcm.default pulse
ctl.default pulse

重開 WSL 就能播放這兩個聲音了。

然而繼續試下去,發現只能播這些聲音啊,餵它別的音檔路徑是沒反應的。
原因應該是 +sound 在 ubuntu 的實作是基於 libcanberra,看它介紹是處理桌面音效的,所以不是系統主題的聲音,我可是不播的喔?

總之最後就放棄了,改用 PulseAudio 的 pactl,這個至少還堪用。

pactl upload-sample {PATH_TO_AUDIO_FILE} {SOME_ID}
pactl play-sample {SOME_ID}

總算可以簡單偵測分頁關閉,發生順序是 TabLeaveTabClosedTabEnter,在 TabClosed 的時候就能取得新的 tabpagenr('$') 了。

簡化 Vim 切換至最後選取的 tab 功能:

if exists('##TabClosed')
  function! LastTab(act)
    let lt = g:lasttab

    if a:act ==# 'TabLeave'
      let lt.prevLeave = lt.leave
      let lt.leave = tabpagenr()
    elseif a:act ==# 'TabClosed'
      let lt.leave = lt.prevLeave
    elseif a:act ==# 'switch'
      let to = lt.leave
      if to == tabpagenr()
        echo 'Already on last tab.'
      else
        execute "tabnext " . lt.leave
      endif
    endif
  endfunction

  if !exists('g:lasttab')
    let g:lasttab = {'leave':1, 'prevLeave':1}
  endif

  autocmd TabLeave * call LastTab('TabLeave')
  autocmd TabClosed * call LastTab('TabClosed')

  nnoremap <silent> <LocalLeader>t :call LastTab('switch')<CR>
  inoremap <silent> <LocalLeader>t <C-\><C-N>:call LastTab('switch')<CR>
endif

Vim 已經有一個盤古之白的專案 PanGu.vim,但我還是另外寫了 bootleq/vim-wordword

根源問題

大部分語言書寫都是以空白作文字區隔,例如 this one 兩字中間的空白;
而中文是一個例外,我們每個字都是連在一起的

當混用兩種文字系統時,例如「word文字word」,就不知道交接處要不要加空白了。
雖然不會有「word文 是一個字,字word 是另一個字」的問題,但「word文字」連在一起又似乎和「word 文字」有不同意境。

Word divider 相關花絮

  • 西方歷史上也有完全不使用空白和符號間隔的 scriptio continuaVergilius Augusteus, Georgica 141

  • 日文一般來說和中文一樣,但包含大量假名時可以有例外,比較像韓文(未確認),在真正斷字的地方加上空白。

  • CSS4 有個 text spacing 草案,但好像沒有明確相關的部分。

精簡的 Vim 盤古之白實作

因為 Vim 的 regexp 可以搜尋 word boundary,又 multibyte 文字永遠被視為 word(見 'iskeyword'選項),所以其實只要 :%substitute/\>\<./ \0/g 就搞定了加空白的工作。

Vim plugin 適合 Unix 哲學

比起處理更多(甚至包含未有定論的)排版規則,專心做好「加上空白」比較重要。
而要發揮工具的最大價值,應儘力減少在 Vim 中使用它的成本,例如提供 user-operator 或其它容易呼叫的介面。

Gblame 運用

Gblame 是什麼

tpope/vim-fugitive 提供的指令,把 git blame 結果顯示在分割視窗。
裡面按 -~P 可以對其它版本再 blame(詳見 doc),比直接在 command line 操作方便多了。

Gblame 畫面


開啟後自動 resize 到作者名

:Gblame 打開後,預設會顯示到 Date,很佔畫面,按 A 可以縮到 Author。

煩的是沒有選項能自動這件事,只好另設一個 command,在 :Gblame 後多送一個 A

command! -nargs=0 GBlameA Gblame | call <SID>fugitiveblame_after()
function! s:fugitiveblame_after() "{{{
  call feedkeys('A')
endfunction "}}}

然後用 tyru/vim-altercmd 方便輸入……

AlterCommand gb[lame] GBlameA

bling/vim-airline 狀態列

很微小的修改,原本狀態列會顯示 fugitive tmp file 的名稱,我把它改成 "Gblame"。

function! AirlineGblame(...)
  if &filetype == 'fugitiveblame'
    let w:airline_section_a = ''
    let w:airline_section_b = ''
    let w:airline_section_c = 'Gblame'
    let w:airline_section_y = ''
  endif
endfunction
let g:airline_statusline_funcrefs = []
call airline#add_statusline_func('AirlineGblame')

Git compare

配合 bootleq/vim-gitdiffall 使用, 按 gf 將檔案變更以 vim diff 開在新 tab,看那個 revision 改了什麼:

gitdiffall 開在新 tab

<LocalLeader>gf 則是開啟新的 tmux window,把該版本變更的檔案全部打開:

gitdiffall 開在新的 tmux window

function! s:fugitiveblame_gitdiffall(all) "{{{
  let rev = matchstr(getline('.'), '\v^\w{7}')
  let buffer = fugitive#buffer(bufname(b:fugitive_blamed_bufnr))
  if a:all
    call TmuxNewWindow({
          \   "text": "gitdiffall @" . rev,
          \   "title": '⎇',
          \   "directory": buffer.repo().tree()
          \ })
  else
    execute printf("tabnew %s | silent GitDiff @%s", buffer.path(), rev)
  endif
endfunction "}}}

autocmd! my_vimrc FileType fugitiveblame
      \ nnoremap <buffer> gf :call <SID>fugitiveblame_gitdiffall(0)<CR> |
      \ nnoremap <buffer> <LocalLeader>gf :call <SID>fugitiveblame_gitdiffall(1)<CR>
function! TmuxNewWindow(...) "{{{
  let options = a:0 ? a:1 : {}
  let text = get(options, 'text', '')
  let title = get(options, 'title', '')
  let directory = get(options, 'directory', getcwd())
  let cmd = 'tmux new-window -a '
        \ . (empty(title) ? '' : printf('-n %s', shellescape(title)))
        \ . printf(' -c %s', shellescape(directory))
  call system(cmd)
  if !empty(text)
    let cmd = printf('tmux set-buffer %s \; paste-buffer -d \; send-keys Enter', shellescape(text))
    call system(cmd)
  endif
endfunction "}}}

這裡比較悶的是 Gblame 的 mapping 都寫死死,最後只好選 gf 來用。
看 issue 感覺是 tpope 不大想理 mapping 的彈性。

一、tpope/vim-rails

我最常用的是 :RT:Tmodel foo:A


二、basyura/unite-rails

可設定按下 ,fR 就打開 Unite 介面,選擇 Rails 相關的 source。

nnoremap [unite] <Nop>
nmap <LocalLeader>f [unite]
nnoremap <silent> [unite]R :<C-U>Unite -start-insert -input=rails/ source<CR>

於是按下 ,fR 就顯示
Unite rails/xxx

接著選擇 initializer,就能進入 rails/initializer 這個 source 的畫面了。
※下圖我按了 ke 示範一下 filter 功能
搜尋 initializers


這兩個 plugin 的功能都不限於「尋找檔案」,這篇就不多說了。

我的 FuzzyFinder 壞掉了

忘了何時開始,本機 FuzzyFinder 初次打開時,如果前幾個字輸入太快,就會「過早自動完成」補成一條無效候選。

例如 FufFile 的 mapping 是 ,f 的話,輸入 ,ff 就會出現下圖狀況,非常惱人。

過早自動完成是萬惡的根源

調查不出原因,只好放棄,反正沒人維護了,就跟 FuzzyFinder 說拜拜吧。

最後一張照片,親愛的 fuf

用了三年的 FuzzyFinder 介面

替代品當然是 unite.vim

之前寫過一篇簡介「試試新的萬能尋找器 unite.vim」,那時就它用來輔助 FuzzyFinder 了,選 action 的介面有時候真的比較方便,現成的實用 source 又多。

而且 2011 至今,幾乎每次跑 BundleInstall 都看到它在更新,相當驚人。 回報 bug 很快就會被處理,這點相當酷。

豪放的大視窗

Unite buffer 開出來整個外觀如下圖,分割位置、寬或高、buffer 名稱、訊息(兩行 [file_rec] 的部分)都可以調整。

Unite file_rec 整體外觀

輸入方式(narrowing 語法)的變更

需要重新習慣的不多:

  1. FuzzyFinder 以 ; 分隔多個關鍵字,在 unite.vim 改用空白。
  2. 離開 Insert Mode 不會直接退出 unite buffer,要用另外的 mapping 才行。

而關鍵字與候選範圍的比對關係,在 unite.vim 被抽出為 matcher,可以用 unite#filters#matcher_default#use()unite#custom_source()unite#set_substitute_pattern() 等方式作細部調整。

Matcher 可以換成 matcher_regexp、matcher_fuzzy、……,可惜文件不清,我只能邊改邊試。

set_substitute_pattern 可以做到輸入 $HOME 展開 $HOME,輸入 gem/ 展開 ruby gem path,理論上應該能自由調整 narrowing 用語。 ※我是參考這篇 unite.vim の設定を一部晒してみる - 永遠に未完成調的。

我的 mappings

註:我認為分享 key mapping 無聊透頂,尤其選的按鍵只是習慣、沒有語義,那你改個按鍵也要分享一篇嗎?

這裡只是形容一下操作的感覺,有些按鍵是預設的、沒改。

  • ,ff - 開啟 Unite file 介面,由目前 buffer 所在目錄開始搜尋。
  • ; - 在 unite buffer Insert Mode 向前刪除一個 word,反悔或打錯字時用。
  • <Ctrl-J><Ctrl-K> - 移動到下一個/上一個候選。如果在 Insert Mode,會自動進入 Normal Mode,然後只要按 j k 就能上下移動。
  • <Tab> - 對選中的項目,開始選擇 action。
  • <CR> - 對選中的項目執行 default action,例如檔案的預設 action 我調成在新分頁開啟。
  • ,<CR> - 對選中的項目執目第二 default action(目前 unite.vim 沒有所謂第二 action,要自己設 mapping),我設定成在目前分頁開啟檔案。
  • <F5> - 在 unite buffer 中,重新整理,清除候選快取。
  • ,q - 退出一層 unite buffer。
  • ,Q - 退出全部 unite buffer。

API 與設定感覺

其實設計蠻漂亮的…… sourcekind 是要處理的情境和候選物件類型, action 是要執行的動作, filter 包含 matcher、sorter、converter 的微調。 以上定義清楚,設定時的指令自然會乾淨,這部分做得不錯,比 FuzzyFinder 進步多了。

比較惱人的是文件趕不上開發速度,英文不太講究、repo 裡的 commit message 也敘述不清。 有點難搞。

目前常用的 source

※更多 source,參考 unite plugins · Shougo/unite.vim Wiki

  • file - 最常用的就是這個。
  • file_mru (most recent used files)
    Unite file_mru
  • register - 多行內容也能顯示喔。
  • bookmark - 目錄、檔案、位置都通,比 fuf 方便。
  • help - 但要用 tags-caching 這個 branch 才有快取,否則實在太慢。
    Unite help
  • session
  • tab
  • outline - 有了這個,幾乎就沒在開 Tagbar 了(雖然我本來就少用 tag)。

還不大會用的其他功能

  1. file_rec source

    Fuf 要找深層目錄下的檔案,會使用 **/ 查詢。 Unite.vim 也能使用 **/ 寫法,我一般用這個就 OK 了。

    而 unite.vim 還有專門做深層搜尋的 file_rec source,作者也經常磨亮這個功能。 不過目錄樹很大時,總是會跑到 too many candiates(預設 1000 項),加大快取數又要跑很久。 解法應該是用另外的 file_rec/async source,可以非同步發動更快的搜尋,但需要裝別的東西,我就沒試了。

  2. Quick Match

    類似 EasyMotion 的選擇法,打一個鍵就能對某個項目動作。 還沒想到要用在哪,也許適合用在 bookmark 吧。

    :Unite -quick-match alignta

總之一下子就能習慣,早就可以換了

我目前的設定放在 bootleq/vimrc_human/.vimrc#L808-923 at 500f59,沒什麼特別的,doc 裡也有範例,純擺設。

舊篇「文字對齊 plugin - Alignta 使用方式」已過時,本文以 Alignta 0.3.0 為準重寫。
※變更項目參考原 doc(日文)alignta.jax#更新履歴 - 2011-09-25。


Alignta (h1mesuke/vim-alignta) 是什麼?

對齊文字用的 plugin,效果舉例如下,將多行文字依等號對齊:

  原文           :Alignta =
  ------------   ------------
  a = 1          a     = 1
  bbb = 10       bbb   = 10
  ccccc = 100    ccccc = 100

另一支比較有名的對齊 plugin 是 Dr. ChipAlign.vim,可惜用法難懂,不容易記; Alignta 或許功能沒那麼豐富 (?),但實在清楚多了,名字也比較可愛。


SYNOPSIS of Alignta command (for v. 0.3.0, UNOFFICIAL)

                           (Shifting
                            Alignment)
                              <-
                              ->           n
                              <--
                              -->

:[range]Alignta [filter]  [align method][margin]    [-p] [-erER] pattern[/{count}] [pattern2 ...]

               g/pattern   (Padding                                       nunber
               v/pattern    Alignment)                                     'g'
                               <           n:n
                               |        [0-9][0-9]
                               >          [0-9]
                               =
                           (repeat as
                             LM[R]...)
  • filter (doc#556)

    指定哪些特殊的行(通常是註解)要/不要進行對齊。

    g/pattern 只處理符合的行;v/pattern 不處理符合的行。

  • align method

    Alignta 的對齊分為 ShiftingPadding 兩種,參數也分為兩套。

    Shifting Alignment (doc#153) 比較簡單,只是將 pattern 的開頭垂直排在一起。
    以下參數表示採 Shifting 對齊: (doc#445)

    • <- 將每一行符合 pattern 的部分,對齊到其中最靠左的 column
    • -> 同上,但對齊到最靠右的 column
    • <--
    • --> 兩個 dash 的版本表示用 tab 字元排版

    Padding Alignment (doc#121) 應該比較常用,依 patten 將文字拆成多個欄位,每行再補上空白(padding)使各欄等寬。

    各欄以 {L}{M}{R} 為一個單位,{M} 就是 pattern 符合的位置,{L}{R} 是它的左右側。 寫法是以 <|>= 指定各單位靠左、靠中、靠右或保持原本方式對齊, 例如 :Alignta <|> = 會讓等號兩邊靠外對齊。 (doc#304)
    註:{R} 可省略,表示跟 {L} 相同,例如 <=<=< 同義。
    註:若後面有指定多個 pattern,這裡可以重覆 {L}{M}{R} 來指定多個對齊方式。

  • margin (doc#304)

    指定各欄位的間隔。

    Padding 對齊時,間隔是指 {L}、{R} 和 {M} 之間的空白數; 寫法是 {L-margin}:{R-margin},例如 :Alignta <<3:1 =
    可簡寫為 {L-margin}{R-margin} 或 {LR-margin},但 margin 必須是個位數 [0-9](10 就不行了),例如 46 即 4:6,4 即 4:4。

    Shifting 對齊時,間隔是指 pattern 左側至少要留的空白數。 單純寫個數字即可。

  • -e -r -E -R (doc#227)

    明確指定後面 pattern 的解析方式。

    • -e 視為純文字
    • -r 視為 regexp pattern
    • -E 之後的 pattern 一律視為純文字
    • -r 之後的 pattern 一律視為 regexp pattern

    例如 :Alignta -e \d 是以 \d 對齊,而非以數字(\d)對齊。

  • -p (doc#592 )

    若後面的 pattern 是特殊字,加上 -p 可確保它被視為 pattern。 例如 :Alignta -p << 中的 << 是指 pattern,而非對齊方式。

  • pattern[/{count}]

    指定要用什麼來對齊。(doc#188)

    若 pattern 包含 \.,就會被視為 regexp 解析,否則一律以純文字處理。

    pattern 後面可以加 /{count},指定可符合幾次,超過 count 就不再對齊(doc#254), 例如 :Alignta ={2} 只會處理前兩個等號,之後的就保持原狀。
    另 count 使用 /g 表示有 pattern 符合時才對齊,否則不理它。

    註:count 預設值在 Shifting 對齊時是 /1,Padding 對齊時是 /g
    註:Alignta 的參數若不符合其他任何 option 寫法,就會被視為 pattern。
    註:若有設定 alignta_default_arguments 變數,則此參數也能省略。

  • pattern2 ...

    支援複數的 pattern,直接寫在後面即可,例如 :Alignta = /* */。 (doc#287)


可設定的變數 (doc#618)

  • g:alignta_default_options
    :Alignta 的預設對齊選項。 參數中未指定對齊選項時,就會以此為準,預設為 "<<<1:1"
  • g:alignta_default_arguments
    直接下 :Alignta 不帶參數時,會使用的預設參數。 無預設值。

以上變數也可使用 b: 前綴設定 buffer local 版,例如 b:alignta_default_arguments


Unite.vim 介面 (doc#654)

如果配合 Shougo/unite.vim 一起使用,可以做到:

  • 先把常用的對齊方式定義好,以後選取文字 → 呼叫 :Unite alignta:arguments,就能在 unite 介面中直接選擇對齊方式,不必再想指令了。
  • 直接呼叫 :Unite alignta:options,就可以在 unite 介面中調整 :Alignta 的預設選項(可事前定義),也有提供方便的 <RESET> 選項。

Tmux 1.7 新增的 bracketed paste mode 支援,可以讓 Vim 分辨現在是 tmux 貼上而不是手動打字。

引用 Bracketed Paste Mode | dodaの日記 | スラッシュドット・ジャパン(稍有修改):

" Ref http://slashdot.jp/journal/506765/Bracketed-Paste-Mode
" - Use tmux 1.7 `paste-buffer -p` to paste
" - Use <F11> and tmux `send-keys "\e[201~"` for pastetoggle
if &term =~ "xterm" && exists('$TMUX')
  let &t_ti = &t_ti . "\e[?2004h"
  let &t_te = "\e[?2004l" . &t_te
  let &pastetoggle = "\e[201~"
  map <F11> <Esc>[201~
  imap <F11> <Esc>[201~

  function! XTermPasteBegin(ret)
    set paste
    return a:ret
  endfunction

  map <special> <expr> <Esc>[200~ XTermPasteBegin("i")
  imap <special> <expr> <Esc>[200~ XTermPasteBegin("")
  cmap <special> <Esc>[200~ <nop>
  cmap <special> <Esc>[201~ <nop>
endif

\e[?2004h\e[?2004l 是啟用括弧貼上模式的控制碼,即 XTerm Control Sequences 中的
P s = 2 0 0 4 → Set bracketed paste modeP s = 2 0 0 4 → Reset bracketed paste mode

Tmux 1.7 的 paste-buffer 指令新增了 -p 選項,會在貼上文字前加 ESC[200~、後加 ESC[201~ 控制碼。 (參考 SF.net SVN: tmux:[2712] trunk

利用這兩個控制碼設定 mapping,就能自動調整 'paste' 的狀態了。

不過這個 pastetoggle 會把原本的設定蓋掉,變得無法一鍵切換 'paste'
將就方案是在 tmux 設定,例如我原本是用 <F11>,就加上 bind F11 send-keys "\e[201~"

預設的 :TOhtml 會生成整個 HTML document,我只要其中的 <pre>...</pre> 部分而已,於是寫了一層 function 處理,原功能則改名 TOhtmlDoc。

最近發現這 function 失效,似乎是 TOhtml 在七月有變更,原輸出 <pre> 會變成 <pre id='vimCodeElement'>

修正後 script 如下:

let g:html_no_progress = 0
let g:html_use_css = 1
let g:html_ignore_conceal = 0
let g:html_pre_wrap = 0
let g:html_use_xhtml = 0
function! s:to_html(line1, line2)
  let save_number = get(g:, 'html_number_lines', -1)
  let g:html_number_lines = 0
  call tohtml#Convert2HTML(a:line1, a:line2)
  setlocal buftype=nofile bufhidden=hide noswapfile nobuflisted
  call search("<pre[^<]*>")
  normal! dit
  %delete _
  let @" = '<pre>' . substitute(@", '\v^\n\s*', '', '') . '</pre>'
  call setline(1, split(@", '\n'))
  if save_number > -1
    let g:html_number_lines = save_number
  else
    unlet g:html_number_lines
  endif
endfunction
command! -range=% TOhtml :call <SID>to_html(<line1>, <line2>)
command! -range=% TOhtmlDoc :call tohtml#Convert2HTML(<line1>, <line2>)

neocomplcacheneocomplcache-snippets-complete 再到 neosnippet,舊設定基本上不必修改。 如 README #Configuration 的內容就很好用,而即使沿用舊 command 名稱、不換成 neosnippet#expandable 等新名字也還是相容。

不過 snippet 語法有變(doc FAQ 有寫),我的 1 個展開就這麼壞了。

例:Ruby 的 in 展開為 inspect

NeoSnippet 預設不會在 .in 的情境(前文是 .)展開,要加上 options word 才行。解決。
另外多了 regexp 選項,可以限制在 . 之後的 in 才展開,又比以前更聰明了。

snippet inspect
alias in
regexp '\.in'
options word
  inspect
※ 這個 snippet 其實改成 snippet .in 也行啊。

Vim 開啟大檔案會變慢、慢到難以忍受的問題,通常停用一些功能就會改善了。 極端點用 vim -u NONE filename(不讀 vimrc 也不開任何外掛)開檔,就知道是檔案太大還是設定問題。

Dr Chip 的 LargeFile plugin 就是在偵測到大型檔案時,自動關閉 syntax、folding 等昂貴功能,離開 buffer 時再調回來。 使用上不必多做什麼動作,就能達到明顯加速效果,20 萬行的 rails log 開起來也沒什麼感覺。


原版 LargeFile 設定較少,所以我的 .vimrc 一直有整段抄襲版的 BigFile 實作,用了好幾年,一直有解不開的怪問題,發作起來會把全域 'undolevels' 設為 -1 害我完全不能 undo,非常困擾。

這次直接 fork 原版 LargeFile,程式先重構一下,居然就解掉了……
自訂的選項也都加了進去,專案在 bootleq/LargeFile,再測一陣子就會送交原作者處置。


具體設定例

基本選項,就這幾個:

let g:LargeFile           = 40            " 超過這個 size 才會處理。 預設是 20
let g:LargeFile_size_unit = 1024          " g:LargeFile 的單位,1024 就是 KB。 預設是 MB
let g:LargeFile_patterns  = '*.log,*.sql' " 只處理 log 和 sql 檔案。 預設是 *
let g:LargeFile_verbose   = 0             " 不要顯示訊息(例如 LargeFile 生效時)。 預設是 1

LargeFile 會停用 FileType autocmd,如果想對特定檔案動手腳,可使用新增的 autocmd 事件:

autocmd User LargeFileRead call s:large_file_read()   " 每次 Large File 發生 BufRead 時發動
autocmd User LargeFile call s:large_file_detected()   " 偵測到 Large File 時發動,包含 :edit! 或 jump 進入時

function! s:large_file_detected()
  let ext_name = expand('%:e')
  if ext_name == 'log'
    " nnoremap <buffer> <LocalLeader>ddd :EmptyFile<CR>   " 按這個鍵把 log 全清掉(實作省略……)
  endif
endfunction

function! s:large_file_read()
  let dir_name = expand('%:p:h')
  if dir_name =~ '/home/www/some_rails_app/\(\w\+/\)\?log'
    if &fileencoding != 'utf-8'   " 不是 utf-8 就重讀吧
      edit! ++enc=utf-8
    endif
    syntax match railslogEscape '\e\[[0-9;]*m' conceal    " 有這個才不會看到一堆色碼(需要裝 rails.vim)
  elseif dir_name == '/home/www/logs'
    set syntax=httplog    " 上色(需要裝 httplog)
  endif
endfunction

xml.vim(原 vim script 1397)這個 plugin 提供一些小功能,寫 HTML 的時候很實用。
不過檢討一下發現,我只用到其中一兩個 mapping 而已…… 最後決定移除,自己重製。

各種功能重現

  • 自動關閉 tag

    <div 後面輸入 > 時,會自動補上 </div> 並把游標放回 tag 中間。
    現改為以下 function,按 >> 發動。
    inoremap <buffer> >> <C-\><C-N>:call <SID>html_close_tag()<CR>
    function! s:html_close_tag()
      " Find < or >, should take action only when < appears first.
      if search('\v(\<)|(\>)', 'bnpW') == 2
        let missing_gt = getline('.')[col('.')-1] == '>' ? '' : '>'
        execute "normal! a" . missing_gt . "</" . "\<C-X>\<C-O>" . "\<C-\>\<C-N>F<"
        call feedkeys('i', 'n')
      else
        call feedkeys('a>>', 'n')
      endif
    endfunction
    無論在 <div<div> 後都能正常運作。
  • 將 tag 拆為多行

    前述 <div>|</div>再按一次 >(| 為游標位置)的話,tag 會被拆為
    <div>
      |
    </div>
    這個我沒做。 但可配合下面的 function 改為按 >> 後再按 ;; 達成。
  • 自動建立 tag

    輸入 div;; 就會展開為
    <div>
      |
    </div>
    且如果該 tag 前面已有文字,就不會拆為多行,而是變成單行的 <div>|</div>
    新的實作如下:
    inoremap <buffer> ;; <C-\><C-N>:call <SID>html_make_tag()<CR>
    function! s:html_make_tag()
      let save_reg = [getreg(), getregtype()]
      execute "normal! a \<C-\>\<C-N>db"
      let tag_name = @"
      call setreg(v:register, save_reg[0], save_reg[1])
    
      " Skip invalid tag_name (non-words)
      if tag_name =~ '\W'
        " Special case to break single-line tag into multiline form. (that is, 'make' an existed tag)
        " e.g.: when input ;; between <div></div>
        if tag_name == '>' && search('\%#\s*<', 'cnW', line('.'))
          execute "normal! cl" . tag_name . "\<C-\>\<C-N>a\<CR>"
          call feedkeys('O', 'n')
        else
          execute "normal! cl" . tag_name
          call feedkeys('a;;', 'n')
        endif
        return
      endif
    
      let unaryTagsStack = get(b:, 'unaryTagsStack', "area base br col command embed hr img input keygen link meta param source track wbr")
      if index(split(unaryTagsStack, '\s'), tag_name, 'ic') >= 0
        execute "normal! a<" . tag_name . ">"
        call feedkeys('a', 'n')
      else
        " Don't break tag into multi lines if current line is not empty.
        if getline('.') =~ '\S'
          execute "normal! cl<" . tag_name . "></" . tag_name . ">\<C-\>\<C-N>F>"
          call feedkeys('a', 'n')
        else
          execute "normal! cl<" . tag_name . ">\<CR></" . tag_name . ">\<C-\>\<C-N>O"
          call feedkeys('"_cc', 'n')
        endif
      endif
    endfunction
  • 在開/關 tag 之間移動

    <div> 上按 % 就能跳到 </div> 的位置,也可來回跳。
    這個其實不必裝 xml.vim,啟用 matchit 就能辦到了,見 :help matchit
  • change 外圍 tag

    <di|v>foobar</div>\c,可以把成對的 div 取代為別的值。
    這個也不必裝 xml.vim,改用 surround.vim 比較實在。
    按鍵是 cst< (Change Surrounding Tags with Tag...) 就會提示你輸入 tag name。

備註

  • 本文的 2 個 inoremap 正確用法是在 filetype 為 html 時才生效。
  • 以上實作會依賴一些選項,譬如 auto close tag 是藉由 </ 後的 omni-completion 達成,換行部分可能也需要正確的 indent 設定。
  • xml.vim 還有一些額外功能,例如處理 HTML comment 什麼的,因為沒在用,我就沒研究了。