vimperator completer

liberator.commands.addUserCommand の第 4 引数に指定するオブジェクト内で completer プロパティに callback 関数を設定するとそのコマンドの引数を補完してくれるというスグレモノなんだけどクセがあるのでメモ。

callback 関数の書式は以下のような感じ ( via: http://wiki.livedoor.jp/shin_yan/d/addUserCommand#content_2 ) 。一応 CVS のソースもチェックしてみたので ( http://www.mozdev.org/source/browse/~checkout~/vimperator/src/content/completion.js?rev=1.87;content-type=application%2Fjavascript の ex プロパティ。えーとソースの一番下。 ) 以下で間違いないはず。

[start, completions] = function(args, special){}

/*
 * start       : Number  - 補完開始位置。
 * completions : Array   - 補完候補群
 * args        : String  - 入力済み文字列
 * special     : Boolean - special command かどうか
 *                         ( コマンドに '!' がついているかどうか )
 * */

completions = [
    [name, description],
    [name, description],
             ・
             ・
             ・
];

/*
 * name        : String - 補完候補名
 * description : String - 説明
 * */

説明がやたら長くなったのでとりあえず注意点に関してだけは先にまとめよう。

  • args は前後の空白が trim される。
  • start をちゃんと設定すると vimperator が良きにはからってくれる。

の 2 つだけ。これ知ってると知ってないでは全然違う。

以降は ':tabnew' やなんかのデフォルトで使えるコマンドぽく賢い補完をさせたい場合の注意点を以下つらつら。あ、説明のために仮定をしとく。補完対象が 'google', 'yahoo', 'wikipedia' の 3 つしかない ':search' っていう独自コマンドを addUserCommand したという状況ね。

まず callback 関数に渡されてくる args のハナシ。実は打ちこんだのがそのまま渡されてくるわけじゃないみたい。どうも args は前後の空白文字を trim してから渡してくれるようで ':search google ' ( 最後に半角スペース ) と打った場合 'google' が args に渡される。

そして start の説明の「補完開始位置」という表現はは確かにあってるんだけど「補完時にどこから書き換えるかの指定」と言ったほうがわかりやすい気がする。言い換えるとどこまで書き換えないかというハナシ。

とかいってもおれにもサッパリ理解できないので ':search google ya' とか打ちこんでタブを押して 'yahoo' を補完するという状況を仮定するよ。このとき start が 0 なら ':search yahoo' ってな感じで 'google' が消える。で、 start を増やしていくと 1 で ':search gyahoo' 、 2 なら ':search goyahoo' みたいに消えない部分が増えていく。というわけですでに打ちこんだ部分を消したくない場合はちょと考えなきゃならない。

ここで start の決定に args の最後に空白があるかどうかを判別するっていう楽な手が使えたらよかったんだけど args の最後の空白は取り除かれるので無理。というわけでかなりメンドクサイ手段を使わざるを得ない。普通は以下でいいと思うんだけど、

// 半角スペースが見つからない場合は -1 + 1 = 0 になるので結果オーライ
var start = args.lastIndexOf(' ') + 1;

':search google yahoo' とかいう具合に最後の文字列が補完済みだった場合サムネが釣りだったときくらいテンションが下がるので以下のような感じにすると幸せになれるはず。

var inputted = args.toLowerCase().split(/\s+/);
var current = inputted[inputted.length - 1];

var candidates = availableCommands.filter( function(commandSet) {
    for(var i=0, numofInputted=inputted.length ; i<numofInputted ; ++i) {
        if(commandSet[0] === inputted[i]){
            inputted.splice(i, 1);
            return false;
        }
    }
    return true;
});

if(inputted[inputted.length - 1] !== current) {
    start = args.length + 1;
    return [start, candidates];
}

要は args の中のすでに打ちこまれた文字列のうち補完可能なものを除外していくわけだね。で、何も残らなかった場合はすべて補完可能だったわけなのでそこまでを確定とする、と。ここらへんマジでややこしいのでそれなりに時間をかけないといけないかも。

上のコードだとすでに打ちこまれた補完候補は表示しないようにするっていう仕組みも組み込まれてるけどそれは Array.filter() 関数内で === 演算子を使って文字列比較をしてるところね。真偽値のどっちを返すかで表示するかしないかということ。

で、上記が前段階。これからが本編なんだけどまぁ正直短い。

var commands = candidates.filter( function(commandSet) {
    return (commandSet[0].indexOf(current) === 0);
});
return [completePosition, commands];

これでおk。実際に使う場合は上記全部書くといいかも。だけどこれでもまだ仕様が単純化されてるほうでたとえば色を表す言葉は 1 つだけとか重複を許すとか考えると脳汁でそう。