record key strokes #2

キーストロークの記録は、:map と間接的な関係がある。両者とも、キーストロークを文字列化したものを扱う。さてキーの中には、一般的なコードポイントに対応しないものがある。例えば Page Up とか F1 とかね。そういうキーをどう文字列化するか? というところが現在の wasavi の実装ではまだ不完全なのだ。

vim では、特殊なキーは CSI (Control Sequence Introducer, 0x9B) と呼ばれる特別なバイトを前置して区別している、ようだ。wasavi でも同じような形式にしよう。私用領域の U+e000 を 使用する。たとえば delete キーに対応するキーストロークは、文字列化すると “\ue000<delete>” となる。

:map では同じような、しかし逆の変換を実装することになる。:map の rhs で “<delete>” と書いたら、内部的には “\ue000<delete>” として扱わないといけないのだ。

record key strokes

@ コマンド、:@ コマンドはすでに作ってある。これらは、指定のレジスタの内容をそれぞれ vi コマンド、ex コマンドとして実行する。
ではコマンド列をどうやってレジスタに突っ込むかというと、素の vi では適当な行にコマンド列を打ち込んで yy することになる。

一方 vim では、q{register} コマンドが新設されている。これはエディタの状態を「キーストローク記録中」にする。この状態のキー入力は記録される。この状態を抜けるには、単に q を押す。すると記録されたキーストロークが {register} に保存される。この機能はつまり、インタラクティブにキーストロークを記録するものだ。

というわけでこれを実装する。

Opera and DOM3 Composition Events

こんなことを書いてそろそろ 1 年経つのだけど:

DOM3 Composition events、いつのまにやら Chrome(と、おそらく Safari)、IE9、Fx9 と実装済みのブラウザが揃っている 2011 年末。

さて Opera は何年後くらいに実装するの?

# そしてまさかの実装されない、というオチ

まさか 1 年たってもまったく状況が変わってないとは思わなかった。強いて変わった点といえば、Opera は 12.50 で DOM3 Events に準拠するようになるらしいということ。ただし開発版 Opera である Opera Next の最新ビルドでも、Composition Events はまったく実装されていない。さすがに最終的に「実装されない、というオチ」はないと思うけど。ない……よね?

ちなみに、Composition Events をサポートしていないとしても、たとえば google でやってるように、思い切りタイマーをぶんまわして textarea#value をポーリングすれば確かに同じようなことはできる。できるのだけど、ただ 1 つだけ問題がある。IME を通した入力サイクルを考えてみると、

  • 仮入力
  • 変換して候補を選択
  • enter を押して確定

の繰り返しだ。しかし実際は、確定として enter を押すことはそれほど多くない。(少なくとも MS-IME は)変換後の候補選択状態で、選択のための特定のキー以外のキーを押せば、暗黙的に確定が行われて仮入力状態に移行する。この暗黙的な確定が発生した正確なタイミングをタイマー方式では認識できない。

そういうわけで、wasavi には Composition Events がないブラウザのためのフォールバック機構は入れていない。つまり今のところ Opera で IME を通した入力は正しく動作しない。どうしたものかなあ……。

a difficult AMO #5

AMO の中の人の手元でもサーバーエラーになるそうなので、bugzilla に登録してもらった。

うーんこちらで直接登録してもよかった気がするが。まあでも中の人……というか「the Add-ons Developer Relations Lead for Mozilla」という肩書きのおかげか、あるいはアサインとかも最初から割り当てられてたせいか、すぐに修正されたようだ。あとは、そのパッチがレビューされて実際に適用されるのを待つだけということかな。

長かったなあ。

関係ないけど、wasavi の preliminary / full review をしてくれた方は Pentadactyl の開発者の一人ということで、vi 繋がりなのかと思ってたら、単にものすごく多くのアドオンをレビューしているだけだった。なーんだ。

beauty is relative #2

relativenumber を実装してみたが、カーソルから上の領域の行番号が負の符号付きになる件。これはこれでしかたないかなーと思っていたところ、コメント欄で間接セレクタを使えばいいと教えていただいた。

あー、間接セレクタね! うんうん間接! 間接……間接セレクタってなんだっけ?

バカにしないでくれる!? 知ってるわよそれくらい!!(ググりながら)

というわけで無事 vim の relativenumber と同様の見た目になった。ありがとうありがとう。

beauty is relative

「relativenumber、いいよね」「いい……」

vim の機能を全て知っているわけもないが、知ってる機能の中で、初めて知ったとき最も衝撃的だったのは relativenumber かもしれない。relativenumber 好きだ、大好きだ。お前が好きだ、お前が欲しい!

というわけでこっそりと wasavi へ移植する。

内部的には、wasavi の行番号は css のカウンタだ。したがって、操作できるのは counter-reset、counter-increment くらいで、それほど自由度があるわけではない。そういう環境下でカーソル行を 0 とした相対的な行番号ぽくするには、バッファの counter-reset を -(カーソル行位置+1) で初期化する。カーソルの上方向は負の符号付きになってはしまうが、これでだいたい目的は達せられる。

