😁

ddu.vimのアクション周りを便利にしよう

2023/10/04に公開

まえがき

最近使い始めたdduとかいうPluginが神になっていたので、
個人的に便利な使い方を紹介しようかなと思います。

この記事を読むと嬉しくなる人

  • 暇な時間にvimの設定をちょこちょこと進めてる人
  • dduの簡単な設定を終わらせて、とりあえず起動できるようになった人
  • dduのcustom action設定してみたいものの、億劫な人

ddu.vimって何?

広大な†インターネット†の海からこの記事を見かけた方々なので、説明不要だと思います。

https://zenn.dev/shougo/articles/ddu-vim-beta

↑こちらにもある通り、一定の規則の上で、vimのUIまわりを良い感じに統合しちゃえ~ってプラグインです(少なくとも自分はそう思ってます)。

色んな事を一つに統合し、扱いやすくなるという利点はありますが、
反面、その設定量が多くなってしまうというのが欠点ですかね。

欠点を欠点と捉えない粋狂な方々には、ばちこりとはまるプラグインでもありそうです。

設定の大まかな概要は以下の記事を見るとそれっぽく理解できるようになると思います。
おすすめです!

https://zenn.dev/vim_jp/articles/c0d75d1f3c7f33

ま、実の所この記事は上記の記事の

  • Sourceで様々なユースケースを吸収しようとしない
    • ...
    • アクションが気に食わない→Custom Actionや、Action Overwriting(:help ddu-source-option-actions)を設定すれば良い
  • Kindに完全性を求めない
    • Custom Actionを使う
    • ...

の部分を試しに設定してみたので、その紹介をするぜ!みたいな感じですね。

もう面倒なキーマップ設定はいらない!使おうchooseAction

dduの設定を初めた時に面倒だと考えていたのがキーマップの設定ですね。
UI用に設定したり、各々のsourceに対して設定したり...とそれはもう大変でした。

nnoremap <buffer><silent> <CR>
            \ <Cmd>call ddu#ui#ff#do_action('itemAction')<CR>
nnoremap <buffer><silent> t
            \ <Cmd>call ddu#ui#ff#do_action('itemAction', {'params': {'command': 'tabnew'}})<CR>
nnoremap <buffer><silent> o
            \ <Cmd>call ddu#ui#ff#do_action('itemAction', {'params': {'command': 'split'}})<CR>
nnoremap <buffer><silent> v
            \ <Cmd>call ddu#ui#ff#do_action('itemAction', {'params': {'command': 'vsplit'}})<CR>
" ...以下大量のitemActionのキーマップを設定しようとしていた

dduの設定を終えた方々にはもうお馴染みかもしれませんが、itemAction周りの設定をしている所ですね。
このitemActionというのもなかなか鬼門で、dduが様々なsourceを複数表示できることもあり、
細かく設定することが非常に難しくなっております。
上記だと、それぞれのsourceに対してdefaultActionを設定し、Enter(<CR>)を押すことで、各々のsourceに応じた簡易的な設定を可能にしています。
ただ、この手法だと対応する一つしか設定できません。愚直に細かく設定しようとすると、

inoremap <buffer><expr> <CR>
			\ ddu#ui#get_item()->get('__sourceName') == 'file' ?
			\	"<ESC><Cmd>call ddu#ui#do_action('itemAction', { 'name': 'open' })<CR>" :
			\	ddu#ui#get_item()->get('__sourceName') == 'window' ?
			\		"<ESC><Cmd>call ddu#ui#do_action('itemAction', { 'name': 'close' })<CR>" :
			\		"<ESC><Cmd>call ddu#ui#do_action('itemAction')<CR>" 

こんな感じでddu#ui#get_item()を用いてカーソル下のitemを取得して、そこから条件分岐させて~といった事をするはめになります。
これだと導入するsourceが増えるにつれ、より面倒になっちゃいますね。

そんなまどろっこしさから開放されるには、chooseAction(:h ddu-ui-ff-action-chooseAction)を使いましょう。

詳細な設定方法は後々のversion upで変ってしまうかもしれないので、詳しくは書かないでおきます。
面倒くさいわけじゃあないですよ、そういう事にしておきましょう。

簡単に説明をしますと、
https://github.com/Shougo/ddu-source-action
こちらのsourceを普通のdduソースのように導入し、

nnoremap <buffer><silent> a
            \ <Cmd>call ddu#ui#ff#do_action('chooseAction')<CR>

のようにuiからchooseActionを呼び出すだけでOKです。

動作例としてはこんな感じです。
itemActionとして実行できるアクションがdduに表示され、絞り込みができるようになっています。

ここで表示されるactionの一覧は、この後で設定するcustom actionも表示されるので非常に便利です。
custom actionを設定しようと思う方々は先にchooseActionを設定しておいたほうが良さげですね。

