はじめに
fzfとは,percolやpecoと同様,絞り込みの検索を行うことのできるコマンドラインツールである. 日本では,percolやpecoが有名で,fzfはあまり有名ではないが,海外では有名であるらしい. fzfはVimから利用できるように,公式のリポジトリにAPIを提供するVimプラグインが付属している. この記事では,fzfをVimから使う方法について述べる.
fzfの特徴
fzfはGoで実装されている. Goで実装されているから,マルチプラットフォームなのかと思ってしまうが,実は Windowsでは利用することができない(Cygwin上では利用可能).
fzfを利用したプラグインの作り方
正直,fzfのREADME.mdやwikiのサンプルを見るのが早いのだが,それではこの記事の意味が無いので,ちゃんと書く. (日本語で書いておくと,日本人が読みやすいという利点もあるだろうし)
まず,fzfをVimから利用するためには,fzfが提供しているプラグインに runtimepath
を通す必要がある.
これはfzfをクローンした場所にもよるが,
$ git clone https://github.com/junegunn/fzf.git ~/.fzf
としたならば,
set rtp+=~/.fzf
とするだけでよい.
すなわち,fzfのルートディレクトリを runtimepath
に追加するだけでよい.
上記の場合,fzfのVimプラグイン本体は ~/.fzf/plugin/fzf.vim
にあるので,Vimからfzfの関数を利用できるようになるわけだ.
fzfをVimから利用するのは簡単で, fzf#run()
に渡す適切な辞書を定義するだけだ.
後は,その辞書を第一引数とし, fzf#run()
をコールするだけで,Vimからfzfを利用できる.
例えば,wikiには,
nnoremap <silent> <Leader>C :call fzf#run({ \ 'source': map(split(globpath(&rtp, "colors/*.vim"), "\n"), \ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"), \ 'sink': 'colo', \ 'options': '+m', \ 'left': 30 \})<CR>
というfzfを用いるキーマッピングの例が紹介されている. これは,利用可能なcolorschemeを候補として表示し,選択したcolorschemeに変更するというものだ.
この fzf#run()
に渡す辞書のキーと値には以下のようなものが有効である.
同じキーであっても,値の型によって挙動が異なる点に注意しよう.
source
- 値の型が 文字列 のとき,その文字列を外部コマンドとして実行し,その結果をfzfに渡す
- 値の型が リスト のとき,このリストをfzfに渡す
sink
sink*
- 値の型は 関数参照 のみで,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
- 複数選択された候補を リストとして扱う
- すなわち,選択された候補はリスト化されて,関数の第一引数に渡される
options
- 値の型は 文字列 のみで,fzfの起動オプションを指定する
dir
- 値の型は 文字列 のみで,fzf起動時の作業ディレクトリを指定する
up
,down
,left
,right
- 値の型は 数値 または 文字列
- 数値型では行数,または列数を指定できる(
20
など) - 文字列型では行,または列の占める割合を指定できる(
'50%'
など) - 文字列型の場合,末尾に
%
を付けないと,行数,または列数の指定になる('50'
では50行,または50列で50%でない )
- 数値型では行数,または列数を指定できる(
- tmux上でVimを起動している場合のみ
- 値の型は 数値 または 文字列
window
- neovimのみ
- 値の型は 文字列 のみで,fzfのウィンドウを開くコマンドを指定する(例:
vertical aboveleft 30new
)
launcher
キーとして必ず持つべきものは,候補に対するアクションを担当する sink
または sink*
である.
候補の取得を担当する source
は,後述するが,ほぼ必須の項目である.
なお, sink
と sink*
の2つがある場合, sink
, sink*
の順番に処理がなされる.
ちなみに, source
を指定しない場合,デフォルトのfzfの動作(再帰的なファイル検索)になるので,単純にファイルを検索したいのであれば, source
を敢えて指定しないというのも手だ.
単純なファイラとして用いるのであれば,以下のようにするとよいだろう.
call fzf#run({'sink': 'edit'})
sink
も指定しなかった場合,候補を選択しても何のアクションも行われない.
" カレントディレクトリ以下のファイルが再帰的に表示されるが, " 候補を選択しても,何のアクションも行われない call fzf#run({})
プラグインに組み込む
僕の場合,fzfをプラグインに導入する場合,実装を別ファイルに分割している.
unite.vimやctrlp.vimに習い,分割したファイルは autoload/fzf/
に配置することにする.
以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo
するという単純なサンプルである.
plugin/sample.vim
if exists('g:loaded_sample') finish endif let g:loaded_sample = 1 let s:save_cpo = &cpo set cpo&vim command! FZFSample call fzf#run(fzf#sample#option()) let &cpo = s:save_cpo unlet s:save_cpo
autoload/fzf/sample.vim
let s:save_cpo = &cpo set cpo&vim function! fzf#sampleoption() abort return { \ 'down': 20, \ 'sink': function('s:sink'), \ 'source': s:gather_candidates() \} endfunction function! s:gather_candidates() abort return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)') endfunction function! s:sink(candidate) abort echo len(readfile(a:candidate)) endfunction let &cpo = s:save_cpo unlet s:save_cpo
autoload/fzf/sample.vim
の fzf#sample#option()
で返却する辞書の一部の項目は,いちいち新しく定義し直す必要は無いので,以下のように記述してもよいだろう.
let s:save_cpo = &cpo set cpo&vim let s:option = { \ 'down': 20 \} function! s:option.sink(candidate) abort echo len(readfile(a:candidate)) endfunction function! fzf#sample#option() abort let s:option.source = s:gather_candidates() return s:option endfunction function! s:gather_candidates() abort return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)') endfunction let &cpo = s:save_cpo unlet s:save_cpo
この例では,候補の取得をVim Script側で行っているが,カレントディレクトリのファイルを検索するのなら,外部コマンドを用いてもよいだろう. fzfがコマンドラインツールであることを考えれば,そちらの方が好ましいかもしれない.
let s:save_cpo = &cpo set cpo&vim let s:option = { \ 'down': 20, \ 'source': 'find -maxdepth 1 -find f' \} function! s:option.sink(candidate) abort echo len(readfile(a:candidate)) endfunction function! fzf#sample#option() abort return s:option endfunction let &cpo = s:save_cpo unlet s:save_cpo
複数選択したい場合
先述の例では触れていないが,fzfの拡張は複数選択できるようにすることが可能である.
複数選択はfzf自体の機能にあるので, options
にfzfのオプション -m
,または --multi
を指定するだけでよい.
先述の例であれば,以下のように s:option
を変更するだけだ.
let s:option = { \ 'down': 20, \ 'options': '-m' \} function! s:option.sink(candidate) abort echo len(readfile(a:candidate)) endfunction
このとき, sink
に指定したアクションが,選択した各候補に対して実行される.
もし,複数選択したものを,Vim Script側ではリストとして扱いたいならば, sink*
を利用するとよい.
function! s:sink(candidates) abort for candidate in a:candidates echo len(readfile(candidate)) endfor endfunction let s:option = { \ 'down': 20, \ 'options': '-m', \ 'sink*': function('s:sink') \}
感想
fzfはあくまで外部ツールであって,Vimからfzfに操作を移さなければならない点が残念に思えた. 僕としては,外部ツールはあくまでシームレスに用いることができるようにしてほしいと思っている. ただ,tmux上のVimで用いた場合に,候補選択用の新しいペインを作成する点や,neovimに対応している点など,なかなか面白い試みをしていると感じた.
また,複数選択に対応している点も魅力的だと感じた. 候補選択型インターフェースで,ユーザが作成する拡張で,複数選択ができるものを作ることができるのは,実質unite.vimとfzfぐらいのものではないかと思われる(ctrlp.vimは特殊な場合に限り,複数選択可能).