pair matching extension

% コマンドは、カーソル直下、あるいは前方方向に最も近い括弧(({[)に対応する括弧へジャンプする。

「言われてみれば確かにそうだなあ」なのだけど、” や ‘ では % コマンドは働かない。vim であっても同じ。vim では括弧のペアは matchpairs オプションで “(:),{:},[:]” のように指定できるが、括弧の両端は異なる文字であることが求められる。

しかし内部的には、テキストオブジェクトで用いる関数はクォート文字で囲まれる領域の左右の各境界を認識できるのである。にもかかわらず % コマンドではそれを利用していない。

ということで、もったいないので wasavi では ” や ‘ や ` でも対応する境界へジャンプするようにした。あー、これはまあまあ便利かも。

a difficult AMO

AMO に行って wasavi の最新の xpi をアップロードしようとしたら、自動 validation に弾かれる。90 秒のタイムアウトで強制中断されるか、サーバの内部エラーで強制中断。何度やっても同じだ。なんだこれ。

今なにやら AMO では、AMO にアップロード済みの SDK ベースのエクステンションを最新の SDK で repack & compatibility-validation している/しようとしている状態らしいので(参考)、それが関係して一時的にアップロードできないのかもしれない。全然関係ないかもしれない。

Firefox(10日)、Opera(2日)、Chrome(即日) の順で更新版が公開されるのに手間と時間がかかるので、Firefox 版から先に片付けようとした先からこんな調子でずっこけた。

作った xpi の出来が悪いんじゃないのか? という可能性はなくはないのだが、AMO へログインした後、すでにレビュー・公開済みの 0.4.175 を再度 validate してもやはりサーバの内部エラーになるので、やはり向こう側の問題のようだ。

もう、駄目な AMO ぞうさんですね。

 * * *

bugzilla に登録されていた。現象は 10 月 1 日からだそうです。でも中の人からの何の反応もなく誰にもアサインされてもいなくすでに 3 日が経過している。

うー。まさにこの気持ちだ:

くるぞう: できるだけ急いでくれよな
くるぞう: 3日経ってますけど・・・

changed cursor substance

コマンドモードのとき表示されるカーソルは、position:absolute な div であり、カーソル位置の 1 文字の座標、文字を得て適切な場所に表示している。

が、なんだか微妙にずれる。しかも、wasavi 起動するページによってずれ方が違うのである。wasavi は iframe 内のコンテンツなので影響は受けないはずなんだけど、謎だ。

そういうわけでカーソルの実体を変更してずれないようにした。けっこうな修正。

 * * *

バッファの有効な行以降には、vi 同様 ~ を表示している。この tilde、実は canvas に描画した画像であり、縦方向に連続しているのはつまり css の background-repeat の機能だ。

これ、Opera においては canvas に対する文字の描画がいろいろバグっているのか、やたら小さく表示されるのが気になる。これはまあ、そのうち直るだろうと踏んで手をつけないが、行番号を表示させた場合に折り返される行の左端に tilde が残るのが目障りなので、出ないようにした。これもけっこうな修正。

 * * *

編集モードに入った最初の段階でキャレットが表示されていなかったのを修正。これは軽い修正。

 * * *

ステータスラインは、wasavi が寄生する textarea の外側に位置するようにしている。つまり、textarea のサイズ = wasavi のテキスト表示領域のサイズになるようにして、ステータスラインはそこからはみ出すようにしている。が、chrome ではその調節がうまく行ってなかったのを修正。

はみ出させるか否かは、wasavi が iframe 内にあるか、トップレベルのブラウジングコンテキストかどうかを判断する必要がある。そのために、window.frameElement を見ていたのだが、chrome の場合これを参照するだけで例外が発生するのだ。chrome の場合だけ window.parent == window かどうかで判断するようにした。

とりあえずこんなところでまた各ブラウザのエクステンションリポジトリに更新してもらおうというところ。

logging is conditional

例えば wasavi の起動時、終了時、あるいは何かエラーが発生したときなど、コード中の何箇所かで console.(log|error|info) している。これはデバッグの際にちょっと役に立つ。

さて実は、Firefox 版 wasavi の full review をしてもらった際、「次に更新するときはログは取っといてね。製品バージョンでふつうこんなことしねーから」と申し渡されている。そうか? と疑問に思わなくもないがちょっと考えてみることにしよう。

前述の通りログはデバッグに役立つ。自分でコードを書いて自分で動作を確認するときはもちろん、製品版であっても「~が動かない」「~のログは出てる?」というやり取りがよくあるのであって困るものではない。しかしまあこれは譲歩しよう。開発時はログを出し、公開版では出さないようにする。これで文句はあるまい。

これはまるでネイティブアプリの Debug ビルドと Release ビルドみたいな話だが、もちろんブラウザのエクステンションにはそういう区別は特にない。実行時に自分が開発バージョンか否かを判断できるような何か代替になるものを作らなければならない。

現在 wasavi のバージョンは 0.4 である。これが例えば 0.0 に戻ることはありえない。そこで、0.0 = 開発版ということにしよう。manifest.json、config.xml、package.json のそれぞれのバージョン情報を 0.0.1 にし、バックグラウンドの起動時に保持しておく。また wasavi が起動した際は開発版かどうかを通知し、開発版のときのみ console.なんちゃらを実行するようにする。

ということでそうした。

ところでエクステンションが自身のバージョン情報を得る方法として、Opera は widget.version、Firefox は require(‘self’).version を単に参照するだけでいいのに対して、Chrome の場合は manifest.json に management パーミッションを追加した上で chrome.management.version を参照しないといけない。

Chrome エクステンションでパーミッションの拡大は割と一大イベントである。エクステンションが更新されたとき、「なんかこのエクステンションがさらなるパーミッション要求してるけど、どうする?」とか聞かれたと思う。こんなダイアログ出たらだいたいの人は怖がって漏らしてしまう。もちろん本当に重要な機能追加のためにパーミッション拡大が必要になったら、そのときはせざるを得ないが、ドキュメントに大書するなり web サイトでアナウンスするなりといったフォローもあわせて行う必要があると思う。

翻ってバージョン情報の参照だが、これはぜんぜん重要な機能追加でもなんでもない。どうしたものか。

 * * *

management API を使わず、直接 manifest.json を xhr で読んで version を参照するようにした。

implementing range symbols #5

だいたい出来てきた。[{()}] の動きも vim と互換になっている。ただし、いくつかの相違がある。

  • word と同様、空白文字にカーソルがある状態で is すると、vim とは違い、カーソル以降の最も近い文が前後の空白なしで選択されるようにした
  • it、at つまりタグまわりは実装していない。
  • vim の場合、カーソルの基点位置と移動位置と移動先の文字によるめんどくさいルールで選択が文字指向となったり行指向となったりする(:help exclusive-linewise)がそれは実装していない。wasavi ではいずれのレンジシンボルも基本的に文字指向である。

implementing range symbols #4

vi には sentence、paragraph、section 単位でカーソルを移動させるコマンドがある。それぞれの定義は正確になされている。

wasavi では、それぞれのブロックの境界を見つけるのに定義に従った正規表現で楽しようとしていて、それはそれでそれなりに動いてはいるのだが、実は現状の実装だと vim や nvi と互換な動きをしていない。ちゃんと互換させようとすると正規表現だけで済ますのはちょっと難しいかもしれない。特に空行の扱いが難しい。vim や nvi ではそれぞれ謎のルールの仕込まれたループで境界を見つけている。

そういうわけでそのへん、つまり search.c 内のブロック系関数一式もごっそり移植せざるを得ない感。ちなみに sentence の検索関数だけ見ると nvi のほうがすっきりして分かりやすい気がする。

そしてこれは wasavi 側にしてもなかなかに大掛かりな書き換えになるのであった。

 * * *

javascript 製のコードエディタ Ace の 1.0 が公開された。これは 1.0 未満の状態でも割と前から公開されていて、オンラインデモも試すことができた。そのなかで、キーバインディングを vim 互換にすることもできたのだが、その再現具合は長らく「ううん……」というものだった。

で、今回 1.0 に達したということで久しぶりに試して見たらけっこうよくなってた。でも ex コマンドはない? 世の中の javascript で再現した系の vi クローンはなんでことごとく ex コマンドを実装しないのかなあ。

implementing range symbols #3

word
クォートされた文字列については終わったので、次に word、つまり iskeyword オプションに格納されている正規表現にマッチする文字で選択する機能。vim では、これもやはり search.c で処理している。current_word() である。これは特に記すこともなく実装した。

ひとつだけ特記しておくとするならば、aw と iw の違いだろうか。単語に付随する空白の扱いと言い換えてもいい。


foobar
^

^ の場所にカーソルがある状態で yaw すると、ヤンクされるのは [cci]” foobar”[/cci] だ。一方、yiw すると [cci]” “[/cci]、すなわち空白だけ。

これは、前者はともかくとして、後者はどうなんだ。空白は word なのか? いやー空白は単語ではないだろう……ということで、wasavi では vim とは違い、この場合 “foobar” がヤンクされる。

また、

foobar foobaz
^

の場合は、aw は [cci]”foobar “[/cci]、iw は [cci]”foobar”[/cci] が選択される。これは vim も wasavi も共通。

block
( ~ ) とか { ~ }、あるいは [ ~ ] の中身を選択する。これはもしかしたらいちばん簡単かもしれない。これらの中身との境界を認識する処理は、% コマンドを呼び出すだけだ。つまり処理のほとんどはすでに出来てあるのだ。というわけで、特に問題なく実装。

ただ、vim では { ~ } の場合だけインデント周りで特別な処理をしているのだけど、それが必要な意味がいまいち読み取れなかったので、実装していない。

sentence、paragraph
とここまではそんなに難しくない。難しいのは残りのセンテンスとパラグラフ単位の選択なのである。

気力が続けば続く。

implementing range symbols #2

というわけで実装し始める。前の記事の通り、レンジシンボルは 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 で指定し、デフォルトの値は "\" だ。クォート文字の検索の際はこれを参照し、エスケープされたクォート文字は無視するようになっている。

implementing text objects

vim にあって、vi にないものはそりゃたくさんあるわけだけど、今回は vim 界で言うところのテキストオブジェクトを考える。

vi コマンドをオペレーション+モーションの組み合わせて実行する際、vim ではモーションの代わりにテキストオブジェクトを指定することができる。[cci]yaw[/cci] と入力すれば、[cci]aw[/cci] がテキストオブジェクト。これは vi で [cci]yw[/cci] と入力するのと何が違うのかといえば、後者は対象になるのがカーソル以降の単語構成文字だけであるのに対し、前者はカーソル位置から必要なら前方・後方に同時に範囲を拡張する。

つまり

foobar
~

という状態のとき(tilde はカーソル)、[cci]yw[/cci] は “bar” が、一方 [cci]yaw[/cci] では “foobar” がヤンクされる。これだけなら、「ふーん。[cci]byw[/cci] すりゃいいじゃん」ということなのだが、テキストオブジェクトとブラケットを組み合わせると真価を発揮し始める。


function (foo, bar, baz) {
~

という状態のとき、[cci]yi([/cci] と打つ。これは ( ~ ) 内の範囲をヤンクする。これはピュアな vi コマンドだけでは容易に代替できない。

というわけで、これを移植してみたい。ただ、「テキストオブジェクト」という命名はちょっと大げさというか、微妙に分かりづらいと思うんですけど……。テキストオブジェクトというのは本来テキスト中のとある範囲のことであって、コマンド中で指定するのはそれを指す名前でしかない。名前はオブジェクトではない。Range Symbol くらいでいいんじゃないだろうか。うんそうだ。そうしよう。

vim のテキストオブジェクトは、以下の種類がある。これは vim で [cci]:help objects[/cci] すれば出てくる。

|v_aquote| a" ダブルクォートで囲まれた文字列
|v_a'| a' シングルクォートで囲まれた文字列
|v_a(| a( ab と同じ。
|v_a)| a) ab と同じ。
|v_a<| a< "a <>" '<' から '>' までを選択。
|v_a>| a> a< と同じ。 |v_aB| aB "a Block" "[{" から "]}" までを選択(括弧を含む)。 |v_aW| aW "a WORD" (ホワイトスペースを含む) |v_a[| a[ "a []" '[' から ']' までを選択。 |v_a]| a] a[ と同じ。 |v_a`| a` バッククォートで囲まれた文字列 |v_ab| ab "a block" "[(" から "])" までを選択(括弧を含む)。 |v_ap| ap "a paragraph" (ホワイトスペースを含む) |v_as| as "a sentence" (ホワイトスペースを含む) |v_at| at "a tag block" (ホワイトスペースを含む) |v_aw| aw "a word" (ホワイトスペースを含む) |v_a{| a{ aB と同じ。 |v_a}| a} aB と同じ。 |v_iquote| i" ダブルクォートで囲まれた文字列。ダブルクォートは含まない。 |v_i'| i' シングルクォートで囲まれた文字列。シングルクォートは含まない。 |v_i(| i( ib と同じ。 |v_i)| i) ib と同じ。 |v_i<| i< "inner <>" '<' から '>' までを選択。
|v_i>| i> i< と同じ。 |v_iB| iB "inner Block" "[{" から "]}" までを選択。 |v_iW| iW "inner WORD" |v_i[| i[ "inner []" '[' から ']' までを選択。 |v_i]| i] i[ と同じ。 |v_i`| i` バッククォートで囲まれた文字列。バッククォートは含まない。 |v_ib| ib "inner block" "[(" から "])" までを選択。 |v_ip| ip "inner paragraph" |v_is| is "inner sentence" |v_it| it "inner tag block" |v_iw| iw "inner word" |v_i{| i{ iB と同じ。 |v_i}| i} iB と同じ。

つまり、

range_symbol := inclusion_specifier range_specifier
inclusion_specifier := i | a
range_specifier :=
double_quote | single_quote | back_quote
| square_bracket | curly_bracket | angle_bracket | parentheses
| word | big_word
| paragraph | sentence | tag_block
double_quote := "
single_quote := '
back_quote := `
square_bracket := [ | ]
curly_bracket := { | } | B
angle_bracket := < | >
paretheses := ( | ) | b
word := w
big_word := W
paragraph := p
sentence := s
tag_block := t

こんな感じになる。

full reviewed

full review の結果を伝えるメールが来ていた。はいはい rejected でしょわかってますよ。

……通ってるー!?

というわけで、通ってた。Firefox 版の wasavi も amo から普通のインストールできる状態になった。
https://addons.mozilla.org/ja/firefox/addon/wasavi/

ちなみに full review も preliminary review と同様、レビューしてくれたのは Pentadactyl の作者の一人、Kris Maglione さんである。やはり vi つながりなのか?