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 を起動したいという要件も確かにあり得るのだ。

lunatic

開発に従って readme の内容も改めることがある。

改めてできるだけ客観的に wasavi の readme を読んでみたのだけど、気が狂ってるレベルで vi を実装していてちょっと作った人頭おかしいと思った。

refreSh!

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 にはない。

Up and Low

issue 41 で、gu/gU オペレータの実装が要望された。

これは vim の機能だ。そして、2 文字ではあるが、y/d/c といったオペレータと同様の動作をする。つまり、オペレータに後続するモーションとセットとなり、オペレータを入力したカーソル位置から、モーションによって移動したカーソル位置までの領域に対して、gu は小文字化、gU は大文字化を行う。

実は issue で指摘されるまで vim にそんな機能があることを知らなかったのだけど、たしかに便利そうなので、wasavi にも移植することにした。

すでに g プリフィクスの機構は(あんまり素敵なものではないにしても、とりあえず)移植済みである。特に g プリフィクスのオペレータとしては gq がすでにある。その仕組みに合わせて実装すればいい。素敵ではない点と言うのは、つまり g プリフィクスが付いているかどうかの判断を現時点では各オペレータのハンドラで行っているということだ。これは将来的には修正されなければならない。

それはそれとして、まあ gu/gU の実装をした。

CAPITAL REGISTERS

vi は a から z の名前付きレジスタを持っていて、ユーザは自由にそれを使うことができる。

vim はこれを拡張し、A から Z までのレジスタの指定も許す。これは新しいレジスタが 26 個増えるわけではなく、[a-z] レジスタに対する特別な別名として振る舞う。たとえばレジスタ A を指定してヤンクした場合、それは a レジスタがもともと持っていた内容への追記を表す。これは有用な機能なので、wasavi でもそういうふうに動作する。

では、A から Z までのレジスタを指定しつつ、それを読み出す動作を行わせた場合どうなるんでしたっけ?

とりあえず、vim では読み出しの場合は A レジスタは単に a レジスタの内容を返すようである。書き込み時のような特別扱いはない。しかしこの透過性って必要なんだろうか? 読み出し時にも特別扱いしていいのではないか。

というわけで、wasavi では、[A-Z] レジスタからの読み出しにおいては、以下のように振る舞うようにした。

  • B レジスタ: ブラウザのユーザーエージェント文字列を返す
  • D レジスタ: 現在の日付時刻の文字列を返す
  • T レジスタ: wasavi が属するページのタイトルを返す
  • U レジスタ: wasavi が属するページの URL を返す
  • W レジスタ: wasavi のバージョン文字列を返す

これ以外の [A-Z] レジスタは、単に空文字列を返す。

Some fixes

関数の頭で [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 では超重要な意味を持っているのかもしれないが、まあいいよね。

keyboard handling

キー入力周りをいじる。これは 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 通りの動作をするのだろう。未確認)。そんなわけで同時にそのへんも直した。

tiny hooks #2

key-hooks

というわけで、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] などを定義した場合はページ側の対応するキーバインドは無視される。

tiny hooks

Linux にも Blink Opera がリリースされて以来、結構使っている。Presto Opera と同時に立ち上げて、気分によってどちらかを使っている的な状態だ。あるいは、ナウそうなサイトは積極的に Blink Opera を選択しているかもしれない。Presto Opera では操作不能に陥るくらい重いサイトが Blink Opera ではまあまあサクサクだったりするからだ。

とはいうものの、もちろん Blink Opera より Presto Opera のほうが優れている点も少なくない。例えばキーボードまわりのカスタマイズ性だ。Presto Opera はブラウザ上だろうがアドレスバー上だろうがあらゆるキーボード入力をカスタマイズできるが、Blink Opera にそういう機能はない。

ちなみにそれほどキーバインドを変えまくっているわけではなくて、

  • [cci]c-h[/cci] でページを戻るか、戻れなければページを閉じる
  • [cci]space[/cci] でビューポートの高さの半分 だけスクロールする
  • [cci]j[/cci]、[cci]k[/cci] で 1 行ずつスクロール
  • [cci]h[/cci]、[cci]l[/cci] でタブを切り替え
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりを textarea に対して定義
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりをアドレスバーに対して定義
  • textarea 上での [cci]c-h[/cci] をカーソル前の1文字削除にする

この程度である。かわいいものである。これをなんとかして Blink Opera に持ってきたいのだけど、どうすればいいだろうか。

まず考えられるのは、そういうキーボードコンフィグ系のエクステンションがすでにあるよね、なくても Chrome 版をむりやり動かせばいいよね、ということだ。しかし、問題は、[cci]c-h[/cci] なのである。これの場合、単にキーに機能を割り当てるのではなく、条件判断が必要になる。そういうことを許してくれるエクステンションはあるだろうか? たとえば Keyconfig は許可してくれるだろうか?

しかしどうやら Keyconfig はそういうことはできないようだ(できるのならごめんなさい)。YakShave ならできるかもしれないが、何やらローカルに web サーバを立てる必要があったりとなんだかめんどうそう(ごめんなさい)。

というわけで、もしかして、wasavi に組み込んだほうが早いんじゃねーの!? という気分になりつつある。

play a sound #3

sounds

wasavi の起動時に効果音を出すようにしてみた。

そうすると、当然「そんなのいらねーです!」というリクエストが来ることが考えられる。したがって音を出すかどうかを設定できるとうれしい。

ところで起動時の効果音というのは、wasavi が起動する前に鳴らすわけなので、つまり wasavi 本体が管理する設定([cci]:set[/cci] で制御できるもの)とは別になる。そこで、オプションページで設定することになる。オプションページに効果音のリストとそれを鳴らすかどうかのチェックボックス、そしてボリュームの設定を置いた。

ところで従来は、wasavi が出す音といえばエラー時のビープ音であった。それは [cci]bellvolume[/cci] で設定できたのだけど、ビープ音も前述の仕組みで鳴らすようにしたので bellvolume は obsoleted になった。