既存アクションの挙動が気にくわねぇ...そんなあなたはactionを†custom†しよう!

source・kind等もろもろの設定をひとまず終わらせて、使い続けていると気付くと思います。
そう、「なんか細かい挙動が自分に合ってない...」と...
ただ、デフォルトの挙動を自分用に変更するためのPRを書くのも違うしなぁ...と。

自分の場合はデフォルトのquickfix action(:h ddu-kind-file-action-quickfix)がそれでした。
quickfixに対象を登録するのはよいけれども、quickfix windowは開きたくないぞい、といった感じですね。

dduのおさらい

さて、これからcustom actionを設定していくのですが、既存のactionに変更を加えたり、追加したりするわけなので、ある程度はdduの挙動を知っておかなければなりません。
dduを設定して、使えるようになるだけで、dduの概要は分かるのですが、ちょっと細かい所まで知っておこうといった感じですね。

まずはdduを起動してからの流れを確認していきましょう。


上図のように、

  1. 対応するsource達がそれぞれのitemを作成する
  2. item達を必要に応じて処理する
    • 絞りこむ(matcher)
    • 並びかえる(sorter)
    • 加工する(converter)(色を変えたり、アイコンつけたりとか)
  3. item達がUIを通してユーザーに表示される
  4. ユーザーがアクションを発火させる

といった感じでしょうか。今回注目するのはユーザーがアクションを発火させる所らへんですね。

...ん?dduの紹介文で紹介されているやつが無いような希ガス...と思ったそこのあなた!
正解です。dduのsource,filter,uiと説明しておきながら、kindの説明をまだしておりませんでした。

と言ってもまぁ、kindはそんなに難しくなく、それぞれのitemに対応するなんらかの処理を決めたものです。
itemがファイルに関係するんだったら、ファイルを開く・削除したり、はたまたファイルをプレビューしてみたり、といった所でしょうか。
今回注目してるところそのものでもあります。

さて、kindが処理するために受け取るitemについて見ていきましょう。

各sourceやkindによって入っている情報は異なってるのですが、イメージとしては上図のようなもんだと思います。
itemを表示するために必要なもの(wordやhighlight等)と、なんらかのアクションに必要なもの(pathやバッファ番号等)が一つのitemに詰めこまれているという感じです。
各kindがこれらの情報を元に色々処理しているため、actionをcustomするためにこれら概要を知っておく必要があったということですね。

ddu#custom#action()ってどう使うねん:awoo:

さて、dduの概要がそれとなく掴めた所で早速custom actionについて見ていきましょう。
:help ddu#custom#action()で表示される現在の説明は以下のように書かれていました。

ま、ようするにactionを追加するために、それ用の関数を登録してねって感じですかね。
対応する関数としては、グローバルに定義した関数の名前or関数への参照?っぽいですね。

function Hoge(args) " ← 関数名の先頭を大文字でグローバルにする
	echomsg "Hogeだよ~"
	return 0
endfunction

" 関数名で登録ver
call ddu#custom#action('ui', 'ff', 'hoge_action', 'Hoge')

function s:piyo(args) " ← s: をつけてスクリプトローカルにする
	echomsg "piyoだよ~"
	return 0
endfunction

" 関数への参照で登録ver
call ddu#custom#action('ui', 'ff', 'piyo_action', function('s:piyo'))

" 例の通り
call ddu#custom#action('ui', 'ff', 'poyo_action', { args -> execute('let g:piyo = 1') })

それぞれの関数名としては上記のように登録するのが良さそうなのかなと思っています。
他にも良さそうな登録方法もありそうな気がします。気になった方々は:h Funcrefから調べると、より良い方法が見つかるかも。
あとそうですね、:h ddu-action-flagsを確認して対応する数を返り値として記述しておきましょう。

ddu-ui-option-actionsを上書き

考える事が比較的少ないuiから紹介していきます。
uiのactionをcustomするってのは、簡単に言うと以下のように設定しているitemActiontoggleSelectItemの挙動を変更したり、はたまた新に追加するイメージです。そのため、chooseActionでは表示されないのでキーマップを登録する必要があります。

" ...
nnoremap <buffer><silent> v
            \ <Cmd>call ddu#ui#ff#do_action('itemAction', {'params': {'command': 'vsplit'}})<CR>
nnoremap <buffer><silent> a
            \ <Cmd>call ddu#ui#ff#do_action('chooseAction')<CR>
nnoremap <buffer><silent> s
            \ <Cmd>call ddu#ui#ff#do_action('toggleSelectItem')<CR>
" ...

さっそく設定例を見ていきましょう。

function s:yank_list(args)
    " vimscriptだと引数にアクセスするためにa:をつける
    " 辞書にアクセスするのに->getが使いやすそう
    let maxItems = a:args->get('context')->get('maxItems')
    call setreg("*", maxItems)
    return 4
