input モードでは [cci]^V[/cci] を前置することでリテラルを入力することができるが、vim 同様、特殊なシーケンスがいくつかある。
このうち、リテラル入力中に backspace が使えるようにした。
しょーもない機能追加ではあるのだけど、重要なのである。なぜなら [cci]^R =[/cci] をポートしようとしているのだが、やはり同じようなインターフェースにするからだ。
input モードでは [cci]^V[/cci] を前置することでリテラルを入力することができるが、vim 同様、特殊なシーケンスがいくつかある。
このうち、リテラル入力中に backspace が使えるようにした。
しょーもない機能追加ではあるのだけど、重要なのである。なぜなら [cci]^R =[/cci] をポートしようとしているのだが、やはり同じようなインターフェースにするからだ。
しかしながら、やはり [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 のサイズだけである。
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 を起動したいという要件も確かにあり得るのだ。
開発に従って readme の内容も改めることがある。
改めてできるだけ客観的に wasavi の readme を読んでみたのだけど、気が狂ってるレベルで vi を実装していてちょっと作った人頭おかしいと思った。
ex コマンド [cci]s[/cci] について、とあるバグを直そうとしたら根本的に書き直さないとダメっぽかったので、根本的に書きなおした。
s コマンドの内部は、javascript の global と multiline フラグを立てた RegExp のインスタンスについて exec() を連続して呼び出すという処理がコアになっている。exec() を呼び出しながら、マッチした箇所について置換を行い、lastIndex プロパティを適宜調整し、次に備えるという 1 パスの処理になっている。
大体の場合はこれで上手く行くのだけど、ゼロ幅にマッチする正規表現の場合に上手く行かない。そして、それを直そうとすると、今まで上手く動いてた部分がこぞっておかしくなるという排他的な状態になってしまうのであった。
これを根本的に直すために、まず exec() のループと置換ループを別に分ける 2 パスの処理にすることにした。exec() で得た各マッチ位置の情報を配列に押し込んで覚えておく必要があるので、若干富豪的ではあるのだが、まあ、だいじょうぶだいじょうぶ。
その他見つけた細々としたバグも直した。
たとえばゼロ幅でマッチする正規表現、たとえば [cci]:s/a\?/!/g[/cci] というコマンドを実行すると、カーソル行の各文字の前後に [cci]![/cci] が挿入されるが、行末には挿入されない。少なくとも vim では挿入されない。これは割と奇妙な動作だ。
つまり、
0123
とある行に前述のコマンドを実行すると、本来ならば
!0!1!2!3!
となっておかしくないはずであるが、少なくとも vim では
!0!1!2!3
となる。行末の [cci]![/cci] がない。
この通りに動作させるには、マッチした位置が改行で、かつマッチした正規表現にメタ文字 [cci]$[/cci] が含まれない場合は置換を行わない、という例外を加えればいい。いいのだけど、後者はとても難しい。javascript ではとても難しい。マッチに使用された正規表現のパスを得る方法が javascript の RegExp にはない。
issue 41 で、gu/gU オペレータの実装が要望された。
これは vim の機能だ。そして、2 文字ではあるが、y/d/c といったオペレータと同様の動作をする。つまり、オペレータに後続するモーションとセットとなり、オペレータを入力したカーソル位置から、モーションによって移動したカーソル位置までの領域に対して、gu は小文字化、gU は大文字化を行う。
実は issue で指摘されるまで vim にそんな機能があることを知らなかったのだけど、たしかに便利そうなので、wasavi にも移植することにした。
すでに g プリフィクスの機構は(あんまり素敵なものではないにしても、とりあえず)移植済みである。特に g プリフィクスのオペレータとしては gq がすでにある。その仕組みに合わせて実装すればいい。素敵ではない点と言うのは、つまり g プリフィクスが付いているかどうかの判断を現時点では各オペレータのハンドラで行っているということだ。これは将来的には修正されなければならない。
それはそれとして、まあ gu/gU の実装をした。
vi は a から z の名前付きレジスタを持っていて、ユーザは自由にそれを使うことができる。
vim はこれを拡張し、A から Z までのレジスタの指定も許す。これは新しいレジスタが 26 個増えるわけではなく、[a-z] レジスタに対する特別な別名として振る舞う。たとえばレジスタ A を指定してヤンクした場合、それは a レジスタがもともと持っていた内容への追記を表す。これは有用な機能なので、wasavi でもそういうふうに動作する。
では、A から Z までのレジスタを指定しつつ、それを読み出す動作を行わせた場合どうなるんでしたっけ?
とりあえず、vim では読み出しの場合は A レジスタは単に a レジスタの内容を返すようである。書き込み時のような特別扱いはない。しかしこの透過性って必要なんだろうか? 読み出し時にも特別扱いしていいのではないか。
というわけで、wasavi では、[A-Z] レジスタからの読み出しにおいては、以下のように振る舞うようにした。
これ以外の [A-Z] レジスタは、単に空文字列を返す。
関数の頭で [cci]Array.prototype.slice.call(arguments)[/cci] としているしている箇所がけっこうある。Arguments を配列に変換する関数を書き、それを使用するように修正。
変数 runLevel を削除。これは何かと言うと、vi コマンド(含 ex コマンド)の実行のネストレベルを表していた。例えば、ユーザーが直接入力したキーストロークから実行される vi コマンドがレベル 0 として、そこからさらに実行されるキーボードマクロとしての vi コマンド群はレベル 1 である。
レベルを区別する理由は、レベル 0 の場合は適宜ビジュアルな要素(つまりステータスラインの状態とか、カーソルの形状とか、カーソル近辺までスクロールさせる必要があるとか)を更新しなければならないのに対し、まとめて実行されるレベル 1 では本質的にはそれを考慮する必要はないということである。レベル 0 に戻った時点でつじつま合わせをすればいいのである。それにより処理の高速化が図れるはずであるのだけど、とりあえず削除した。ただレベルの概念自体が不要というわけではない。将来的には何らかの形で再実装するかもしれない。
wasavi の iframe の最小幅については従来 320px と定義していたが、最小高についても 240px を下限とした。
DOM Range オブジェクトを使用した際、使い終わったあと detach() していたのだけど、これをやめた。そもそも存在理由が微妙なメソッドではあったが、DOM4 で公式に何もしないものと定義された。もしかしたら Presto Opera では超重要な意味を持っているのかもしれないが、まあいいよね。
キー入力周りをいじる。これは https://github.com/akahuku/wasavi/issues/35 への対応である。
wasavi のキーボード周りはちょっと…いやかなり複雑で、keydown や keypress イベントに引っ掛けたリスナで即 vi 的な処理を行っているわけではない。
まずキーボードマネージャというものがあり、この中で keydown と keypress、および composition events をまとめて扱っている。特に Opera 用の composition events エミュレータ層はかなりうんざりさせられる複雑さである。キーボードマネージャ内でいろいろなつじつま合わせを行った後、改めてキー入力イベントを発生させる。
次にマップマネージャがあり、これがキーボードマネージャからのイベントをリスンしている。これの仕事はもちろんマップの展開である。適宜マップの展開を行った後、改めてキー入力を発生させる。
最後に wasavi の process() がそれを受け取り、モードに応じた処理、abbreviation の処理、対応するカッコの点滅処理、および自動的なフォーマットの処理などなどを行う。などなどである。この他にもいろいろな処理がある。
複雑なのは、あいまいなマップの展開で次の入力に応じて展開を決定する場合や、クリップボードの読み書きを行う場合や、ex コマンドの実行完了を待つ場合や、スクロールコマンドの処理完了を待つ場合など、動作が非同期に行われる箇所があるのだ。これら全てに正しく応答しないといけない。
一方、dot コマンドやキーボードマクロを任意のタイミングで実行するためにキー入力をエミュレートしなければならない部分もある。実はこちらで非同期処理への対応が不完全だったり同じような処理が分散しているのである。このへんを直したい。
まずキーボードマネージャ内にデキュー、つまり double ended queue を設けて、発生したキー入力イベントをこれに押し込んだ後、FIFO 的にイベントを発生させるようにした。キューではなくデキューなのは、マップの展開が行われた場合、展開されたシーケンスはキューの最後ではなく先頭に押し込めなければならないからだ。
それからデキューのロック機構を設けて、非同期処理が間に入った場合は適宜ロックを行うようにした。
ただ、ファイル I/O 系の ex コマンドの実行を途中で中断する場合などに [cci]^C[/cci] を押した場合は、まさに SIGINT 的な特別扱いをしないといけない。
キーボード周りとは関係ないけれど、先に挙げた issue では map コマンドの右辺でダブルクオートを書けないよ、というバグが指摘されている。実はこれはバグではなく、POSIX では map コマンドの引数中のダブルクオートは [cci]^V[/cci] でエスケープしなければならないのである。vim はダブルクオートをそのまま map の引数として扱う(vi compatible にすればたぶん vi 通りの動作をするのだろう。未確認)。そんなわけで同時にそのへんも直した。
というわけで、wasavi に組み込んだ。
設定ページに javascript を書く。関数の名前に意味があり、編集可能な要素上では [cci]edit_[/cci]、そうでない場合は [cci]view_[/cci] から始める。次に、修飾キーがある場合は [cci]s_[/cci]、[cci]c_[/cci]、または [cci]sc_[/cci] を続ける。最後にキー名を付ける。ここまで指定した情報がそのままその関数が実行されるコンテキストを示している。
ここで指定した関数が実行された場合、それはページ上のスクリプトによるキーハンドリングより排他的に優先される。たとえば Twitter や slashdot など、[cci]j[/cci]、[cci]k[/cci] キーに特別な機能をもたせているサイトは少なからずあるが、[cci]function view_j[/cci] などを定義した場合はページ側の対応するキーバインドは無視される。