しかし実際に作ってみるとやっぱり負符号が気になる……。

handling wrapped rows #2

ということで、表示行単位でカーソルを上下に移動させるコマンド gj、gk を実装した。また、オプション jkdenotative を追加した。これがオンの状態では、単なる j、k が表示行単位で移動し、gj、gk は物理行単位で移動することになる。そして重要なことに、jkdenotative の初期値はオン、にしようと思う。

それにしても jkdenotative ってものすごく覚えにくいな。なんかいいのないかな。

さて vim では gj、gk のほかに g0、g^、g$ なんかもある。というかもっと他にやたらある。全部はいらないので、g^ と g$ くらいは作るかな。これらも jkdenotative の影響を受けることになる。とても重要。

handling wrapped rows

バッファ内において「物理行」を改行で区切られたそれぞれの領域とする。1 つの物理行を表示した際、ビューより長いものは折り返される。このとき折り返された、折り返しポイントで区切られたそれぞれの領域を「表示行」とする。

j コマンドや k コマンドは、物理行単位で移動する。ものすごく 1 行が長い物理行のうえでこれらのコマンドを実行すると一気にスクロールが行われて面食らう。

vim では、gj とか gk コマンドが用意されていて、これらは表示行単位でカーソルを上下に移動させる。vi にはない。

次はこれを持って来たいという話。

この機能を作る準備として、1 行内のその折り返しポイントを得ることを考えなければならない。できれば一発で取れる夢のような API があればうれしいのだが、ない。getClientRects() あたりが惜しいのだけど(これは各表示行についての座標情報を返す)、折り返しポイントは返してくれない。

うーん地道にループ回すしかないかな。

ところで vim で表示行を使う際、[cci]noremap j gj / noremap k gk[/cci] すると便利ですよ奥さん! 的な話題になりがちだと思う。でもこれは割とバッドノウハウなんじゃないのかなー。ぶっちゃけていうと j/k は常に表示行単位で動かすようにしても別に困らないと思うんだけど。移動単位を切り替えるオプションくらいは新設する必要があるかもしないが。

the heavenly school #4

LocationChange イベントのハンドラで、document に対して wasavi の起動と終了イベントをリスンさせているのだが、これはなかなかに乱暴だ。つまり、リスンさせっぱなしなのだ。

本来なら、タブが切り替わる直前に document からハンドラを取り除きたい。しかしそういうイベント(BeforeLocationChange みたいなの)はないようだ。LocationChange イベントは実質的に AfterLocationChanged だ。

したがってその結果、タブが切り替わるごとに同じイベントハンドラを何度も登録するというちょっと餡子の足りない動作になってしまっている。もちろん同一のイベントタイプ・ハンドラ・キャプチャフラグで何度 addEventListener を呼び出しても、登録は高々 1 度だけに収束するという DOM の仕様が前提なわけだけど。

Invoking addEventListener (or removeEventListener) repeatedly on the same EventTarget with the same values for the parameters type, listener, and useCapture has no effect. Doing so does not cause the event listener to be registered more than once and does not cause a change in the triggering order.

それと、現状では wasavi が終了すると共に [cci]passAllKeys = false;[/cci] しているのだけど、これだと wasavi に関係なく常にパススルーで動かしたいサイトで誤動作することになる。wasavi が起動する時点での passAllKeys の値を保存しておいて、wasavi 終了時はその値に復帰させるとかすればよいのだが、passAllKeys が実質グローバルなことを考えるとそれでつじつまが合うのか不安だ。いや合わないケースが確実にある。

この周辺をバグ報告されても、vimperator の仕様がそうなんだよーとおあしす運動(「おれじゃない」「あいつらじゃないの?」「しらない」「すんだこと」)を開始せざるを得ない。どうしよう。

the heavenly school #3

対象ページの window は、content オブジェクトが持っているみたいだ。なので、content.document instanceof HTMLDocument のときにそれに対してごにょごにょすればいい。

なお前の記事では MutationObserver を使うつもりでいたのだが、なにか使い方を間違っているのか、要素を追加・削除しても一切ハンドラが呼ばれない。しかたがないので MutationObserver を使うことはあきらめ、wasavi 側で開始・終了時に CustomEvent を document に対して発行するようにした。このイベントを vimperator のプラグイン側でリスンし、適宜 passAllKeys を書き換える。

ところで気になるのは、wasavi を適当なタブで起動し、そのまま他のタブに切り替え、また戻ってきた場合。この場合、LocationChange イベントのハンドラで、wasavi が起動しているかを見て passAllKeys を制御することになる。しかしここで passAllKeys を直接触らざるを得ないのは vimperator の間違った設計だと思う。LocationChange 内で passAllKeys を触るプラグインが複数あったら破綻してしまう。ハンドラ側では passAllKeys = true としたい、というリクエストを出すだけにして、ハンドラの呼び出しが全て完了した後に呼び出し側でリクエストの有無によって passAllKeys を制御する構造にならないかな。

まあそれはさておき、とりあえず編集したいときに wasavi を起動して終わったら即抜けるっていう使い方をする限りは動くようになった。やったね。