Vimの折畳に対応したtreeコマンド

golang.tokyo #21DevQuizはGoでtreeコマンドを作成するというものでした。

treeコマンドといえば、以前Vimでファイル一覧をツリー表示するためのtree.vimというプラグインを作る時に使いました。

github.com

このプラグインディレクトリにマーカーをつけることでサブディレクトリを階層ごとに折り畳んで表示します。
当時、マーカーを(Go製の)自作コマンドでつけることも検討しましたが、実装が非常に面倒くさそうだったのでVim scriptでtreeコマンドの結果をパースすることにしました。

そんな事情があったため、せっかくなのでDevQuizをやりつつ、追加機能としてVimの折畳に対応した出力形式も実装してしまおう!と思って作ったのがgo-treeです。*1

こちらはコマンドラインフラグに-Vをつけると以下のような出力になります。

$ go-tree -V github.com/daisuzu/tree.vim
github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
github.com/daisuzu/tree.vim/autoload/{{{
  github.com/daisuzu/tree.vim/autoload/tree.vim}}}
github.com/daisuzu/tree.vim/ftplugin/{{{
  github.com/daisuzu/tree.vim/ftplugin/tree.vim}}}
github.com/daisuzu/tree.vim/plugin/{{{
  github.com/daisuzu/tree.vim/plugin/tree.vim}}}
github.com/daisuzu/tree.vim/syntax/{{{
  github.com/daisuzu/tree.vim/syntax/tree.vim}}}

コマンド自体はVimから実行しても良いし、

:r! go-tree -V [DIR]

シェルで実行した結果をVimに流し込んでもOKです。

# -cでfoldmethod(fdm)をmarkerにする
go-tree -V [DIR] | vim - -R -c 'setl fdm=marker'

# モードラインでfoldmethod(fdm)をmarkerにする
echo -e "vim: fdm=marker\n$(go-tree -V [DIR])" | vim - -R

そうするとgithub.com/daisuzu/tree.vimVimで以下のように表示されます。

github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
+--  2 lines: github.com/daisuzu/tree.vim/autoload/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/ftplugin/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/plugin/-------------------------------
+--  2 lines: github.com/daisuzu/tree.vim/syntax/-------------------------------
~
~
~
~

さらに、コマンド単体で動作するのでjob機能を使った非同期実行も簡単です。

:vnew | setl fdm=marker | call job_start('go-tree -V', {'out_io': 'buffer', 'out_buf': bufnr('%')})

f:id:daisuzu:20190128202818g:plain

このように、github.com配下に大量のファイルがあるような場合でもVimをブロックすることなく操作を継続することができます。

現在はvimrcにAtreeコマンドを定義してgo-treeを常用しています。
filetypeをtree.vimのものにすることで、そのまま表示するよりも少し見栄えが良くなっています。

command! -nargs=? -complete=dir -count -bang -bar ATree
      \ call s:async_tree(<q-args>, <count>, <bang>0, <q-mods>)

function! s:async_tree(dir, depth, bang, mods) abort
  let cmd = 'go-tree -V ' . g:tree_options
  if a:bang
    " コマンドに!をつけたら隠しファイルも表示する
    let cmd .= ' -a'
  endif
  if a:depth > 0
    let cmd .= ' -L ' . a:depth
  endif
  " cd後もgfできるようにディレクトリ名を絶対パスにする
  let cmd .= ' ' . fnamemodify(a:dir != '' ? a:dir : '.', ':p:h')

  execute a:mods . ' new'
  setfiletype tree

  let bufnr = bufnr('%')
  call job_start(cmd, {
        \   'out_io': 'buffer',
        \   'out_buf': bufnr,
        \   'exit_cb': {channel, msg -> s:goto_first(bufnr)},
        \ })
endfunction

" オリジナル版はカーソル位置が先頭行になるので同じ挙動にする
function! s:goto_first(bufnr)
  if bufnr('%') != a:bufnr
    " バッファを移動していたら何もしない
    return
  endif
  if line('.') != line('$')
    " 行を移動していたら何もしない
    return
  endif
  normal gg
endfunction

予想通り実装は面倒でしたが、Vimをより快適に使えるようになりました!

*1:人が見ることはあまり考えてませんでした...