Line on a cursor

wasavi-cursorline

Some essential changes

wasavi 0.6.387 を github に上げた。このビルドには、いくつかの重要な変更がある。

まず、Firefox 版が Github 上で起動しない問題。前述の通り、これは wasavi をページ上に出す際の iframe に割り当てる src のドメインの問題だ。Github の CSP が data: スキームを許可していない。

そこで、iframe の src へは [cci]about:blank?wasavi-frame-source[/cci] というアドレスを割り当てることにした。当然中身は空っぽなので PageMod を経由して中身を差し込む。ちなみに about: スキームも CSP は許可していないのだが、結局 CSP の記述を実際にどう利用するかはブラウザが判断することであり、いろいろな例外があるわけで、すなわち about: 系は許されるのであり。このへんは実際に試してみないとわからないのだった。

実は Github で起動しない理由は他に、Hotkey モジュールに定義したショートカットよりもページスクリプトが消費するキー入力イベントが優先されるというのもある。しかし Firefox 版で Hotkey モジュールを使った理由というのは、それがページスクリプトよりも優先されるはずだからなのであり、実際その当時はその通り動作した。しかし現在はそうではないのである。しかし、まあ、Firefox というものは、大体において、そういうものなのでしょう、きっと。これは単に Chrome や Presto Opera と同様、keydown のフック機構を経由してキー入力を監視する方式に切り替えればいい。このくらいでは泣かないぞ。

* * *

insert モードと replace モードを総称して input モードと呼ぶ。従来はそれを実現するために、カーソル位置に重ね合わせる形で透明な textarea を配置し、それが受けたイベントを wasavi へ転送するというわけのわからない複雑さを備えた謎の機構を経由していた。なんでこんなめんどくさいことをしているのかといえば、すなわち Presto Opera のためだ。あの苦労してこさえた擬似 composition イベントは、textarea がフォーカスを持っている状態じゃないと正しく生成できないのである。

しかし、まあ、Presto Opera でありますよ。そろそろ引退させたほうがいろいろな方面が幸せになるのですよ。

というわけで input モードでは div 要素の contenteditalbe 属性を利用するようにした。これにより結構な量のコードをばっさり削除できるのである。おまけに入力のオーバーヘッドも減り、Selenium のテストも速くなり、word wrapping なども CSS での指定をそのまま使用でき、悪いものはないのである。強いて言えば前述の通り Presto Opera で IME を通した入力ができなくなるが、Presto Opera 上で wasavi を使っている人間なんて地球上にただ数人いるかいないかであり特に問題ではない(とはいえ最低限おかしくならない程度の処理は入れたが、まあ完全ではない)。

Some topics

  • [cci]cfx run[/cci] で wasavi のデバッグ用 Firefox を起動する。そうすると console への出力がそのまま端末にリダイレクトされるのだが。Firefox 自身が持っている chrome:// なんちゃらドメインの各種 javascript ソースから発生するログがあまりに多すぎる。たいていは strict モードにまつわるエラーで、中には本気でこれバグってるんじゃないの? 系のエラーメッセージもあったりする。オブジェクトの存在しないプロパティにアクセスしてたり、null のオブジェクトに触ってたり。

    ちょっと、ひどい。直したほうがいいんじゃないですか。

  • Blink Opera の wasavi では、ページフックスクリプトをいうものを有効にしていたのだが。すべて除去した。

  • Firefox 版の wasavi では、たとえば github 上で動作しない。これはなぜかといえば github のサーバから送られてくるレスポンスヘッダ内の CSP が原因だ。Firefox 版の wasavi は data スキームの内容を src にした iframe だ。しかし github のページの CSP は data スキームの iframe を許可しない。

    これ、どうしたものかなー。もしかしたら解決の糸口になるかもしれないものを一応メモ。

    Firefox Addon SDK: Loading addon file into iframe
    http://stackoverflow.com/questions/21082162/firefox-addon-sdk-loading-addon-file-into-iframe

