execution #2

s コマンドについてもまとめておきたい。

s コマンドは global に似ていて、前段の処理と後段の処理が分かれている。前段で正規表現にマッチした箇所を覚えて、後段でそれらを置換する。したがって中間形式でも、ユーザがアクセスできる ex コマンドは前段、実際に仕事をするのは後段というように分けるのが自然だ。後段を latter-subst と名付けよう。

s コマンドは c オプションを付加するかどうかで動作がまるっきり変わる。すなわち、置換ごとにユーザに動作を確認するインタラクティブなモードと、一気に置換するモードだ。後者の場合は、latter-subst を生成する必要はない。前段の処理の最後に置換処理を呼び出して終わりである。したがって latter-subst は c オプションが付加されている場合のみ生成される。

c オプションが付加されると、モードが ex_s_prompt になる。また、置換に必要な情報は SubstituteWorker インスタンスに格納され、保持される。そのモードでのキー入力によって適切な動作を行う。現在は、modeHandler が SubstituteWorker を直接制御しているのだが、これは latter-subst に移譲したほうがよいだろう。つまり ExCommandExecutor のプロパティとして現在のプログラムカウンタが指す ex コマンドオブジェクト的なものを参照できないといけない。

なかなか固まってきた。そろそろ実装したい。

execution

ex コマンドの実行処理を再考したい。

実行処理の核となるループは単純だ。

  • ソース文字列は ex コマンド列である
  • ソース文字列から ex コマンドをひとつ取り出す。このとき、ex コマンドの区切りは改行だったり、[cci]|[/cci] だったりコマンドごとに異なる。この違いを吸収するのはパーサの仕事だ
  • 取り出した ex コマンドから引数を解析する
  • コマンド毎のハンドラを呼び出す
  • ハンドラがエラーを返すか、ソース文字列の最後まで実行するまで繰り返す

しかし単純ではないものもある。まず、動作が非同期に行われるコマンドがある。

  • クリップボードからの読み込み
  • dropbox などからのファイルの読み込み

これらのコマンドを実行した場合、いったん実行ループをそこで終了し、待機状態にする必要がある。ex コマンド実行中ではあるが、待機中ということだ。そして、それぞれの処理が完了した時点で実行ループを再開する。なお、待機状態のタイムアウトや、[cci]^C[/cci] を特別なシグナルとして扱う処理は必要だ。

もうひとつ面倒なのは、ex コマンドが入れ子に実行される場合がある。global、v、および edit コマンドなども該当する。

現在の ex コマンド実行クラスである ExCommandExecutor は、非同期処理の機構は基本的には実装済みであるものの、先の記事の通り global の入れ子としてのコマンド群の実行が非同期処理に対応していない。これをどうにかするのがこのミッションのステートメントだ。

ExCommandExecutor はまずソース文字列を解析し、ひとつの ex コマンドにひとつのオブジェクト(引数とハンドラを保持する)が対応するその配列を生成する。ソースそのものではなくいわば中間形式にして保持するのは、非同期の完了時に呼ばれる仮想的・擬似的な ex コマンドを動的に ex コマンド列中に挿入する必要があるためだ。ソース文字列のままだと挿入するポイントをつかみにくい。なお擬似というのは中間形式の状態では存在するが、あくまで内部的に使用するものであって、ユーザーが直接使用することはできないということだ。

それを踏まえて入れ子コマンドを考えるに、中間形式も入れ子の構造にする必要はない。たとえば配列の [n] 番目が edit でその引数に読み込み完了後に実行される ex コマンドを指定した場合、時系列的にはその入れ子のコマンド群は [n+1] 番目のコマンドの前に実行されるわけなので、単に一次元の配列のまま [n] の直後、[n+1] の直前に新しい中間形式を挿入すればよい。あくまで一次元の配列のまま考えばよい。

ただしそうは言っても、global の場合はもっと複雑になる。[n] 番目が global だったとき、[n] の直後に入れ子コマンドの中間形式を挿入するのは同じだが、先頭には擬似 ex コマンドとして global の処理の後段、つまり与えられた正規表現にマッチする全ての行に対して入れ子コマンドを実行するループを制御する処理を司る中間形式 latter-global-head が入る。また末尾にもまた擬似 ex コマンドとして、入れ子コマンドの実行後の分岐を行う処理を司る中間形式 latter-global-bottom が入る。latter-global-head は引数とハンドラの他に

  • 正規表現にマッチした全行への参照
  • 入れ子の ex コマンドの個数

