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 になった。

play a sound #2

SDK のドキュメントをつらつらを眺めてみると、HiddenFrame なるものを見つけた。これは要するに、バックエンド側で保持することのできる iframe のようなものらしい。HiddenFrame 内に構築される window はごくごく普通のそれであり、Audio コンストラクタも持っている。

そういうわけで、Firefox においては、HiddenFrame 経由で Audio 要素を得ることができた。子供の window で生成したオブジェクトをそのまま親の側で使用できてしまうのがかなり気持ち悪いが、たぶん、きっと、だいじょうぶ。

play a sound

wasavi を実行中、音を出す場合がある。つまりビービー鳴らすモードを持つ vi と同様、必要なときにエラーベルを出す。もし「この音うるせーよ!!」と思われる場合は、ワークアラウンドとして [cci]:set bellvolume=0[/cci] とすれば、とりあえずミュート状態にはなる(例えばこの方がやっている方法だ)。将来的にはビジュアルベルに切り替わったりする気遣いをするかもしれない。

さてこの時に出す音は Audio 要素を使っていて、フロントエンド側で鳴らしている。音源は wasavi 起動時にバックエンドから送られてくる ogg または mp3 データである。

しかし、これは割と無駄なのではないか。複数のタブで wasavi を動かしているとして、各タブがもともと同じデータである音源のコピーを個別に抱えている状態だ。現状は 1 秒未満しか発音しないビープ音なのでデータ自体は数 KB だ。したがってものすごく容量を食っているというわけではないが、やっぱり無駄だ。

これを、Audio 要素をバックエンドで保持するようにして、フロントエンドで音が必要になったら発音リクエストを投げるだけにすれば、そういう無駄は解消できる。ということでやってみて、Opera と Chrome では思ったとおりになったのだけど、Firefox が問題だ。Firefox の Add on SDK 上のバックエンドは、Window ではない不可思議なサンドボックスオブジェクトがグローバルになっていて、つまり Audio 要素をバックエンド側で使えないのだった。

こういう場合、たいてい [cci]Cc[/cci] や [cci]Ci[/cci] といった Firefox 特有の黒魔術をごにょごにょして何とかなったりするものなのだが(Kosian では XHR や Blob や FormData についてごにょごにょしている)、黒魔術はしょせん黒魔術であり、あんまり使いたくない。なにしろ、AMO にアップする際の機械的チェックにすら「おい黒魔術使ってんぞ、気をつけろよな!」などと言われる始末である。禁止されているわけではなく、「気をつけろ」なのがミソではあるのだが。

さて、サウンドプレイヤについてもこの黒魔術を通して Audio 要素っぽいものを得ることはできるようなのだけど

var {Cc, Ci} = require("chrome");
var sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
var uri = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService)
.newURI(self.data.url(...), null, null);

sound.play(uri);

これで得られる nsISound は Audio 要素とは全然異なるものであって、たとえばボリュームの指定とかはできないようなのである。

Positioning

文書のスクロール量によりtextareaが半分隠れている場合などを考慮して初期の位置決めにおけるクリッピングを真面目にやるようにした。これにより gmail などで wasavi 自身がビューからはみ出すことがなくなった。

ということでスクリーンショットでも貼ろうかと思ったのだけど、なかなか gmail のページはおいそれとお外に出せないプライベートな情報満載なのであった。

ところで、最近 github に上がった issue はほぼ対応でき、ぼちぼちエクステンションストア上の安定版(ということになっている) wasavi も更新する時期に来ている。ただ設定ページでショートカットキー書くのたるいよー! 入力したキーストロークを自動的にショートカットキー記述にしてよー! 的なアレが残っていて、これは安定版にはタイミング的に入らないかもしれない。なぜこれを残していたのかといえば、設定ページ自身のスクリプトの構成も変えたので、そのへんが固まってからにしようということだったのである。