unite-source を作成する流れをまとめてみた

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


さて、前回の Vim Advent Calendar で iTunes のプレイリストを操作する euphoric_player.vim を公開したんですがその時に作成した unite-source の書き方なんかをまとめてみたいと思います。

[欲しい機能]

  • トラック一覧の表示
  • 選択したトラックの再生
  • 並び順の変更
  • 表示形式の変更


今回は euphoric_player.vim を利用して上記の機能を実装した unite-source のコードを載せっていってみたいと思います。

[トラック一覧の表示]

unite.vim でトラック一覧の表示を行います。
この機能は unite-source を使用して定義します。

" unite-source の設定を定義する
"
" 詳しい設定オプションは
" :help unite-source-attributes
let s:source = {
\   "name" : "tracks",
\   "description" : "example unite-source",
\}


" unite.vim で表示される候補を返す
function! s:source.gather_candidates(args, context)
    " euphoric_player#playlist() の戻り値については
    " :help euphoric_player#playlist()
    let playlist = euphoric_player#playlist()
    if !has_key(playlist, "tracks")
        return []
    endif

    " トラック名だけ表示
    " word に設定した文字列が表示される
    return map(copy(playlist.tracks), '{
\       "word"  : v:val.name,
\   }')
endfunction


" untie.vim に source を登録
call unite#define_source(s:source)
unlet s:source

:Unite tracks


これで unite.vim を起動させるとカレントのプレイリストのトラック名一覧が表示されます。

[unite-source で引数を受け取る]

unite.vim では、

:Unite hoge:arg1:arg2


のようにして unite-source に引数を渡すことが出来ます。
これで unite-tracks にプレイリスト名を渡せるようにしてみたいと思います。

let s:source = {
\   "name" : "tracks",
\   "description" : "example unite-source",
\}


function! s:source.gather_candidates(args, context)
    " unite-source の引数は a:args にリストで保存されている
    " echo a:args
    " => ['arg1', 'arg2']

    " unite-source の引数をそのまま euphoric_player#playlist に渡す
    let playlist = call("euphoric_player#playlist", a:args)
    if !has_key(playlist, "tracks")
        return []
    endif

    return map(copy(playlist.tracks), '{
\       "word"  : v:val.name,
\   }')
endfunction


call unite#define_source(s:source)
unlet s:source

:Unite tracks:{プレイリスト名}


こんな感じで unite.vim の起動時にプレイリストを指定する事が出来ました。

[選択したトラックを再生する]

unite.vim で選択した候補に対して何かを行う場合、unite-action を定義します。
unite-action の定義方法はいくつかあるんですが、今回は unite-source に追加する形で定義してみたいと思います。

" action を定義する action_table を追加する
" :help unite-kind-attribute-action_table

" default_action には定義する action を設定
let s:source = {
\   "name" : "tracks",
\   "description" : "example unite-source",
\   "action_table" : {
\       "play_track" : {
\           "description" : "play",
\       }
\   },
\   "default_action" : "play_track",
\}


" action が呼ばれた時の処理を定義
function! s:source.action_table.play_track.func(candidate)
    " candidate には gather_candidates で設定した値が保持されている
    call euphoric_player#play_track(a:candidate.action__track_name, a:candidate.action__playlist_name)
endfunction


function! s:source.gather_candidates(args, context)
    let playlist = call("euphoric_player#playlist", a:args)
    if !has_key(playlist, "tracks")
        return []
    endif

    " action で必要な値を設定する
    " action で参照される変数名にはプレフィックスとして
    " action__
    " を使用する
    return map(copy(playlist.tracks), '{
\       "word"  : v:val.name,
\       "action__track_name"    : v:val.name,
\       "action__playlist_name" : playlist.name,
\   }')
endfunction


call unite#define_source(s:source)
unlet s:source


これで候補を選択した場合に s:source.action_table.play_track.func が呼ばれます。
このように action で必要は値は gather_candidates で定義して返します。

[並び順を変更する]

unite.vim で表示される候補の並び順を変更する場合は unite-filter のsorter を使用します。
この機能は unite-source に対して予め定義しておいた sorter で並び順を設定することが出来ます。

" unite-filter を定義
" この機能で並び順を変更する事が出来る
let s:filter = {
\   "name" : "sorter_track_name",
\}

