Vim Advent Calendar 2012 の208日目の記事です。昨日の記事は@manga_osyoさんで、明日の記事は@hecomiさんでした。
さて、前々回の著者による記事をお覚えでしょうか。前々回の記事を執筆した日は著者の誕生日です。
- Vital.ProcessManager https://gist.github.com/ujihisa/5761509
さて。このライブラリを用い、とあるプラギンを魔改造し、実用的な水準に引き上げることに成功しました。
(Vimではなく) Ruby Advent Calendar 2012の記念すべき一番目の記事がLinda_pp
さんによるVim で Ruby を書くためのプラグイン3つ書いたでした。いま気づいたのですが、著者もRuby Advent CalendarにVimの記事を投稿していたのを発見しました。また、残念ながら、Ruby Advent Calendarは志半ばにして昨年12月25日に中断してしまったようです。その魂は、Vim業界の我々に受け継がれていくこととなるでしょう。
その記事、Vim で Ruby を書くためのプラグイン3つ書いたでは、unite-ruby-require
というVimプラギンが紹介されています。
Ruby で外部のソースを読み込むのに使う
require
ですが,標準だけでも大量のライブラリがあり,なかなか全部覚えるのは大変です. そこで,unite.vim
でrequire
の候補をインクリメンタルに検索できるプラグイン unite-ruby-require.vim を作りました.:Unite ruby/require
で起動すると,Ruby の標準のライブラリやインストールした
gem
,bundler
でプロジェクトローカルにインストールしているライブラリのパスを収集し,require
の候補を一覧表示します. あとはunite.vim
のインクリメンタルサーチで目的のライブラリを探すだけです.
とても便利そうです。が、記事には書かれていない実用上の重大な問題点がありました。
- 遅い (インストールしてるgemの数が多ければ10秒以上のときも)
- Vimをブロックする (!)
この2つの組み合わせは絶大です。時間がかかるというのは、広義のバグです。
以下の3つのpullreqを行いました。 (あとのふたつはおまけみたいなものです)
- "魔改造しました" rhysd/unite-ruby-require.vim#2
- rhysd/unite-ruby-require.vim#3
- rhysd/unite-ruby-require.vim#4
まず、同期処理としてのライブラリパスの探索の単純な高速化を行い、その後ProcessManagerを用いた非同期処理によりVimをブロックしないようにし、最後にキャッシュの作成と再利用により二回目以降の実行を神速にしました。 これらの変更点について実装上の細かい解説については Linda_pp さんがVim Advent Calendar 2012にて記事を投稿する予定です。
一番のキモである関数、unite用のasync_gather_candidates()
をみてみましょう。
autoload/unite/sources/ruby_require.vim
43 function! s:source.async_gather_candidates(args, context)
44 let cmd = printf('%s %s', g:unite_source_ruby_require_cmd, s:helper_path)
45 call s:P.touch('unite-ruby-require', cmd)
46 let [out, err, type] = s:P.read('unite-ruby-require', ['$'])
47 call unite#util#print_error(err)
48 if type ==# 'timedout'
49 let formatted = s:_format(out)
50 let s:ramcache += formatted
51 return formatted
52 elseif type ==# 'inactive'
53 call s:P.stop('unite-ruby-require')
54 return s:source.async_gather_candidates(a:args, a:context)
55 else " matched
56 let a:context.is_async = 0
57 call s:P.stop('unite-ruby-require')
58 let formatted = s:_format(out)
59 let s:ramcache += formatted
60 call s:_spit_cache(s:ramcache)
61 return formatted
62 endif
63 endfunction
- まず
ProcessManager.touch()
で外部コマンドの起動を行います。すでに起動している場合の副作用はありません。 - 続いていきなり
ProcessManager.read()
その出力を読み込んでいます。^$
にマッチするまで、つまり出力の最後の最後まで読み込みます。 - 環境によってはこれは即座に完了します。そのときは
matched
を返すので、else
句に移動します。 - 大抵の場合は即座(0.05秒以内の遅延以内)に処理が完了することはないでしょう。
timeout
の句に移動し、とりあえずその時点までで取得できたものをunite
のcandidateとして返しています。 a:context.is_async
が1になるまでこの関数async_gather_candidates()
は何度も呼ばれます。そのたびにtouch
してread
します。
ProcessManagerのおかげでリエントラントなコードの記述が非常に簡単になっていることに気づかれたでしょうか。明示的にスクリプトローカルな関数外部の変数に状態を保持する必要がないのです。