Skip to content

Instantly share code, notes, and snippets.

@ujihisa
Created June 28, 2013 03:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ujihisa/5882225 to your computer and use it in GitHub Desktop.
Save ujihisa/5882225 to your computer and use it in GitHub Desktop.
Vim Advent Calendar 2012 http://atnd.org/events/33746

Vim Advent Calendar 2012 ujihisa 13 (208日目)

Vim Advent Calendar 2012 の208日目の記事です。昨日の記事は@manga_osyoさんで、明日の記事は@hecomiさんでした。

さて、前々回の著者による記事をお覚えでしょうか。前々回の記事を執筆した日は著者の誕生日です。

さて。このライブラリを用い、とあるプラギンを魔改造し、実用的な水準に引き上げることに成功しました。

unite-ruby-require

(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.vimrequire の候補をインクリメンタルに検索できるプラグイン unite-ruby-require.vim を作りました.

:Unite ruby/require

で起動すると,Ruby の標準のライブラリやインストールした gembundler でプロジェクトローカルにインストールしているライブラリのパスを収集し,require の候補を一覧表示します. あとは unite.vim のインクリメンタルサーチで目的のライブラリを探すだけです.

とても便利そうです。が、記事には書かれていない実用上の重大な問題点がありました。

  • 遅い (インストールしてるgemの数が多ければ10秒以上のときも)
  • Vimをブロックする (!)

この2つの組み合わせは絶大です。時間がかかるというのは、広義のバグです。

魔改造

以下の3つのpullreqを行いました。 (あとのふたつはおまけみたいなものです)

まず、同期処理としてのライブラリパスの探索の単純な高速化を行い、その後ProcessManagerを用いた非同期処理によりVimをブロックしないようにし、最後にキャッシュの作成と再利用により二回目以降の実行を神速にしました。 これらの変更点について実装上の細かい解説については Linda_pp さんがVim Advent Calendar 2012にて記事を投稿する予定です。

unite-ruby-requireでのProcessManagerの用い方

一番のキモである関数、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のおかげでリエントラントなコードの記述が非常に簡単になっていることに気づかれたでしょうか。明示的にスクリプトローカルな関数外部の変数に状態を保持する必要がないのです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment