template/snippet plugin for Battle Programmer

これは, Vim Advent Calendar 2012 74日目の記事です.
昨日の記事は, [twitter:@manga_osyo] さんの neobundle.vim の遅延処理で Vim の起動を高速化する でした.
僕もちょうど .vimrc を掃除しているので, ついでに高速化もしてみようと思います.


さて, 何番煎じかという感じですが, プラグインを作ったので, この記事ではその紹介をしたいと思います.

前置き

これまた何番煎じかという感じですが, テンプレート/スニペット挿入プラグイン的な何かである, sniplate.vim を作りました.
まずは, これを作るに至った理由を書いておきます.

テンプレート挿入プラグインスニペット挿入プラグイン

皆さんは, テンプレートやスニペットを挿入するプラグインを使っていますか?
以前軽く調べてみたのですが, テンプレート用のプラグイン, 実に沢山ありますね.
テンプレート挿入用のプラグインは, 好みの挙動をするプラグインはなかなか無く,自分で作ってしまえ!という人が多いようです.
スニペット挿入プラグインは, neosnippet, snipMate等が有名ですね.

今回僕が作ったのは, テンプレートとスニペットの合間くらいのコード断片, 特に小さな関数などを挿入するためのプラグインです.

やりたかった事

僕は競技プログラミングをやっています.
馴染みの無い言葉かもしれませんが, その紹介はこのあたりにお任せするとしておきます.
さて, 競技プログラミングでは, 素数判定をする関数であったりとか, 最大公約数を求める関数であったりとか, 同じ関数をよく作ります.
当然, ライブラリ化して, いつでも簡単なコマンドで挿入できるようにしたくなります.

しかし, テンプレート挿入プラグインでは, 1つのテンプレートを1つのファイルで管理する物ばかりであり, 数行程度のコード断片をファイル分けして管理するのは結構大変です.

スニペット系のプラグインを使えばよいと思うかもしれません.
しかし, 既に使っているスニペット程頻繁に使うものではありませんから, スニペットの一部として同居させたくはありませんでした.

また, よく書き換えたりする為, スニペットファイルその物を実行可能にし, テストコードを埋め込めれば便利です.
スニペット系のプラグインは, 大抵, スニペットファイル自体を実行可能にしようという感じでは無いと思います.

また, コード断片には依存関係があります.
例えば, (もちろんそうでなく書く事もできますが) 平均を求める関数は, 合計を求める関数に依存するでしょう.
平均を求める関数を挿入する際, 自動的に合計を求める関数も挿入して欲しくなります.

更に厄介な事に, いろいろな関数が同じ関数に依存していたりします.
同じ関数が何回も挿入されて欲しくはありませんから, 依存している関数が既に挿入されている場合は, 新たに挿入しないで欲しいです.

このあたりの要求が満たされるようなプラグインは, 僕が探した限りでは見つかりませんでした.

本題

sniplate.vimは, ここで管理しています.

インストール

他のプラグインと同様です. neobundle等のプラグイン管理プラグインを使っている方は,

NeoBundle 'MiSawa/sniplate.vim'

とし,

:NeobundleInstall

します. neobundle以外では, 適宜読み替えてください.
プラグイン管理プラグインを使っていない方は(今からでも入れる事をおすすめしますが), ~/.vim/ や $HOME/vimfiles/ 等, 自分の Vim ディレクトリに, ダウンロードしたファイルを展開します.

競技プログラマーの方は, コンテスト前に挙動が変わって死んだりしないよう, NeoBundle 'MiSawa/sniplate.vim', 'nosync' とかしておいて,
暇な時にアップデートと動作確認をした方が安全かもしれません.

使い方 その1

いくつかオプションで変更出来る箇所があります.

let g:sniplate#sniplate_enable_cache = 0

としておくと, スニペットのキャッシュが無効になるので, スニペット編集中はおすすめです.*1
そうでない場合は, 各ファイルタイプにつき, 最初の呼び出し時にスクリプト変数としてメモ化しています.