Up and Down Quickly

github 上の wasavi の issue で、[cci]j[/cci] [cci]k[/cci] が重い というものがあるのだが、いくらなんでもそれらのキーの機能が重かったら即気が付くわけで、従ってまったく手元で再現せず手を付けられないでいた。

しかし、ふともしかして [cci]:set jkdenotative[/cci] してるんじゃないの? と聞いてみたところその通りであるらしい。なるほど。すべての謎が解けた。

本来 [cci]j[/cci] と [cci]k[/cci] は、改行までの 1 行(物理行)を単位としてカーソルを上下に移動させる。一方、[cci]gj[/cci] と [cci]gk[/cci] は、レンダリングされた結果における折り返し行を単位とする。ここで、もしユーザーが改行までの 1 行が十分に長いテキストを頻繁に編集するのなら、[cci]j[/cci] [cci]k[/cci] は折り返し行単位で移動したほうがカーソルの移動は圧倒的に楽である。

そういうわけで、[cci]jkdenotative[/cci] オプションは [cci]j[/cci] [cci]k[/cci] と [cci]gj[/cci] [cci]gk[/cci] の機能を交換するか否かを指定する。まあ [cci]:map[/cci] でもいいんだけど。

で。この折り返し行単位の移動が確かに重いのである。従って [cci]:set jkdenotative[/cci] とすると、[cci]j[/cci] [cci]k[/cci] でのカーソルの移動がめちゃ重くなるわけだ。

よしわかった。やってやろうじゃん。100 倍速くしてやる。

折り返し行単位でのカーソルの移動を行うためには、折り返し行のそれぞれの先頭位置が、物理行のどの位置に対応するかというマッピング情報が必要になる。wasavi における物理行はすなわち div 要素なのだけど、しかし DOM にはそのマッピング情報を得る手段がない。そういうわけで、かなり力技を使わざるを得ない。物理行のある位置 n が属する次の折り返し行の先頭の物理位置を得るには、n から物理行の末尾に向かってループする。ループ内では 1 文字ずつカーソル位置を span でくくり、getBoundingClientRect() を呼び出す。これが前回の呼び出し位置と top プロパティが異なっていればそれが折り返し行の区切りになる。従来のやり方はこれであり、そして、すんごく重い。getBoundingClientRect() が重い。そういうわけでできるだけこのメソッドの使用回数を抑えるか、あわよくばゼロにする必要がある。

getBoundingClientRect() が重いのは、それがページ全体における要素の位置を計算するからだと思う。display:static な要素の場合、その要素の前のすべてを再計算しないといけない(レンダリングの結果でそれぞれの要素がそれを保持していないんだろうか? という気もするが)。

それはそれとして、とにかく別の方法を使わなければならない。ループを用いるのは同じだが、先立ってカーソル位置をまず span でくくり、これを left とする。また、カーソル位置から物理行末尾までの文字列を別の span でくくり、これを right とする。

ループでは right の先頭を left の末尾へ追加するという処理を行い、その都度 left の offsetHeight をチェックし、ループ直前のそれよりも増えていれば、折り返し行の区切りに達したことになる。offsetHeight はその要素だけで再計算が完了するはずなので、getBoundingClientRect() よりは速い。

また、ループは 2 つのステージで構成するようにする。第 1 ステージでは、ループカウンタの増分は 1 ではなくループごとに 2 倍して、ガンガン先に進める。この場合、「次の」折り返し行を超えた先へ到達してしまう可能性があるので、そのようである場合(初期 offsetHeight + 1 行分の lineHeight よりも offsetHeight が大きければ)増分を 1/2 してやり直したりする微調整は必要。

いずれにしてもカーソル位置の次の折り返し行に達した場合は第 2 ステージへ移り、増分を 1 に固定して正確な折り返し行の区切り位置を見つけ出す。これによりオーダーは O(n) から O(log2 n) になる、はず。

