というわけで実装し始める。前の記事の通り、レンジシンボルは inclusion_specifier、つまり “a” か “i” から始まる。なのでコマンドマップのそれぞれのハンドラを修正する。
“a” と “i” は、もちろん通常は append と insert コマンドだ。これらのコマンドのハンドラは、d/y/c などのオペレーションが先行入力されていないことを最初に確認する。何かオペレーションが入力されていたらエラーにする。つまり “3da” とかいったコマンドは成立しない。これを、オペレーションが入力されていたらレンジシンボルとして評価してみて、評価成功ならエラーにせず、オペレーションを実行させるようにする。
ちなみに vim においては、この辺の処理、つまり “a” と “i” の処理をテキストオブジェクトに差し替える辻褄あわせは normal.c でやっている。また、テキストオブジェクトのそれぞれの具体的な選択処理は search.c でやっている。まずは、これらのソースを参照することにしよう。
とりあえず、単純すぎず、かつ複雑すぎなさそうなクォーテーション周りから見てみる。search.c の current_quote() だ。
/*
* Find quote under the cursor, cursor at end.
* Returns TRUE if found, else FALSE.
*/
int
current_quote(oap, count, include, quotechar)
oparg_T *oap;
long count;
int include; /* TRUE == include quote char */
int quotechar; /* Quote character */
{
仮引数の中で、count だけが妙だ。何かの判定に使用しているほかは、クォーテーションの検索そのものには使っていない。ちょっと保留。
current_quote の中では、まず大体こんな感じの処理をしている:
if (line[col_start] == quotechar)
{
int first_col = col_start;
/* The cursor is on a quote, we don't know if it's the opening or
* closing quote. Search from the start of the line to find out.
* Also do this when there is a Visual area, a' may leave the cursor
* in between two strings. */
col_start = 0;
for (;;)
{
/* Find open quote character. */
col_start = find_next_quote(line, col_start, quotechar, NULL);
if (col_start < 0 || col_start > first_col)
return FALSE;
/* Find close quote character. */
col_end = find_next_quote(line, col_start + 1, quotechar,
curbuf->b_p_qe);
if (col_end < 0)
return FALSE;
/* If is cursor between start and end quote character, it is
* target text object. */
if (col_start <= first_col && first_col <= col_end)
break;
col_start = col_end + 1;
}
}
else
{
/* Search backward for a starting quote. */
col_start = find_prev_quote(line, col_start, quotechar, curbuf->b_p_qe);
if (line[col_start] != quotechar)
{
/* No quote before the cursor, look after the cursor. */
col_start = find_next_quote(line, col_start, quotechar, NULL);
if (col_start < 0)
return FALSE;
}
/* Find close quote character. */
col_end = find_next_quote(line, col_start + 1, quotechar,
curbuf->b_p_qe);
if (col_end < 0)
return FALSE;
}
まず、カーソル位置がクォート文字かで処理を大別している。クォート文字である場合は、それがクォートされた領域の最初なのか最後なのかわからない。そこで、行の先頭からクォートされた領域を検索し、カーソルが属するクォートされた領域を確認している。
カーソルがクォート文字内にない場合は、カーソルがクォート領域内にあると仮定した上で後方へ検索する。クォート文字が見つからなければ、カーソルはクォート領域外にあるとみなし、前方へ検索する。クォート開始文字が見つかったら、そこから更にクォート終了文字を検索する。
興味深いのは、前者(カーソル位置がクォート文字である場合)は、あくまで行内でという前提ではあるが、行をクォート文字で囲まれた領域とそれ以外で区別して扱っている、すなわち基本的には構文解析のようなことを意識しているのに対して、後者は単に後方と前方にクォート文字がある領域、みたいな乱暴な区切り方をしているうえにそれらが混ぜこぜになっている点だ。まあ実際に vim を使っててなんかこの動作おかしくね! って思ったことはないのでいいのだろう。
それから、クォート文字の検索があくまで 1 行内で完結しているのも覚えておくべき点かもしれない。言語によっては、文字列リテラルに直接改行を含められたり、あるいはそうでなくても \ で改行をエスケープしたりして、文字列リテラルが複数行にわたる場合があるがそういう場合にはこのテキストオブジェクトはうまく機能しない。
もっともうまく機能させるにはテキストオブジェクトの処理内を膨らませるよりもシンタックスハイライティングの副産物としての構文解析情報を参照したほうがいいだろうから、これもこれでいいのだろう。
もうひとつ重要なことに、文字列内のエスケープシーケンスのプリフィクスがある。vim ではオプション quoteescape で指定し、デフォルトの値は "\" だ。クォート文字の検索の際はこれを参照し、エスケープされたクォート文字は無視するようになっている。