unite-filters の converter を活用しよう

この記事は Vim Advent Calendar 2012 293日目の記事になります。

[元ネタ]


http://lingr.com/room/vim/archives/2013/09/16#message-16690895


元々は Lingr での Lindan さんの発言がきっかけだったのですが、今回は unite-filters を利用して memolist.vim の内容をいい感じに unite.vim で出力してみたいと思います。
コンセプトとしては専用の unite-source を定義するのではなくて unite-filters で頑張る感じ。

[unite-filters とは]

今回、主に使用する unite-filters の converter なのですが、unite-filters には大きく分けて3種類があります。

  • matcher
    • 入力されたワードから候補の絞り込みを行う
  • sorter
    • 候補の並び順を制御する
  • converter
    • 各候補に対する処理を行う
      • e.g.表示形式を変更したり


unite-filters は各 unite-source に対して設定する事ができます。
例えば unite-file で『更新日順に表示したい』のであれば以下のような設定する事ができます。

" 昇順
call unite#custom#source('file', 'sorters', ['sorter_ftime'])
" 降順
call unite#custom#source('file', 'sorters', ['sorter_ftime', 'sorter_reverse'])


unite.vim ではデフォルトでいくつかの unite-filters が用意されているので覚えておくと便利です。
デフォルトで用意されている unite-filters は、

:help unite-filters

を参照して下さい。


また、基本的には3つとも候補のデータに対して処理を行うのですが、呼ばれるタイミングが違っていたりします。
呼ばれるタイミングに関しては以下の Issues が参考になるかと思います。

[unite-memolist]

さて、本題。
memolist.vim で保存されているメモファイルは以下のように unite-file を使用することでファイルの一覧を unite.vim で表示する事ができます。

execute "Unite file:" . g:memolist_path


これで g:memolist_path 内のファイルが unite-file で出力されます。
しかし、このままでは色々と不便なので実際には以下のように g:unite_source_alias_aliases へと予め設定しておきます。

let g:unite_source_alias_aliases = {
\   "memolist" : {
\       "source" : "file",
\       "args" : g:memolist_path,
\   },
\}


このように設定を行うことで、

:Unite memolist

のように unite-source を定義した時と同じような呼び出しを行う事ができます。
また unite#custom#source() で unite-filters の設定を行うこともできます。

call unite#custom#source('memolist', 'sorters', ['sorter_ftime'])


これをベースとして色々と converter を設定していきます。

[unite-filters-collection]

unite.vim はデフォルトでいくつかの unite-filters が用意されています。
しかし、それとは別に汎用的な unite-filters をまとめた unite-filters-collection というプラグインを実験的に basyura さんと作成していたりします。
以下の設定ではこのプラグインで定義されている unite-filters を使用しているので予めインストールしておく必要があります。

NeoBundle "osyo-manga/unite-filters-collection"

[候補の表示に更新日を追加する]

さて、実際に converter を設定してみます。
unite-file_mru のように候補に更新日が表示されているとわかりやすいですね。
unite-filters-collection の converter_add_ftime_abbr を使用すると候補の先頭に更新日を追加します。

" abbr の先頭に更新日を追加する
call unite#custom#source('memolist', 'converters', ["converter_add_ftime_abbr"])

[abbr と word の違い]

unite-source を書いたことがある人は知っているかもしれませんが、表示する候補の文字列は abbr や word に設定を行います。
この abbr と word は非常によく似ているのですが、以下のような違いがあります。

  • 実際にバッファで表示される文字列は abbr の方を優先して使用される
  • 絞り込みを行う対象は word の方を優先して使用される


なので、候補として表示したいけど絞り込みには使用してほしくない文字列は abbr に、逆に表示したくはないけど絞り込みを行って欲しい文字列は word に設定するとよいです。
これを知っていると無駄な絞り込みや表示するデータをなくす事ができるので覚えておくとよいと思います。
余談ですが、unite#get_current_unite() で現在のデータの中身を参照する事ができます。
実際 abbr や word がどんな値なのかを確認したい場合は利用できるかと思います。

[memolist の1行目を候補に表示する]

ついでにファイルパスではなくて memolist のファイルの1行目を候補に表示させてみます。
これも unite-filters-collection で定義されているものを使用します。

call unite#custom#source('memolist', 'converters', ["converter_file_firstline_abbr", "converter_add_ftime_abbr"])

