この記事は Vim Advent Calendar 2014 3日目の記事です。
前説
VAC2014 のネタを全く考えていなくて、ぜんぜん思いつかなくて困っていたところ
面接官「テキストに書かれた4行をキーストローク5回で確実に上下反転させるにはどうしたらいいですか」 KoRoN「ちょっとvimのソース弄って来る」 mattn「五人だけに誤認逮捕でしょ」 thinca「普通に無理なんじゃ」 Shougo「そのまさかだ!」 一同「言わせねぇよ!」
— mattn (@mattn_jp) 2014, 12月 1
まっつんさんから急にネタを振られたので
@mattn_jp :rev<ENTER>
— MURAOKA Taro (@kaoriya) 2014, 12月 1
やってみることにしました。
ということで早速 :reverse
コマンドを作ってみましょう。
調査
まずは :sort
が、作りたいものにもっとも近いだろうということで :tag ex_sort
して、何をやってるのか読んでみます。おっと200行を超える関数ですね。マジメには読みたくありません。なのでざっくり見て…4つの処理からできていそうです。
- 引数の解釈
- ソート対象(行番号とかだけ)の配列作成
- 実際のソート(qsort + comparator)
- バッファの書き換えと微調整
今作ろうとしている :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
を実行します。
おっけー。 :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
は完了します。
どうです? 簡単だったでしょう。
まとめ
さあ動作確認してみましょう。
うまく動いているみたいですね。パッチ全体はココに置いてあります。
駆け足でしたが、 Vim の Ex コマンドの追加・実装方法を紹介しました。きっと大半の人にとって、想像してたよりもずっと簡単だったのではないでしょうか。あとはドキュメントを書くだけで、vim_dev に投稿できるパッチになります。
もちろんExコマンドはプラグインで実装することもできますが、C言語で実装するのも決して難しいわけではありません。むしろVimのフル機能を用いて実装できる分、Vimスクリプトでプラグインを書くよりも、はるかに楽な側面もあったりします。
機会がありましたら、是非ネイティブにVimを拡張してみてください。