やってみたところ、修正前は [cci]10j[/cci] するのに 1600ms ほどかかっていたものが、160ms 程度まで短くすることができた。うーんさすがに 100 倍速は無理だった。また、折り返し行の区切り位置を見つけ出すループに関しては、たとえば単純ループが 38 回かかるところを 14 回で済んでいる。これはちょっと多いが、第 1 ステージは 6 回程度で抜けているのでオーダーの計算としては合ってる。第 2 ステージは更に最適化できるかもしれない。

というわけで折り返し行単位のカーソル上下移動が実用的になったと思う。最初からそう組んどけよ! と言われればはいその通りですと言わざるを得ないのだが。

Input mode revised

キーボード周りで Qeema を利用することにしたが、それにより若干従来のキーボードマネージャからインターフェースが変わった部分がある。この際なので、input モード周りのキー入力のハンドリングのおさらいをしたい。

モードが insert / replace へ遷移することにより、カーソル位置にまず I ビームを表示させる必要がある。この I ビームは、textarea 要素のそれを流用する。ちなみに input モード時に textarea 要素を流用するのは I ビームの見た目がほしいからではなく、composition events は、activeElement が編集可能な要素じゃないと発生しないからである。

textarea 要素のその位置は、折り返し行単位のカーソル行の上にぴったり重ね合わせるようにする。また、ここでカーソル行の前後に 2 つの span を生成する。

1 つは、折り返し行の先頭から I ビームまでの文字列で、これを leading と呼んでいる。leading span は visibility:hidden にする。その代わり、textarea の value 要素を leading で初期化する。これにより、カーソル行のカーソル位置と input 用 textarea の I ビームの位置が(だいたい)一致する。だいたいというのは…理屈の上では、margin やら padding やらを合わせれば wasavi の行の div と textarea で揃えればぴったり重ね合わさるはずが、ブラウザによってはそうならないものもあるのである。ただまあ、ずれるとしても数ピクセルなので大したことではない。ただ I ビームの先頭位置を を textarea の内容で調節するのはもう 1 つの問題がある。つまり色分けできないのである。現状では wasavi は色分けしないのでいいのだけど、将来必ず困ってしまうのであるのであるが、それはさておき。

もう 1 つの span は I ビーム位置に挿入する空の span で、compositionupdate や compositionend イベントで送られてくるプリエディット文字列を入れる場所だ。このようにして入力用 textarea とプリエディットの状態を同期させることによりインライン入力っぽいような動作をさせている。実際に IME から一気に送出された文字列を挿入する処理は、IME 経由以外の方法で入力された文字と共通する。この仕掛けは qeema の恩恵による。

composition events を正しくリスンし正しく相応しい処理を行うのは、アジア圏の言語のためだけではない。Compose キーを併用した文字の入力は、たとえば Firefox では composition events を経由するので(それを踏まえて qeema でもそうしてある)、より広範囲の言語に対応するために必須の処理だ。

ところで Project Spartan なる新生 IE は、javascript ベースの拡張をサポートするそうだ。Presto Opera のサポートを漸次的に終了すると同時に、Spartan の動向をウォッチする必要があるかもしれない。

Some spices for Qeema

Qeema によってキーボード周りを wasavi と赤福プラスで共通化したのだけど、キーボードの扱い方は両者でかなり異なる。

赤福プラスはいくつかのキーボードショートカットを定義しているが、それ以外に関しては手を付けない。一方 wasavi は全てのキー入力をリスンし、全てのキー入力を消費し(つまりデフォルトアクションをキャンセルし)、自前の処理を行う。この中で、全てのキー入力というのは composition events によって発生したものも含まれる。

ところが、composition events というのは単なる通知イベントなので、DOM の定義上はキャンセルできないのである。なので、qeema 側で compositionstart の時点での入力文字列を覚えておき、compositionend で復帰するようにしている。