とりあえず, その他は初期オプションの前提で説明します.

~/.vim/sniplates/ 以下に, ファイルタイプ別のフォルダを作り, そこにスニペットを書いたファイルを置きます.
フォルダ階層が深くなっていても問題ありません.

サンプルのスニペットファイルを, ここ に置いておきました.
まず, これ を見てみてください.
和, 平均, 標本分散と普遍分散を計算する関数をスニペット化したものです.*2
コメントになっている部分を除くと基本的に普通のソースで, スニペット化したい部分を "BEGIN SNIPLATE スニペット名" と, "END SNIPLATE" で囲っているだけです.
これ以外の部分は必須ではありませんし, 囲われていない部分は無視されるので, main関数でテストコードを書く事も出来ます.

いくつかコメントでキーワードを埋め込んでいるのがわかると思います.
肝心なのは, {{pattern: foo}} と {{require: bar}} です.
{{pattern:foo}} では, そのスニペットが挿入されている時のみにマッチする正規表現を書きます.
例の関数 sum のように, ユニークになりそうな文字列をコメントで埋め込んでも良いですし, 他のもののように, 宣言にマッチしそうな正規表現を利用するのもよいでしょう.
patternキーワードを指定しないと, 既に挿入されているか否かに関わらず, 常に挿入するようになります.

{{require: bar}} では, そのスニペットが依存している他のスニペットを書きます.
variance_p から見た sum のように, 間接的に依存しているスニペットについてまで書く必要はありません.
この状態で, 新しい cpp タイプのファイルを作り,

:SniplateLoad variance_s

と入力すると, このようになります. 更に続けて,

:SniplateLoad variance_p

と入力すると, このようになります.
variance_s は, sum に依存していたため, sum の後に variance_s が挿入されています.
variance_p は, average に依存しており, その average は sum に依存していますが,
既に sum のパターンにマッチする文字列が存在するので, average と variance_p のみが挿入されます.
もちろん, sum がまだ挿入されていない場合は, sum も同時に挿入されるようになっています.

ちなみに, この例の場合,

Unite sniplate

すると, 次のような表示になります.

{{abbr: hoge}} というキーワードは, Unite sniplate をした時に, 候補の右側に表示されるコメントを指定するものです.
{{class: fuga}} というキーワードは, そのスニペットの分類を示していて, Uniteでの候補の絞り込みに利用できます.
マッチに利用される他に, 例えば,

Unite sniplate:statistics:math

等とすると, statistics と math の少なくとも一方のクラスに含まれているスニペットを候補にできます.

しかし, unite.vim を使う場合は, 大抵キーマッピングで済ませてしまい, Unite の引数を書かない事が多いと思います.
そこで, 候補としてクラスを扱う unite source, sniplate/class も作ってみました.
アクションは, 選んだクラスに属するスニペットをソースとして開くもののと, 全て挿入するものがあります.

また, よく使うスニペットというのは, 結構限られているものです.
候補が表示される時, よく使うものは上の方に表示したいですよね.
という事で, {{priority: 10}} とかで, スニペットの表示の優先度を設定出来ます.
指定しない場合は 0 で, もちろん負の値も指定できます.
ちなみに, これは :SniplateLoad での補完候補の表示順にも影響します.

使い方 その2

さて, スニペットの上下の話をしていましたが, 表示すらしない為のキーワードもあります.
{{invisible}} を入れると, そのスニペットは表示されません.
何の役に立つのかと思うかもしれませんが, require と組み合わせる事で, これが結構便利なのです.
次のサンプルは, これです.
通常のテンプレート系プラグインで挿入したいようなやつを, スニペット化したものです.*3

内容は無視してもらうとして, include 部分や, typedef 部分, define 部分をそれぞれスニペットにしているのがわかるでしょう.
それらを require し, その後に空白を入れるだけのスニペットである header がその後に続いています.
require で挿入される順序は, require の中で前にある物ほど上になるので, これで header を挿入すると, include, typedef, define の順に挿入されるようになりました.
require では, こういうグループ化みたいな事を行うことも出来ます.