endfunction

call ddu#custom#action('ui', 'ff', 'yank_list', function('s:yank_list'))

" ここはddu-ffの時のみ実行されるようにしておくと便利かも
nnoremap <buffer><silent> y
            \ <Cmd>call ddu#ui#ff#do_action('yank_list')<CR>

試しにこんな感じに設定してみました。UIに表示されているitemの最大数を*レジスタにyank(コピー)する新なactionを追加してます。*レジスタなので、OSのクリップボードに貼りつけられます。
設定するコツとしては、まず登録する関数の先頭で試しにechomsg a:args等を実行して、受け取る引数に何が入っているかを確かめてみると良い所ですかね。この引数は今後のversion upで変更されるかもしれないので、ひとまず確認しておきましょう。
後キーマップを設定する時はddu#custom#actionの第三引数の名前を登録する必要があるのも重要です。

ddu-source-option-actionsddu-kind-option-actionsを上書き

設定の仕方はuiの時とほぼ同じなので、簡単な設定例だけを紹介しようかなと思います。
まずはsourceですね。

function s:deleteBuffer(args)
    let items = a:args->get('items')
    for item in items
        let action = item->get('action')
        let bufNr = action->get('bufNr')
        execute 'bdelete ' . bufNr
    endfor
    return 0
endfunction

call ddu#custom#action('source', 'buffer', 'bdelete', function('s:deleteBuffer'))

これはbufferソースに対して:bdeleteを実行するようなアクションを設定例しています。
次にkindに設定する方法です。

function s:isDirectory(args)
    let items = a:args->get('items')
    for item in items
        let action = item->get('action')
        echomsg action->get('isDirectory')
    endfor
    return 0
endfunction

call ddu#custom#action('kind', 'file', 'isDirectory', function('s:isDirectory'))

これは選択してるitemがディレクトリか否かを表示するようにするアクションです。
source・kindの設定のどちらにも言えるのですが、

  • 先程の説明よろしくそれぞれのitemにはactionと呼ばれるkind用のデータがあり、そちらが便利に使える
  • 複数選択に対応できるようにitemをfor文で回しておくとよさそう

これらの事を気に留めておくと良いのかなと思います。

ちなみにディレクトリ判定の動作イメージとしてはこんな感じですね。

sourceとkindはどっちに設定すればええんやねん問題

さて、ここまで設定できた所で、ふと感じませんでしたか?
あれ?sourceとkindどっちに設定例すればええんや?...と、僕は感じました。

実はsourceとkindはおおまかに下図のような関係にあります。それぞれの役割を適切に表現する図が思いつかなかったため、少々分かりにくくなっております。

  • sourceはkindで処理を行う用のデータを含めたitemを生成
  • sourceは生成したデータを処理する用のkindを埋め込みの形で登録している
  • 複数のsourceが同じkindを使用する事もできる
  • kindはsourceを指定できない

そのためsourceとkindが分離されてる事で、それぞれの開発者が自由に選択できるといった利点がありそうですね。
設定をするユーザー目線からだと、

  • 各々のsourceが採用しているkind全てに対応するアクションを登録したい
    • kindへcustom actionを登録する
  • より限定的な範囲のsourceのみに対応させたい
    • sourceへcustom actionを登録する

といったように設定するのが良いのかなと思います。

custom actionの設定例

さて、custom actionの設定方法も†完全に理解†ところで、他の人の設定も含めたいくつかの例を紹介します。
手元で再現できそうなものはgif付きで紹介しています。

簡易的な:cdoを実現

https://github.com/kamecha/dotfiles/blob/8c814beb870fee31bb207cfa6b00b1ee0a748c54/nvim/ddu.toml#L334-L352

  1. lineソースを起動
  2. 選択したものをquickfixへつっこむ
  3. 各行に対して{cmd}を実行するために、input()を用いてユーザーからの入力を待つ

これらの処理を一度に行えるようにしています。
例だと、normal gccというコマンドをquickfixに入っている各行に対して処理するようになっています。ノーマルモードでgccと入力すると言語に応じたコメントアウトをするプラグインを入れているため、それを各行に対してやってね、という意味です。

元々:globalという「v..vimの行思考たる所以...」のようなコマンドがあるのですが、それは出現箇所を全部確認する事ができないのですよね。そのため、:g/debug/normal gccのようにしても、コメントアウトしたくないdebugの箇所もコメントアウトされてしまいます。

dduを用いてプレビューでおおかたの位置を確認しつつ、リストアップ、それぞれに対して{cmd}の実行がやりやすくなり、とても満足しています。

ただ、この手法も完璧ではなくて、quickfixの機能を利用しているためdeleteコマンド等の行番号が変更されるコマンドを実行しちゃうと壊れちゃいます。いつか:globalコマンドと同等の事をこの手法でエミュレートしたいなぁ...