ついでに、Presto Opera での composition events エミュレーションをできるだけシンプルな構造に書きなおした。まあ、いまさらこれをブラッシュアップしたところで誰も幸せにならないんですけどね……。

for Firefox #12

というわけで赤福プラスが Firefox でも動くようになって、また常用ブラウザ自体も Firefox に移行できた。まだメモ機能の代替品を見つけていないのだけど。

それはさておき、赤福プラスは wasavi とソースレベルではいろいろなものを共有している。例えば Kosian(バックエンドのエクステンションの抽象化ライブラリ)、Qeema(キーボード周りのライブラリ)、フロントエンドのエクステンションラッパー、Brisket(ビルドスクリプト群)と言ったものだ。

これらもいろいろと更新したわけなので、wasavi のほうに戻り、それらの共有部品を最新のものに置き換えてみたい。また、wasavi も Firefox 上で常用することになる。これはとっても大事だ。Firefox 版の wasavi というのは、これはこれでけっこう苦労して作ったのだが、まっさらのプロファイルでひと通りテストが通ればそれでおっけーということにしている。しかし世の Firefox で実際のところ拡張をひとつも入れてないまま稼働してるのなんて早々ないだろう。もうちょっと使い込んだプロファイルで常用しないといけない。

つまり、この度の常用ブラウザの移行は丁度いいチャンスなのである。

qeema

赤福プラスはいくつかのショートカットキーを受け付けるので keydown や keypress イベントをリスンするのだが、もう言うまでもないのだが、このあたりのイベントのブラウザごとの相違は壮絶なものがあるのである。そのへんを吸収している、wasavi が持っている keyManager を持ってきたい。件の、使用するショートカットを表明する処理もこれに入れてしまいたい。

というわけで、wasavi からそれを抜き出して別のリポジトリに仕立てた。キーボードのマネージャーなので略して qeema とした。ひき肉。
https://github.com/akahuku/qeema

ただひとつ問題があるのである。これを赤福プラスや wasavi に取り込む際、単に git のサブモジュールを使うわけにはいかない。というのは、qeema.js 自体は素の javascript ソースなのだが、これを Presto Opera で inject script として扱うには、ヘッダを付けなければいけない。


// ==UserScript==
// @name frontend of akahukuplus
// @include http://*.2chan.net/*/*.htm
// @include http://*.2chan.net/*/*.htm?*
// @include http://*.2chan.net/*/res/*.htm
// @include http://*.2chan.net/*/res/*.htm?*
// @exclude http://dec.2chan.net/up/*
// @exclude http://dec.2chan.net/up2/*
// ==/UserScript==

同じソースでも wasavi に取り込む際は @include 節がまた違った指定になるわけで、いずれにしてもサブモジュールそのままの状態で使えるわけではない。

うーんなにかうまい方法がないのかな。

execution #3

ex コマンドの executor をごそっと書き直した。また、global コマンドの内容もそれに合わせた。

基本的には想定したコードそのままなのだが、ひとつ思いがけないことがあった。個々の ex コマンドはアドレスを前置することができる。このとき、絶対指定以外の行番号は現在のカレント行から相対的に決定される。それで、現在は ex コマンドのソース文字列をパースする段階でアドレスも評価し、実際の行番号を得ているのだが、一方で global コマンドの入れ子コマンドはいったん生成した中間形式を繰り返し再利用する形になっている。つまり、アドレスの情報が最初に評価した状態から変更されないのである。アドレス情報だけは実行ごとにダイナミックに生成しないといけない。

とりあえずあんまり綺麗ではない手法で何とかしたが(ex コマンドを実行した後にアドレス情報を破棄する。再度実行した時アドレス情報が無効なら再生成する)、これはパースの段階では単に文法的な誤りがないことだけを判断し、評価は実行直前まで遅延させたほうがいい。これはそのうち直す。

次は s コマンド。

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 コマンドオブジェクト的なものを参照できないといけない。

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