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] をポートしようとしているのだが、やはり同じようなインターフェースにするからだ。

allow resizing from wasavi #2

しかしながら、やはり [cci]columns[/cci] と [cci]lines[/cci] を書き込み可能にするというのはちょっと無理のある仕様だ。たとえば端末の中で立ち上げた vi から [cci]:set lines=25[/cci] とかやっても、端末のサイズが変わるわけではない。それと同じことだ。もちろん gVim であればそういうサイズ変更もできるが、元の textarea と wasavi の関係性というのはやはり端末とその中で動く vi に近い。

そういう中で、あえてサイズ変更可能にするならば、元の textarea から wasavi へ向かっている強制力の鎖を断ち切る手段も導入する必要があるよね、ということを issue の中でまとまった。というよりも、そのへんまでしかまとまっていない。どういう仕様が正しいのかいまいち自分の中でイメージがわかない。

と入っても放置するわけにも行かないので、とりあえず組んでみた。まずオプション [cci]syncsize[/cci] を導入する。これがオンである場合、wasavi のサイズは textarea のサイズに従う。スクリプトにせよ、ウィンドウの端をユーザーが掴んだにせよ、textarea 内のリサイズハンドルをドラッグしたにせよ、何らかの理由で textarea のサイズが変更された場合、即座に wasavi のサイズもそれに追従する。[cci]columns[/cci] / [cci]lines[/cci] への書き込みは可能だが、それは textarea と wasavi のサイズを同時に変更する。

一方、[cci]syncsize[/cci] をオフにすると、[cci]columns[/cci] / [cci]lines[/cci] への書き込みによって変更されるのは wasavi のサイズだけである。

allow resizing from wasavi

issue #36 への対応。

wasavi が持つオプションの中に、[cci]columns[/cci] と [cci]lines[/cci] というものがある。これは、wasavi の実行時におけるあるタイミングでの桁数(正確に言うと、wasavi のスクロール領域のピクセル幅を、選択されたフォントで文字 [cci]0[/cci] を描画した際のピクセル数で割った整数部分)、および行数(正確に言うと以下略)を示している。

そもそも、wasavi のサイズは、寄生元の textarea のサイズに依存していて、そこには明確な主従関係がある。つまりまず textarea のサイズがあり、wasavi はそれに従うのみなのである。したがって、[cci]columns[/cci] と [cci]lines[/cci] は実質的に読み込み専用であり、新たな値を上書きすることはできるが、意味を持たない。何の副作用も起こさない。

件の issue は、これを書き込み可能にして、書き込んだときはその値で wasavi のサイズを更新してほしいというものだ。これは、一見簡単そうなのだが、しかしよく考えてみると前述の主従関係を根本的に破壊する変更なのでかなり困ったのである。

しかしただ拒絶するのもどうか。textarea のサイズによらず、常に同じサイズで wasavi を起動したいという要件も確かにあり得るのだ。