function! s:filter.filter(candidates, context)
    " v:val.source__track.name で sort する
    return unite#util#sort_by(a:candidates, 'v:val.source__track.name')
endfunction

" unite.vim に filter を登録
call unite#define_filter(s:filter)
unlet s:filter


" 別の sorter も定義してみたり
let s:filter = {
\   "name" : "sorter_track_artist",
\}

function! s:filter.filter(candidates, context)
    " v:val.source__track.artist で sort
    return unite#util#sort_by(a:candidates, 'v:val.source__track.artist')
endfunction

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


let s:source = {
\   "name" : "tracks",
\   "description" : "example unite-source",
\   "action_table" : {
\       "play_track" : {
\           "description" : "play",
\       }
\   },
\   "default_action" : "play_track",
\}


function! s:source.action_table.play_track.func(candidate)
    call euphoric_player#play_track(a:candidate.action__track_name, a:candidate.action__playlist_name)
endfunction


function! s:source.gather_candidates(args, context)
    let playlist = call("euphoric_player#playlist", a:args)
    if !has_key(playlist, "tracks")
        return []
    endif

    " unite-filter で参照されるトラック情報も返す
    return map(copy(playlist.tracks), '{
\       "word"  : v:val.name,
\       "action__track_name"    : v:val.name,
\       "action__playlist_name" : playlist.name,
\       "source__track" : v:val,
\   }')
endfunction

call unite#define_source(s:source)
unlet s:source



" unite-tracks でどの sorter を使用するのかを設定する
call unite#custom_source('tracks', 'sorters', 'sorter_track_name')

" 逆順にする場合
" call unite#custom_source('tracks', 'sorters', ['sorter_track_name', 'sorter_reverse'])

:Unite tracks


こんな感じで unite-source に対してどのように sort を行うのかを設定する事が出来ます。
この機能を利用すれば unite-file などといった既存の unite-source も、ユーザ側で自由に sort を行うことが出来ます。

[表示形式を変更する]

さて、今は曲名しか表示していないのでちょっと寂しいですね。
表示するテキストは unite-source の "word" を変更すればいいのですが、今回は unite-filter の converter を使って表示されるテキストを変更してみたいと思います。
この機能を使用すれば unite-source 側のコードを変更することなく、表示されるテキストを変更する事が出来ます。

" unite-filter を定義
let s:filter = {
\   "name" : "converter_my_track",
\}


function! s:filter.filter(candidates, context)
    " 候補に対して操作する
    " 今回は表示されるテキストを変更したいので word に対して設定する
    let format = "%-30S - %-12S - %S"
    for candidate in a:candidates
        let track = candidate.source__track
        let candidate.word = printf(format, track.name, track.artist, track.album)
    endfor
    return a:candidates
endfunction

" unite.vim に filter を登録
call unite#define_filter(s:filter)
unlet s:filter


let s:source = {
\   "name" : "tracks",
\   "description" : "example unite-source",
\   "action_table" : {
\       "play_track" : {
\           "description" : "play",
\       }
\   },
\   "default_action" : "play_track",
\}


function! s:source.action_table.play_track.func(candidate)
    call euphoric_player#play_track(a:candidate.action__track_name, a:candidate.action__playlist_name)
endfunction


function! s:source.gather_candidates(args, context)
    let playlist = call("euphoric_player#playlist", a:args)
    if !has_key(playlist, "tracks")
        return []
    endif

    " unite-filter で参照されるトラック情報も返す
    return map(copy(playlist.tracks), '{
\       "word"  : v:val.name,
\       "action__track_name"    : v:val.name,
\       "action__playlist_name" : playlist.name,
\       "source__track" : v:val,
\   }')
endfunction

call unite#define_source(s:source)
unlet s:source


" unite-tracks でどの converter を使用するのかを設定する
call unite#custom_source('tracks', 'converters', 'converter_my_track')

:Unite tracks


こんな感じで unite-source を変更する事なく、表示テキストを自分で設定する事が出来ます。
これもやはり既存の unite-source の表示テキストをユーザ側で自由に変更する事が出来るので便利ですね。


と、いう感じでちょっとコードの量が多いのですが unite-source の作り方なんかを書いてみました。
unite-filter は最近使い始めた機能なんですがだいぶ強力ですね。
既存の unite-source に対してより使い勝手のいいものにしたいのであればぜひ unite-filter を使って見ることをおすすめします。