しかし, 目的はmain関数の挿入なわけで, それ以外の部分は邪魔なので, {{invisible}} を指定する事で, 候補として表示されないようにしています.

main関数も, 引数を void にするか, ちゃんと取るかなど, 複数パターン考えられます.
通常のテンプレート系プラグインでは, これらを別のファイルに分けていた訳ですが, 引数部分以外はほとんど同じなわけで, 一方を修正するともう一方も修正しなければならなかったり, ちょっと冗長です.

sniplate.vim では, 同じファイルに書いています.
2つのmain関数など, 通常では実行できませんが, スニペットはコメントに囲まれていた所で問題無いので, ブロックコメントで2つ目のmein関数を囲っています.


main関数の中には,

//{{cursor}}

というキーワードが見え, 明らかに「ここにカーソルが来るんだろうな」感があります.
実際に :SniplateLoad main_void するとわかりますが, カーソル位置を | で表すと,

//header部分は省略
int main(void){
    //|
    return 0;
}

という状況になります. つまり, 2つ目の / 上に行きます.
コレジャナイ感がMAXというか, 行コメントの // も消して欲しいですよね.
という事で, 放っておいた設定を, 少しいじりましょう.

let g:sniplate#filetype_config = {}
let g:sniplate#filetype_config['cpp'] = {
    \ 'keyword_pattern' : '\%(//\)\?\s*{{\s*\(.\{-\}\)\s*\%(:\s*\(.\{-\}\)\s*\%(:\s*\(.\{-\}\)\s*\)\?\)\?}}',
    \ }

のようにします.
C++のファイルについての keyword_patternを上の正規表現で設定しています.
keyword_patternは, スニペットの読み込み/適用の際に使われ, 先ほどから出てきているキーワードの構文を設定する変数です.

これにマッチする場合, matchlist 関数に渡し, 1番目から3番めまでのサブマッチを使います.
1番目のサブマッチをキーワード, 2番めのサブマッチをその引数としていて, 後述する, 引数をもう一つとるようなキーワードの為に, 3番目のサブマッチを使います.
{{invisible}} や {{cursor}} のように, 引数が無いものもある事に注意してください.
サブマッチの番号が重要なので, サブマッチにならないグループ化の \%(hoge\) が便利です.

キーワードが cursor であった場合, keyword_pattern にマッチする部分全体を削除し, そこにカーソルを持っていく仕様になっています.
keyword_patternを変更した事で, {{cursor}} の前に行コメントがある場合, そこまで削除するようになります.

あとは, 新しいバッファでの自動挿入とかしたいですよね.

autocmd BufNewFile * call sniplate#load_sniplate_if_exists('template')

とか書いておけば, template という名のスニペットがあるファイルタイプであった場合は, 新しいバッファを開いた時にロードします.


もう一つ, テンプレート読み込み用として使う際に, とても重要な事があります.
ファイルタイプが cpp な空のバッファで :SniplateLoad main_void とすると, このような 感じになります.(|はカーソル位置です.)
何が重要かというと, このファイルは14行である, という事です.
このプラグインは, 行単位でしか扱っていないので, 行を上書きして挿入しているという事になります.

行を上書きするか否かの挙動は, 変数, {{overwrite:hoge}}キーワードで指定出来, 常に上書きする, 常に上書きしない, 空バッファの時のみ上書きする の3つから選択できます.

使い方 その他

これまでで, abbr, class, require, pattern, priority, invisible, cursor, overwrite の8つのキーワードについて説明しました.
実は, まだ5つ残っています.

1つ目はおそらく皆さんの予想通り, eval です.
{{eval:hoge}} が, eval(hoge) に置換されます.
よくある {{eval: strftime('%c')}} とか出来ます.

2つ目は, exec です.
{{exec:hoge}} が execute(hoge) されます.
{{exec:setlocal foldmethod=marker}} とか出来ます.