も保持する必要がある。latter-global-bottom は

  • 入れ子の ex コマンドの個数

を持つ。

そして、実際にこの中間形式を実行する際は、

  1. global: 次に実行されるべき latter-global-head を挿入する
  2. バッファから正規表現にマッチする行を抜き出し、latter-global-head に保持させる
  3. 入れ子のコマンドの中間形式を latter-global-head の直後に挿入する
  4. 続けて latter-global-bottom を挿入する
  5. latter-global-head、latter-global-bottom に挿入した中間形式の個数を保持させる
  6. latter-global-head: マッチしている行が残っているなら、何もしない。すなわち直後の中間形式を実行させる
  7. マッチしている行が残っていないなら、中間形式のプログラムカウンタ的なものに入れ子の ex コマンドの個数を加算し、実行をスキップさせる
  8. latter-global-bottom: 入れ子のコマンドが実行されるとその最後にこの中間形式が実行される。プログラムカウンタを latter-global-head へ戻す

というような、逐次処理・分岐処理・繰り返し処理を備えたちょっとした構造化言語として振る舞わせる必要がある(これは一見、ex コマンドの実行処理部がそのままスクリプティングの実行エンジンに転用できそうな感じがするが、その予定はない)

それから、入れ子のコマンドでは特定のコマンドが使用を制限されることに気をつけなければならない。global の入れ子コマンドは global と v を使うことができない、と POSIX は定義している。これはわかる。しかし不思議なことに、edit コマンドに付加できるコマンドにはそういう制限はないように思える。つまり edit の入れ子に edit したりすることが許される。[cci]:edit +edit\ B A[/cci] としたとき、最終的に読み込まれるのは B だ。これにどういう意味があるのかはわからないが、まあそういうふうに動作するように組んでみよう。

global revisited

vi というエディタに神秘的な力を与えている機能の一つ、それが [cci]:global[/cci] コマンド。

もちろん wasavi もこれを実装している。が、実装したのが結構前でそれほどコードをブラッシュアップしているわけでもなく、また今回ちょっとなかなか致命的なバグを見つけたので、おさらいしたい。下手をすると、このバグに対する修正は ex コマンドを実行する機構そのものに手を入れないといけないかもしれない。

まず global コマンドは 2 つのパスで構成される。第一に、与えられた正規表現にマッチする行を抜き出して、一時的なバッファにそれを覚えておくループを行う。第二に、バッファから 1 行取り出し、その行が存在するならば、与えられた ex コマンドを実行するというループを行う。この 2 パス動作は POSIX で記述された仕様でもある(ただし logically two-pass となっているので必ずしも実装において 2 パスにすることが求められているわけではない、と思う)。

このとき、入れ子で実行される ex コマンドには若干の制限がかけられる。

  • global、v、undo コマンドは入れ子にできない
  • edit コマンドなどでバッファの内容全体が置き換えられる ex コマンドを実行した場合は、動作を終了し、その旨を表示する
  • その他 open、visual コマンドの動作について特別扱いがあるが、wasavi はこれらを実装してないので省略

で。

問題はそのへんではなく、global コマンドの入れ子になっている、[cci]c[/cci] オプションを付加した [cci]:s[/cci] コマンドが正しく動かないのだ。これはむずかしい。c オプションを付加した :s コマンドは処理が 1 関数の中で完結しない。一旦マッチ候補を洗い出して substituteWorker インスタンスへ突っ込んだらそこで :s コマンドの処理を抜ける。このとき、モードを ex_s_prompt 状態へ移行する。キー入力がなされたら substituteWorker インスタンス内の置換処理を呼び出す……というように、処理を分割しないといけないのだ。

入れ子のコマンドの実行が分割されていると、global コマンドの動作が正しくならない。これはすなわち、入れ子の ex コマンド群が分割されていたら、global コマンドの実行も分割しないといけないということだ。現在はそうなっていないのである。

