Vimにrevコマンドを実装してみた

急にネタを振られたので、 Vim に reverse コマンドを実装してみました。

この記事は Vim Advent Calendar 2014 3日目の記事です。

前説

VAC2014 のネタを全く考えていなくて、ぜんぜん思いつかなくて困っていたところ

まっつんさんから急にネタを振られたので

やってみることにしました。

ということで早速 :reverse コマンドを作ってみましょう。

調査

まずは :sort が、作りたいものにもっとも近いだろうということで :tag ex_sort して、何をやってるのか読んでみます。おっと200行を超える関数ですね。マジメには読みたくありません。なのでざっくり見て…4つの処理からできていそうです。

  1. 引数の解釈
  2. ソート対象(行番号とかだけ)の配列作成
  3. 実際のソート(qsort + comparator)
  4. バッファの書き換えと微調整

今作ろうとしている :reverse においてはそうですね…2と4をパクれば楽にできそうです。まぁ、ネタなのであまり凝ってもしかたありません、1は要らんし、4も適当で良さそうです。

あとExコマンド定義となる ex_cmds.h の該当部分も見ておきましょう。

EX(CMD_sort,            "sort",         ex_sort,
                        RANGE|DFLALL|WHOLEFOLD|BANG|EXTRA|NOTRLCOM|MODIFY,
                        ADDR_LINES),

実装その1 (周辺作業)

うーん…(各フラグの意味をタグジャンプしながらひと通り確認して)…こんな感じでいいんじゃないですかね。わかんないですけど。

EX(CMD_reverse,         "reverse",      ex_reverse,
                        RANGE|DFLALL|WHOLEFOLD|MODIFY,
                        ADDR_LINES),

というわけで、これを CMD_rewind の前に書き足しましょう。順序はソートされているみたいなので、それに合わせておきます。

なおフラグの意味はこうなっています。

フラグ名 意味 要否
RANGE 範囲指定を受け付ける
DFLALL 範囲指定がない場合はバッファ全体を対象とする
WHOLEFOLD 折りたたみがあった時に上手いことやってくれる
BANG ! を受け付けるか ×
EXTRA その他の引数を受け付けるか ×
NOTRLCOM コマンドの後ろにコメントを許容しない ×
MODIFY 変更不可のバッファに適用できなくする

あとは src/proto/ex_cmds.pro も手でいじっちゃいましょう。本来は自動生成させるものですけど、ちょっと遊ぶだけだから逆にめんどくさいですしね。 ex_sort を定義している行を探し(4行目にあります)、コピペして ex_reverse に書き換えちゃいましょう (コマンドは yypfscwrev<C-P> みたいな感じで)。

次に ex_reverse の実体を…こんなふうにしちゃいましょう。

/*
 * ":reverse".
 */
    void
ex_reverse(eap)
    exarg_T     *eap;
{
    /* TODO: implement me. */
    EMSG(":reverse is executed!");
}

そしたらおもむろにビルドして起動し :rev を実行します。

msg

おっけー。 :rev を叩くと ex_reverse() が正しく呼び出されています。

実装その2 (本体作業)

さて ex_reverse 本体の実装なんですが、これは ex_sort を見よう見まねで、必要なところだけ実装する…それだけで特に見どころは、ないです。なので結果だけ貼り付けちゃいます。

    void
ex_reverse(eap)
    exarg_T     *eap;
{
    size_t      count = (size_t)(eap->line2 - eap->line1 + 1);
    size_t      i;
    linenr_T    lnum;
    char_u      *s;
    long        deleted;

    if (count <= 1)
        return;

    if (u_save((linenr_T)(eap->line1 - 1), (linenr_T)(eap->line2 + 1)) == FAIL)
        return;

    lnum = eap->line2;
    for (i = 0; i < count; ++i)
    {
        s = ml_get((linenr_T)(eap->line2 - i));
        if (ml_append(lnum++, s, (colnr_T)0, FALSE) == FAIL)
            break;
        fast_breakcheck();
        if (got_int)
            goto reverse_end;
    }

    if (i == count)
        for (i = 0; i < count; ++i)
            ml_delete(eap->line1, FAIL);
    else
        count = 0;

    deleted = (long)(count - (lnum - eap->line2));
    changed_lines(eap->line1, 0, eap->line2 + 1, -deleted);

    curwin->w_cursor.lnum = eap->line1;
    beginline(BL_WHITE | BL_FIX);

reverse_end:
    if (got_int)
        EMSG(_(e_interr));
}

ml_get() に渡している引数以外は、ほぼ完全に ex_sort からのコピペと不要な箇所の削除だけでできています。とっても簡単ですね。

解説

とはいえ、これだけではなんですから、細かく解説してみましょう。

size_t      count = (size_t)(eap->line2 - eap->line1 + 1);

まず eap->line1 には処理の開始行番号が、eap->line2 には終了行番号が入ってます。よって count には処理すべき行数が計算されます。

if (count <= 1)
    return;

処理する行が1行なら、反転しても現在の状態と変わらないので、即時終了できます。

if (u_save((linenr_T)(eap->line1 - 1), (linenr_T)(eap->line2 + 1)) == FAIL)
    return;

これはざっくりいって undo のために変更前の状態を覚えておく、そんな意味合いです。まぁおまじないってことで良いでしょう。

lnum = eap->line2;
for (i = 0; i < count; ++i)
{
    s = ml_get((linenr_T)(eap->line2 - i));
    if (ml_append(lnum++, s, (colnr_T)0, FALSE) == FAIL)
        break;
    /* (snip) */
}

終了行から開始行に向かって1行ずつ、追加位置(lnum)へ追加しています。これにより操作対象となる行全体が、その後ろに逆さまに貼り付けられた状態になります。ちょっと図示してみましょう。

1st line.
2nd line.
3rd line.
last line.

先の処理を経ることで、もともとはこうだったのが

1st line.
2nd line.
3rd line.
last line.
last line.
3rd line.
2nd line.
1st line.

このようになります。

fast_breakcheck();
if (got_int)
    goto reverse_end;

ループ中で省略したのは <CTRL-C> を受けて処理を中断するための処理です。おまじないとしておきましょう。

if (i == count)
    for (i = 0; i < count; ++i)
        ml_delete(eap->line1, FAIL);
else
    count = 0;

次に、もともとあった部分を削除することで、行の反転は完成します。処理対象の先頭行を消すのを、処理すべき行数分、繰り返しています。下の else は異常時用のワークアラウンド、とでも考えておきましょう。

deleted = (long)(count - (lnum - eap->line2));
changed_lines(eap->line1, 0, eap->line2 + 1, -deleted);

あとは undo の更新をして…

curwin->w_cursor.lnum = eap->line1;
beginline(BL_WHITE | BL_FIX);

カーソル位置を、開始行番号へ移動し、先頭の非空白文字に移動すれば ex_reverse は完了します。

どうです? 簡単だったでしょう。

まとめ

さあ動作確認してみましょう。

demo

うまく動いているみたいですね。パッチ全体はココに置いてあります。

駆け足でしたが、 Vim の Ex コマンドの追加・実装方法を紹介しました。きっと大半の人にとって、想像してたよりもずっと簡単だったのではないでしょうか。あとはドキュメントを書くだけで、vim_dev に投稿できるパッチになります。

もちろんExコマンドはプラグインで実装することもできますが、C言語で実装するのも決して難しいわけではありません。むしろVimのフル機能を用いて実装できる分、Vimスクリプトでプラグインを書くよりも、はるかに楽な側面もあったりします。

機会がありましたら、是非ネイティブにVimを拡張してみてください。