あとの3つは, 変数の機能です.
実行できるスニペットファイルとは何だったのかという感じですが,これを見てください.
sum なんていうのはよく使うと思いますが, 引数が vector だったり, vector だったり, 何でも中身はほとんど同じなわけです.
そこで, 型の所を {{var:T}} としておくと, 変数Tの値に置き換わります.*4

変数の値を変更するには, いくつかの方法があります.
まず, {{input[!]: name [:arg]}} キーワードです.
!が指定された場合, もしくは, name にまだ値が設定されて居なかった場合, 値を設定する為に, 組み込みの input 関数にそのまま arg が渡されます.
argの指定がなかった場合は, 適当なプロンプトを出します.

次に, まだ値が設定されていないのに, {{var:name}} に行き当たった場合です.
この場合も, 適当にプロンプトを出します.

最後に, {{let:name:hoge}} です.
これは, 変数 name に, eval(hoge) を入れるキーワードです.

変数の値は, オプションで, バッファ毎に保つか, 一回の挿入(require の分も含めて)の間のみ保つかの指定が出来ます.
バッファ毎に保つ場合,

:Unite sniplate/variable

をすると, そのバッファで定義された変数と, その値を確認することが可能です.
また, 値を変更する事も出来ます.

おまけ

この記事の下書きをほとんど終えてから, show_information というアクションを追加してみました.
例えば, 最初の例で, average を選択して show_information すると, こんな感じになります.

ルー語チックでアレですが, 複数の kind の candidate を組み合わせて, source にしています.
unite.vim は, こういう事も結構簡単に出来るあたり, 面白いですね.
単体の sniplate の candidate を取得出来る関数を用意しているので, 外部からも sniplate の candidate を扱えるようになっています.
もし何か使える所があれば, 使ってみてください.
そして, 他の unite source を持つプラグインでも, 単体の candidate を取得出来る関数があれば嬉しいなぁとか思います.

まとめ

前年度のものから, ずっと楽しく Vim Advent Calendar を読ませて貰っていました.

このプラグインは, 実は半年ほど前に書いたものを, ほとんど一から書きなおして出来ています.
当初の物は, とても人様にお見せ出来るようなものでは無かったので, ローカルで使っていました.
今の物が人様に見せられるかどうかというと, やはり怪しいですが, 以前のはもっとひどかったです.

Vim Advent Calendar に出そうという事にして, 「Advent Calendar 駆動開発」 的な書き直しをしました.
2日ほどかけて, 大まかに完成し, Vim Advent Calendar に登録する事ができました.
本当に良い原動力となり, 感謝しています.

そして, 一週間ほど, 起きている間はほとんどこれを触っている生活でした.
だんだん, よくある深夜のテンション的なのがデフォルトになって来て, 正直使いそうもない変数機能とか追加してしまったりして, なんか非常にアレです.
誰かよい使い道があったら教えてください.


あと, このプラグインは他の人の目に触れていないので, 自分の趣向に多分に傾いていますし, バグ混入の可能性も高いです.
意見がある方や, バグを発見した方は, ご報告, 又は pull request をいただけるとありがたいです.


最後に, このプラグインは, 一応無くても使えるものの, 殆ど unite.vim に依存していると言っても過言ではありません.
また, sonictemplate-vim, vim-template をはじめとし, いくつかのプラグインを参考にさせて頂きました.
開発者の皆様, ありがとうございます.


さて, 明日の Vim Advent Calendar は [twitter:@plaster] さんです.

*1:編集が終わったら戻すべきです. 処理によっては, O(n^2)からO(n)になるくらいの差があります.

*2:例として利用しやすかっただけで, 僕は競技プログラミングで分散を計算させられた事はないです.

*3:競技プログラミング以外でこういうのを書いたら怒られると思いますが, あくまで例という事で, 見逃してください.

*4:もちろん C++ なら, template という機能を使えばそれでよいのですが.