gitのstatusソースからdiffソースへの変更

https://github.com/kuuote/dotvim/blob/f1fa0adf2390fa1ff8b58dbf941d82a6afa1efcd/conf/plug/ddu/ddu.ts#L60-L77
kuuoteさんからcustom actionの一例を頂きましたので、ここから紹介いたします。kuuoteさんありがとうございます。
dduの設定をTypescriptで書ける事を上手く利用してcustom actionを埋め込みの形で設定していますね。
おそらくgitのstatusソースを使用している際にdiffを簡単に見られるようにするアクションを追加しているのだと思われます。
手元で簡単に再現させられそうになかったので、考察だけでここはどうかひとつ...

gitのbranchソースからlogソースへの変更

https://github.com/kuuote/dotvim/blob/f1fa0adf2390fa1ff8b58dbf941d82a6afa1efcd/conf/plug/ddu/ddu.ts#L198-L217

こちらはgitのbranchを表示するソースからlogを表示するソースへと動的に変更するアクションを追加しているのだと思います。

fileソースからfile_recソースへの変更

https://github.com/kuuote/dotvim/blob/f1fa0adf2390fa1ff8b58dbf941d82a6afa1efcd/conf/plug/ddu/ddu.ts#L178-L190

こちらはfileソースからfile_recソースへの変更をするアクションを追加しているのだと思います。fileでツリー表記で探しつつ、気になった箇所以下を再帰的に探したいといった時に便利そうですね。

例えば以下のようなツリー構造を持つディレクトリ下で、

該当アクションを発火すると下図のような挙動をします。

自分なりにvimscriptへ変換したverがこちら

https://github.com/kamecha/dotfiles/blob/1c4678c41c72383015e6a6ca11cc437cf172f92b/nvim/ddu.toml#L354-L363

file周りのdduソースについての蛇足

dduの設定を始めた頃、fileとfile_recの2つの同じようなソースがあり困惑しておりました。
ddu系統は設定が多い事もあって、初心者の頃は本当に何も分かっておりませんでした。
最近になりFuzzyFinder系プラグインを使い始めたのもあって逆に運が良かったなぁ、などと思っております。始めにAll in one系のFuzzyFinderを使い始めてると、その便利さゆえにdduを使い始める事もなかったんだろうなぁと...

さて、あれからちょこちょこ設定してるうちにdduのfileソース周りはこんな感じの関係なのかな?と思うようになりました。

  • file
    • path配下のディレクトリ・ファイルを一階層文だけ全部取得
    • dduのtree表示に対応
  • file_rec
    • path配下のディレクトリ・ファイルを再帰的に全部取得
  • file_external
    • 外部コマンドを用いたitem取得
    • git ls-filesを用いて.gitignoreを反映させたファイル郡を取得可能
  • file_old
    • vimの組み込み機能のv:oldfilesを参考に取得

filterの動的変更

https://github.com/kuuote/dotvim/blob/f1fa0adf2390fa1ff8b58dbf941d82a6afa1efcd/conf/plug/ddu/ff.ts#L186-L208

こちらはfilterを動的に切り替えていますね。
kensakuというのは日本語検索をローマ字でやりやすくするプラグインで、そちらを使用したfilterに切り替えるようにしているのだと思います。
https://github.com/lambdalisue/kensaku.vim

こちらを参考に自身で設定してみたgifです↓

filterを日本語検索用のfilterに変更する事でローマ字で日本語検索できているのが分かりますね。

自分用にvimscriptで記述したverはこんな感じになりました。†vimscript力†が無いのでもっと良い書き方がありそうですが、面倒なのでひとまずこれでヨシとします。

https://github.com/kamecha/dotfiles/blob/b1e4c4f519c930c9d618c0016223f3174d8cf18a/nvim/ddu.toml#L390-L409

あとがき

ふぅ、とりあえずですが、これでdduのアクションまわりを紹介できたのかなぁと思います。
ただ今回紹介したcustom actionというdduの機能の一部分なので、他にも色々と便利機能があろう事かと思います。ヘルプを眺めて新な使い方を模索してみるのも良いかもしれません。
他のFuzzyFinderプラグインを試してみて、便利な機能をdduの設定として動かしてみるのも良さそう。

また、そのdduの設定自体、vimscript・lua・typescriptと複数の設定方法があります。
現在vimscriptでしか設定をしていないので、

  • pluginマネージャをdein→lazyへ移行し、luaで書くようにする
  • 新しいpluginマネージャdppが完成するとlazy→dppへ移行し、typescriptで書く

というようなムーヴをかましたいなぁとか個人的に考えています。

あ、そうそう今更ですが、自分の設定はdotfilesとして公開しているので、参考になるやもしれません。更新頻度はよくないですが...
https://github.com/kamecha/dotfiles

GitHubで編集を提案

Discussion