fix for Polish

issue #47 への対応。

これはポーランドの方からのバグ報告で、ポーランド語向けのキーボードではいくつかの文字は AltGr キーを併用して入力する必要があるのだが、wasavi の input モードではそれを入力できないというものだ。

詳しく聞いてみると、入力できないのは Chrome を Windows で動かした場合に限るようである。が、これはポーランド語に限った話ではなく、AltGr という日本人にはあまり馴染みのないキーを使用する言語圏に共通する問題に思える。通常キーボードから入力できる文字というのは、直接入力するキー、shift 併用で入力するキーである。これに AltGr が加わると、さらに複数のキー入力を用いて単一の文字を入力できるようなのである。おもしろい。とてもおもしろい。

まず Windows のキーボード設定にポーランド語を追加する。いくつかさらに小分類があるが、どれを選ぶべきなのかはちょっとわからない。とりあえず最初のやつを選ぶ。いずれにしても、しかしそれによってキーボードのどれかのキーに AltGr が現れるわけではないので、スクリーンキーボードを使う。スクリーンキーボードは、選択している言語によってリアルタイムに表示上のキーの配列が変化する(正確には、切り替えたあとキーボードの上をマウスでなぞったりする必要があるので半リアルタイムというところだ)。

そんな環境で テストページを開き、適当にキー入力してみると確かに AltGr 併用の入力ができない。このページで使っているスクリプトは基本的に wasavi と同じロジックでキー入力を処理している。

スクリプトの動きを追ってみると、どうも Chrome は AltGr の入力を Ctrl+Alt の入力に変換しているようだ。つまり modifier としての keydown イベントが先行して 2 回発生し、それは Ctrl と Alt である。また、modifier ではないキー入力の属性として ctrlKey と altKey のフラグが両方立っている。一方 wasavi は、Ctrl 併用のアルファベットはコントロールコードの入力とみなしていて、すなわちそれがまずい。modifier キーに対する条件判断を、Ctrl が押されている場合、ではなく Ctrl「のみ」が押されている場合にする必要がある。そのように判定を厳しくすることで keydown イベントのキャンセルを回避し、keypress イベントを発生させるのである。

というわけでそういうふうに直した。このへんの仕組みはとても新鮮でとても興味深い。というのも、キーボードから直接入力できない文字に関しては、東アジア圏の人間の場合は IME を通して変換するのが通常だからである。

ところでこれに関連して、あるいは偶然のタイミングなのか、issue #52 も上がっている。これは vim の digraph は wasavi で使えないんだけど! というものだ。digraph というのは、input モード時に [cci]^K[/cci] を先行して 1 バイト文字を 2 文字入力することで別の文字に変換する機構のことだ。このへんの変換機構は RFC にまとまっている。

wasavi では vim の digraph は実装しない。というのは、そういうのは個々のアプリケーションではなく、もっとシステムワイドな層で設定すべきものだと思うからだ。たとえば ~./Xcompose とか、あるいは Windows なら AutoHotKey とかでなんとかすればいいのである。

Strange Firefox

自動テストを Firefox でも行ったところ、不思議なことに Firefox でだけ失敗するテストがある。[cci]:[/cci] で行入力モードへ移行し、[cci]^V[/cci] でリテラル入力状態にした後、[cci]escape[/cci] キーを押して状態を途中でキャンセルするというものだ。そして、正しくリテラル入力状態がキャンセルされ、最後の行入力の状態から変化がないことをテストする。

これが Firefox でだけ失敗する。行入力バッファがクリアされてしまうのである。つまり勝手に空っぽになってしまうのだ。なにこれ。というかなんだこれ。

調べてみたところ、[cci]escape[/cci] キーを押されたことで発生するハンドラの中では、最後までバッファが勝手に変更されることはない。そのハンドラが終了した直後に勝手にクリアされてしまうようだ。つまり wasavi のコードがバグってるということではなくて、Firefox の動作がなんかおかしい。

