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 つながりなのか?

sorted out

wasavi のディレクトリ構造を整理した。

wasavi のディレクトリは、ローカルでは Subversion で管理し、さらに公開用に git も使っている。つまり、同一ディレクトリに .svn と .git が共存している。Subversion には、ここのサーバへの ftp のパスワードとか、chrome 用の wasavi.pem といった、ムフフ(謎)なファイルも格納しているが、git に対しては ignore させている。

で、github で見ればわかるとおり、今まではいわゆる trunk 直下に wasavi.js とか諸々を置いていた。これは基本的に chrome の extension を前提とした構造である。opera と firefox 向けにはそれぞれ専用のディレクトリを作り、trunk 直下へのファイルへのシンボリックリンクを置いていた。なぜ chrome 第一主義になっているかというと、別に chrome サイコー! というわけでは全然なくて、chrome は extension のファイルについて ntfs ネイティブなシンボリックリンクを解決してくれないからだ(最近のはどうか知らないが、試していない)。

これはなかなかゆがんだ構造だったので、trunk 直下には chrome/opera/firefox のディレクトリを置くようにし、ソース群は chrome/ へ移動した。すなわち、やはり基本的には chrome extension が前提の構造だ。opera/firefox については、シンボリックの参照先が trunk 直下から chrome/なんちゃらへ変わっただけだ。

それはそれでそれだけの話なのだけど、面倒なのは build.xml と、ビルドの際に必要なこまごまとしたシェルスクリプトで、通して見直さなければならないのであった。

めんどいなー!

 * * *

ぼちぼち Firefox 版の full review がなされる頃合になってきているが、どうもぐぐって見ると、amo 上でユーザーレビューが複数ないとそもそも full review してもらえないとか。現在 wasavi に対するユーザーレビューは 0 である。だめじゃん。まあそもそも full review に一発で通る気ではぜんぜんいないが、どうもまだ時期尚早だったようだ。うーんしばらくは preliminary reviewed の状態で更新したほうがいいのか?

evaluate a expression

以前 manifest v2 な chrome extension では、eval や new Function() が禁止されると書いたが、なぜか new Function() は動いていた。しかし今日通しでテストしてみたらいよいよ動かなくなった。chrome のバージョンは 21.0.1180.89m だ。

wasavi では eval は除去してあるが、未だメッセージ中の特定の単語を付随する数字に従って変化させる処理と :s の置換文字列を生成する処理で new Function() を使っている。いずれも小規模な一種の言語といってよいものであり、コンパイルと実行処理が必要になるわけだが、実際にコンパイラと実行マシンを書くわけではなく javascript の機能でまかなっていた。

これらが今回動かなくなったので、書き換える。

まず前者。適当なメッセージ内の単語が数値に付随するとき、その数値によって単語が変化するパターンを得る。たとえば英語の場合、数値が 1 のときとそれ以外の場合に分けられる。そのためパターンは 2 種類必要になる。

従来、メッセージカタログ内に “n==1?’one’:”” というような javascript の式を置いていた。これを実行時に new Function() で関数に変換・保持し、実行時にメッセージを要求されたときに適宜使用する。今回、new Function() が使えなくなったので、自前で式を解析して評価する仕組みにしないといけない。

といっても javascript の式を解析する処理を書くわけにも行かないので、もっと単純な式にする。

変換識別子列 := 変換識別子
変換識別子 , 変換識別子列
変換識別子 := 変換関数名 ( 変換パターン接尾子 )
変換関数名 := [a-zA-Z_][a-zA-Z0-9_]*
変換パターン接尾子 := [a-zA-Z_][a-zA-Z0-9_]*

というような単純な式を動的に評価し、変換識別子の配列を持っておき、実行するようにした。メッセージカタログに置く式は “is_one(one)” といった形になる。is_one という変換関数の実態は wasavi.js に定義しなければならないので、従来の方法だと新たに言語に対応する際もメッセージカタログの新設で済んだのが、wasavi.js の対応も必要になることになる。

次に後者。これは例えば :s/foo\(bar\+\)/FOO\u\1/g みたいな ex コマンドが与えられたとき、置換文字列から “return ‘foo’ + arg[1].substring(0, 1).toUpperCase() + arg[1].substring(1);” みたいな javascript の式を生成し(arg は正規表現にマッチした結果の配列)、関数を作り出す処理。これも動かなくなるので、やはり適当な中間形式にコンパイルし実行させるようにしなければならない。こちらは、コンパイラと実行マシンを書く手間のほかに実行効率が気になる。なにしろ置換が発生するたびに呼ばれるのだ。しかしそう言ってもいられないので修正した。

 * * *

最新のビルドをここで公開すると同時に、Chrome と Opera のエクステンションリポジトリに更新を申請した。

stable or unstable, that is the question