[word に対して converter る]

さて、今までは abbr に対して設定してきましたが、次に word への設定を行います。
word へ設定する事でその設定したワードで絞り込みを行うことができます。
基本的には xxx_abbr と同様に xxx_word の設定を行うのですがここで注意があります。
word には絞り込みを行いたい文字列の設定を行うのですが、converter で word の設定を行うと処理の順番の関係で絞り込みの後に word が設定されてしまいます。
それを回避するために 'converters' ではなくて以下のように 'matchers' へ設定を行います。
こうすることで converter の後に matcher の処理が行われるようになります。

call unite#custom#source('memolist', 'matchers', ["converter_file_firstline_word", "matcher_default"])

[tags を word に追加する]

ついでに tags も word に追加してみます。
こうすることで tags に設定された文字列で絞り込みを行う事ができます。

let s:filters = {
\   "name" : "converter_add_memolist_tags_word",
\}

function! s:get_tags(filepath)
    return filereadable(a:filepath) ? matchstr(get(filter(readfile(a:filepath), 'v:val =~ ''^tags: \[.*\]'''), 0), '^tags: \[\zs.*\ze\]') : ""
endfunction

function! s:filters.filter(candidates, context)
    for candidate in a:candidates
        if !has_key(candidate, "converter_add_ftime_word_base")
            let candidate.converter_add_ftime_word_base = candidate.word
        endif
        let word = get(candidate, "converter_add_ftime_word_base", candidate.action__path)
        let candidate.word = s:get_tags(get(candidate, "action__path")) . " " . word
    endfor
    return a:candidates
endfunction

call unite#define_filter(s:filters)
unlet s:filters


call unite#custom#source('memolist', 'matchers', ["converter_file_firstline_word", "converter_add_memolist_tags_word", "matcher_default"])


word へ設定しているので実際に表示はされません。
が、絞り込みは行われます。
わかりづらい場合は abbr にも一緒に設定してしまうとよいと思います。

[まとめ]

すべてのコードをまとめると以下の様になります。

let g:unite_source_alias_aliases = {
\   "memolist" : {
\       "source" : "file",
\       "args" : g:memolist_path,
\   },
\}


let s:filters = {
\   "name" : "converter_add_memolist_tags_word",
\}

function! s:get_tags(filepath)
    return filereadable(a:filepath) ? matchstr(get(filter(readfile(a:filepath), 'v:val =~ ''^tags: \[.*\]'''), 0), '^tags: \[\zs.*\ze\]') : ""
endfunction

function! s:filters.filter(candidates, context)
    for candidate in a:candidates
        if !has_key(candidate, "converter_add_ftime_word_base")
            let candidate.converter_add_ftime_word_base = candidate.word
        endif
        let word = get(candidate, "converter_add_ftime_word_base", candidate.action__path)
        let candidate.word = s:get_tags(get(candidate, "action__path")) . " " . word
    endfor
    return a:candidates
endfunction

call unite#define_filter(s:filters)
unlet s:filters


call unite#custom#source('memolist', 'converters', ["converter_file_firstline_abbr", "converter_add_ftime_abbr"])
call unite#custom#source('memolist', 'matchers', ["converter_file_firstline_word", "converter_add_memolist_tags_word", "matcher_default"])


と、いうような感じで unite-source を定義せずに unite-filters を使用する事で自由にカスタマイズを行う事が出来ました。
今回は aliase した unite-memolist への設定でしたが、既存の unite-source に対してももちろん有効です。
最近は unite-source を定義する場合は最低限の処理のみ行い、あとは unite-filters で細かく設定してしまう事が多いです。
このように unite-filters 側で処理を行うことで自由にカスタマイズする事もできますし、必要なない情報を切り離すこともできます。
既存の表示がアレだと思ったら自分好みの unite-filters を書いてみるとよいと思います。

[converter を使用する際の注意]

unite-filters の converter は表示形式などを自由に変更する事ができて便利なのですが、候補全てに対して converter の処理が適用されます。
その為、候補が多い unite-source(e.g. unite-file_mru:long 等)では、処理が重くなってしまう可能性があります。
その場合は、次のようにして max_candidates を設定し、表示される候補の数を制御する事で回避する事ができます。

" 候補を 30 個のみ表示する
call unite#custom_max_candidates("memolist", 30)