もともと、wasavi 側ではキー入力ハンドラ内で 1ms 後に発生するタイマを仕掛け、そこで擬似的にバッファ変更イベント的なものを発生させている(これはインクリメンタルサーチで必要なのだ)。Firefox で動いている場合に限り、そのタイマの中でつじつまを合わせることにした。具体的にはキー入力ハンドラの最後にバッファの内容を data- なんとか属性にコピーしておき、タイマの中でその属性値と現在のバッファの value が違っていたらコピーしていた内容で復帰させるのである。もちろんこんな処理は本来は不要で、あまりに何をやっているのか意味不明なので、さすがにくどいコメントも付けておいた。

もしかしたら、このバグなのかもしれない。しかし件のバグは RESOLVED なので関係ないのかなあ?

とこのように、Firefox でだけ発生するバグを見つけると大変なのである。なにしろ拡張を華麗にデバッグするナウいデバッガがないので、古き良き…ではない古く悪しき printf デバッグでのたうちまわるしかないのだ。Firebug とかは、あくまで web ページのスクリプトのデバッガなので拡張は対象外なのです。

正確に言えば、最近の Firefox は Firebug ではないビルトインのデバッガを備えるようになっている。これが本当に最近の話だから困る。しかもそのデバッガが拡張をデバッグ対象にできるようになったのはさらに最近の話なので本当に困る。ちょっと実装するの遅すぎですよ。

ちなみにそのデバッガであるが、ステップ実行するのに 1 〜 2 秒かかったりステップ実行の単位がソースコードの 1 行ではなく副作用完了点的な何かなのか、複数回ステップ実行しないと停止行が進まなかったり、デバッガが動いている間 CPU を 80% 近く酷使してファンを全力全開にしてくれたり、まあなんといいますか、まだまだまったくもって人様にお出しできない状態なのだった。ひどいかひどくないかで言えば、かなり滅茶苦茶にひどい。

Firefox といえば OSS のブラウザの雄でであり、かつ長い歴史を持った老舗であるわけなのだが、デバッガ周りに関してなんでこんなにもあまりにも質の低いことをしているのか、ちょっとどういうことなのかよくわからない。どういうことなの。どういうことなんでしょうか。

improve functionality tests

機能が増減したわけではないのだけど、モードの移行処理をけっこう大きく書き換えた。こうやって大胆に変更できるのは、Selenium による自動テストがあるからだ。山ほど書き換えようがテストに通ればそれでおっけーなのである。

というわけでテストしたところぜんぜん通らないのでちくちくと修正した。地味な作業といえば地味なのだけど、これを行わないとリリースできない。

ところで、wasavi のコードが変更されるにつれ、テストコードも併せて進化させる必要がある。例えば wasavi 側ではキーイベントをデキューによって処理することで先行入力できるようにした。つまり、様々な理由でキー入力をブロックしている状態でもとりあえずその間に行われたキー入力を覚えておいて、ブロックが解除されたら再生するようになった。一方テストコードでは、先行入力できない前提でキーストロークを割と細かい単位で実行完了待ちのウェイトを行うようになっている。これはもはや不要だ。テスト用のキーストロークを一気にどばっと送出して構わないし、一気に送出しても正しく動作するのが想定された仕様だ。というわけでそのへんもいじった。

 * * *

式の評価を行う [cci]=[/cci] レジスタを vim から移植したわけなのだが、これはレジスタだ。ということは、ex コマンド
:put [register]
において指定することもできるはずだ。指定すると、どうなるんだろう? と vim で試してみると、式入力状態には遷移しない。単に最後に評価された結果が参照されるだけだ。ほう。なんか、一貫してない動作な気がするが、vim がそうやってるならと wasavi でもそのとおりにした。

 * * *

ググってみると、reddit で wasavi についてのストーリーが立っていたらしい。これはちょっとうれしい。個人的な感覚だけど、ソフトウェアが広く知られるにはとりあえず slashdot と reddit と hacker news で取り上げてもらうのがスタートラインなんじゃないかと思うわけです。そのうち reddit はクリアしたのだ。一方で、hacker news でも何度かストーリーが立っていたようなのだけど、どれもまったくコメントがつかないまま流れているようなのでこれはまだクリアしたとは言えない。ちなみに slashdot ではまったく wasavi の話題はない。

関係ないけれど 4chan 系とかでもたまーに wasavi が話題になっているようなのだけどググりにくいのでちょっとよくわからない。