wasavi を Chrome、Opera、Firefox と各エクステンションの公式リポジトリへ登録してある。しかし、それと同時に、ここのサーバでもエクステンションのパッケージを野良 build として置いてある。

この 2 つのパッケージの関係は、ありがちなことに、公式 build が安定版、野良 build が最新だが安定性は保証しない版ということになる。

さて野良 build は今まで週一で更新してきたのだが、最近はさぼり気味だ。なぜか。現在 Firefox 版の wasavi は full review 待ちという状態である。full review がなされたとして、そのまますんなり通るとは考えていない。いろいろと指摘をされるだろう。そうすると当然修正するわけだが、そのときに関係ない機能追加とかがあると向こうのレビュアーを混乱させるだけである。なので、大掛かりなコードの改変はしにくいのだ。

もちろん wasavi はローカルでは Subversion で管理しているので、Firefox 公開版と trunk は別に管理することもできるが……実際面倒だ。

all tests are migrated to java

いろいろ近況。

  • 500 項目すべてのテストを java へ移行した。クリップボードアクセスなどコマンドが非同期で実行されるものもテストできるようになった。
  • ex コマンド入力時のショートカットを拡充した:
    • ^A, HOME: 行先頭へ
    • ^E, END: 行末尾へ
    • ^B, LEFT: 1文字左へ
    • ^F, RIGHT: 1文字右へ
    • ^H: カーソルの左の1文字削除
    • ^N, DOWN: 履歴を1つ進める
    • ^P, UP: 履歴を1つ戻る
    • ^U: 全文字削除
    • ^V: コントロールコード入力のためのプリフィクス
    • ^W: カーソルの左の1単語削除
    • DELETE: カーソルの右の1文字削除

    chromeでは一部のショートカットをスクリプトで制御できない。それには ^N や ^W も含まれる。

  • Firefox の full review は Queue Position: 46 of 113。
  • wasavi 終了時に、マークを寄生先 textarea へ保存するようにした。
  • 寄生先 textarea のサイズ変更にある程度追従するようにした。

watch a test

ググればたくさん情報は得られるが、とりあえずメモ。

junit でテストを書く際、今何のテストをしているのか……などのログを吐きたいことがある。例えば wasavi のテストで、何らかのミスがあって、WebDriverWait がタイムアウトするとする。タイムアウト自体は catch 節で受け取るので、そこで単に System.out.println(“timed out!”); すれば ant が保存するログに含まれる。が、どのテストで発生したかまではわからない。

そこで、テストケースで

@Test public void foo () {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
}

などと書けばログにテスト中のメソッド名が書かれるので判別する材料になる。しかしこれはすべてのテストケースに埋め込まなければならないので、とても煩雑だ。

さて junit4 には TestWatcher というものが用意されていて、テスト開始・終了、あるいは成功時・失敗時に任意の処理をはさむことができる。開始・終了というのは @Before と @After と被っているが、これらはあくまでテスト視点での開始・終了なのに対して、TestWatcher のそれはテストランナーから見た、「テストメソッド」の開始・終了であり、テストメソッドの素性などのメタな情報を利用することができる。


import org.junit.*;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

public class WasaviTest {
protected String logText;

@Rule public TestRule watcher = new TestWatcher() {
protected void starting (Description d) {
System.out.println("Testcase: " + d.getMethodName());
}
protected void failed (Throwable e, Description d) {
System.out.println(d.getMethodName() + " FAILED\n" + logText);
}
};

@After
public void tearDown () {
logText = driver.findElement(By.id("test-log")).getAttribute("value");
}
}

こんな感じで自前のテストの基底クラスに仕込んでおけば、これを継承した個々のテストでは何も考えることなく勝手にテストケースの名前が出力される。

くわしくは javadoc を見れば全部書いてある。

buffering key input

キー入力周りを書き直した。

wasavi は document に対して keydown、keypress 両方をリスンする。keydown では機能キー(esc、tab、backspace、enter、pageup、pagedown、home、end、insert、delete、F1 ~ F12 の入力を見る。keypress では文字を表すキーを見る。

機能キーは ctrl および shift 併用の入力は、併用しない入力とは別個のキー入力として扱う(が、:map に定義する展開パターンではまだモディファイアキーに対応してない)。一方文字キーは、特定の キーと同時に ctrl が押されていたらコントロールコードの入力とみなすほかは、ctrl も shift も無視する……これは、特に shift 併用した際にキーボードレイアウトがらみで判断が難しいからだ。難しいというより、無理だと思う。あくまで入力された文字そのものだけをみてどのコマンドにディスパッチするかを決める。

また、スムーズスクロール中のキー入力を今までは単に捨てていたのを、キューにバッファするようにした。スクロールが完了した直後にキューをすべて掃き出す。これによりテストがちょっと楽になる。

 * * *

ところで amo を覗いてみたら、wasavi の full review は Queue Position: 45 of 106 だった。おーけっこう進んでるー!