uptodate.vimでもっとお手軽にオレオレライブラリ

この記事は Vim Advent Calendar 2012 223日目の記事になります。
222日目はもぷりさんのもぷろぐ: あなたの Vim は もっと Smart に Input できるでした。
224日目は@BOXPさんのVimでClojureする時のあれこれ - はこのLINUXです。

Vim scriptを作り続けていると再利用したい部分、共通化したい部分が出てきます。
本日は自分専用のライブラリを作るのに役立つ新たなプラグインを紹介します。

ライブラリとして単独のプラグインでリリースする手もありますが、そうするとプラグイン間で依存関係が出来、作者、利用者とも管理が面倒くさくなります。
そこで個々のプラグインにライブラリを組み込むことが出来る、vital.vimたるものが開発されました。
ただしこれは、使いこなすのが難しい、組み込む時のコマンド:Vitalizeが面倒、モジュール利用前にvital#of('foo')などしないといけない(手軽に呼び出せない)といった欠点があります。

もっと手軽に簡易にライブラリを用意したい。そこで私は uptodate.vim たるものを作りました。*1

サマリー

  • 複数の同名のautoloadスクリプトの中から、常に最新版を利用するためのプラグイン。
  • スクリプトが読み込まれる際、 runtime! で全ての同名スクリプトが読み込まれる。
  • その中で一番最新のものが最後まで読み込まれ、そうでないものはファイル頭で読込が終了される。
  • これによってスクリプトは(たとえ同名のスクリプトが複数あったとしても)最新のものが読み込まれていることを保証できる。

すなわち複数のバージョンが混在していても常に最新版を読み込ませられるというわけです。

利用方法

uptodate.vimで管理できるのはautoload/以下のスクリプトのみです。
g:uptodate_filenamepatterns変数(リスト)を定義します。
これに管理したいオートロードスクリプトの、autoload以下のパスを入れたものを定義してください。autoload/lclib.vimというスクリプトを管理下に入れたいのであれば、以下のようにします。

let g:uptodate_filepatterns = ['lclib.vim']

次にそのスクリプト冒頭に以下の記述をします。

"UPTODATE: .
if lib#uptodate#isnot_uptodate(expand('<sfile>:p'))
  finish
endif

UPTODATE: .は更新ヘッダです。後に更新時刻を表す数字が書き込まれます。ここを見てファイルのバージョンを知ります。更新ヘッダはファイル先頭から35行以内*2に記述してください。
lib#uptodate#isnot_uptodate(expand(':p'))は現在読み込まれたファイルと同じパターンのファイルをruntime!して、もし今読み込んでいるファイルが最新版でないと判定したら1を返します。こうして最新版でないと判定されたらfinishされます。

そしてそのプラグインのautoload/lib/以下にautoload/lib/uptodate.vimのファイルをコピーすれば準備完了。
一度Vimを再起動するか、:UptodateResetting コマンドで plugin/uptodate.vimを再読み込みさせてください。

再び先ほどのライブラリ・スクリプトファイルに戻り、:write してみてください。
以下のように、更新ヘッダが更新時刻を表す数値に書き換われば設定成功です。

"UPTODATE: 1373476011.

ファイルを更新する度に、更新ヘッダの数字が更新されるのが確認できます。*3
そして、更新されるときに、'runtimepath'にある、他のlclib.vimも同時に現在の内容で更新されるようになります。これによって、自分の'runtimepath'内のlclib.vimは常に同じ内容であることが保証されます。

後はこのファイルのこれより下の部分にモジュールを定義します。

"UPTODATE: 1373476011.
if lib#uptodate#isnot_uptodate(expand('<sfile>:p'))
  finish
endif
" ここより以下にライブラリとして各モジュールを定義する
function! lclib#foo()
  " 何か処理
endfunction

動作

たとえばbar/autoload/以下と、baz/autoload/以下にlclib.vimをコピーした状態で、lclib#foo()を呼び出したとしましょう。
初めにどちらのスクリプトが呼ばれてもruntime! により結局両方のファイルが呼ばれることになります。
bar/autoload/lclib.vimの方が先に呼ばれたとします。
uptodateは UPTODATE: .ヘッダからbar/autoload/lclib.vimの更新時刻を保持します。
次にbaz/autoload/lclib.vimが読み込まれた時、baz/autoload/lclib.vimの更新時刻と保持されていたbarの更新時刻を比較し、barの方が新しかったり同じだったりするとlib#uptodate#isnot_uptodate(expand(':p'))は1を返すので、読込が終了します。そうでなければbaz/autoload/lclib.vimは最後まで読み込まれ、bar側のlib#uptodate#isnot_uptodate(expand(':p'))が1を返してbar/autoload/lclib.vimは読込が終了します。

最新版として読み込まれたファイルの情報は g:uptodate_loaded 変数に記述されます。確認する時はこれを参照してください。

注意

  • pathogenやvundleやneobundle.vimなどの、'runtimepath' をプラグインごとに通すプラグイン管理を前提にしています。昔ながらの、ダウンロードしたプラグインをvimfiles/以下に直接置く前時代的な管理には対応していませんので悪しがらず。
  • ライブラリ・スクリプトファイルを初めて呼び出した(読み込んだ)ときに全てが完結するようになっています。それ以後に新しく追加したファイルは感知しません。そういうときには:UptodateReload コマンドでスクリプトファイルの再読込を行ってください。このコマンドは引数で与えられたファイルパターンを再読込します。(引数が与えられていない時にはg:uptodate_filepatternsにある全てのパターンが読み込まれます。)
  • 常に最新版が読み込まれるので、古いバージョンでしか動かないようなプラグインをサポートしません。後方互換性を意識する必要があります。
  • 設計上、同名のファイルが増えれば(ファイル冒頭しか読まないとはいえ)読み込み時に時間がかかるでしょう。速度が気になりだしたら何らかの対策を講じるべきだと思います。

常に最新バージョンしか使えないという欠点はありますが、普段使っているautoload関数とまったく同じ感覚でモジュールを使えるのは便利です。
もともとはテストの中で使う汎用的な関数を呼ぶのに:Vitalizeなんかしてられるか!といった動機で作ったのですが、うまくいったようです。
そしてコマンドラインからも手軽に使えるようになったのも大きいです。今編集中のスクリプトのスクリプトIDを調べたいと思ったとき、コマンドラインから、

:echo lclib#get_script_id('%')

みたく呼ぶことが出来るのです。

*1:名付けにご協力してくださったujihisaさんに感謝

*2:この行数は将来変更する可能性があります

*3:ファイル更新時間の取得について、はじめはgetftime()を使っていたのだが、GitHubから取得したファイルは取得日時がそのまま更新時間となってしまうので上手くいかなかったために、ヘッダを使う方式にした。