going back and forth #4

モードの遷移についていろいろと修正した。また、サブモードを取り除いた。これにより、キー入力を実際に処理する [cci]processInput()[/cci] も大きく書き換えた。この関数ではモードに従った分岐をしつつそれぞれの処理を行うのだが、それに先立ってサブモードを捌きつつ適宜つじつまを合わせる処理をしていた。今回の修正によりそれが必要なくなった。ついでに、モードごとの処理を別の関数に分割して関数テーブルにより実行を振り分けるようにした。

going back and forth #3

vim で [cci]:help “=[/cci] することで [cci]=[/cci] レジスタの詳細な説明を得られる。

それによれば、単に四則演算式というわけではなくて、vim script の式っぽい。つまり [cci]:help expression[/cci] で見られるような感じのそれである。そして、式の結果として期待されるものの型は本質的に文字列だ。数学的な結果はむしろ異端で、型変換する必要がある。これは移植するには大規模すぎるし、また時期尚早すぎる。wasavi はスクリプティングをまだ実装していない。

算術演算に絞って言えば、四則演算以外の演算子としては

  • %: 除算の剰余

が足りないので、それは実装することにしよう。

すなわち、wasavi の [cci]=[/cci] レジスタで受け付ける式は、

expression :=
addsub := ( "+" )*
( "-" )*
muldiv := ( "*" )*
( "/" )*
( "%" )*
factor :=
"(" ")"
signed-numelic-literal :=
"+"
"-"

という文法になる。ここで [cci]numeric-literal[/cci] は、ecmascript での数値表現に準じることにする。つまり整数表現、小数点表現、指数表現、および 0x なんちゃらの形式である。

というわけで、そういうパーサを書いた。四則演算式のパーサ自体としては特に何か言うこともない素朴なものだ。

going back and forth #2

vim には特殊なレジスタ [cci]=[/cci] がある。これは本当に特殊だ。まず読み込み専用である。そして、このレジスタを指定した時点でモードが 1 行入力状態に移行する。

なにを入力すればいいのかというと、四則演算式である。式を入力すると、その計算結果がレジスタの値になる。したがって、例えば

"=99*99P

などというキーストロークによりバッファに [cci]9801[/cci] が入力される。ちょっとした電卓代わりになる。

これを wasavi に実装したい。

実装にあたっては、いろいろな角度から考えることがある:

  • 四則演算式と書いたが、他の演算子もあるかもしれない。vim のそのへんの仕様を確認する必要がある
  • 前の記事で、リテラル入力時に backspace を使えるようにした。これは四則演算式の入力もリテラル入力の一種として実装するつもりだったからで、つまり長い式を入力するために最低限それくらいはないと辛かろーということなのだが、やはり vim と同様に 1 行入力状態を挟んだほうが良いかもしれない。その方が、すでに存在する様々な編集のためのショートカットが使えるからである。特に、クリップボードから式を貼り付けたい場合にリテラル入力にもクリップボード操作の処理を実装することはない(クリップボードからの読み込みは非同期なので面倒くさいのである)
  • モードの遷移に関して、wasavi のそれはちょっと良くない実装がある。例えば、ex コマンド入力中に [cci]^R=<式>[/cci] などというキーストロークで計算結果を得たい場合、1 行入力状態からさらに 1 行入力状態に移行することになる。この時、遷移前の行の状態(内容とカーソル位置)を覚えておいたり、逆方向に遷移した場合はそれらを復帰させる必要がある。現在の wasavi の実装はそれを考慮していない
  • モードの保持に関してもうひとつ良くない実装は、モードの下にサブモードがあることだ。これはレジスタ名の入力、リテラル入力、コンソールのプロンプトなどがある。これはサブモードにする必要は本来はない。不必要に複雑になっているのでこれも併せて修正したい

going back and forth

input モードでは [cci]^V[/cci] を前置することでリテラルを入力することができるが、vim 同様、特殊なシーケンスがいくつかある

このうち、リテラル入力中に backspace が使えるようにした。

しょーもない機能追加ではあるのだけど、重要なのである。なぜなら [cci]^R =[/cci] をポートしようとしているのだが、やはり同じようなインターフェースにするからだ。