Opera is bad #3

Opera で擬似的に composition events を実装する場合、様々な処理を最終的に keyup ハンドラ内で行うことになる。これはひとつ奇妙な振る舞いを引き起こす。キー入力は、基本的には keydown -> keypress -> input -> keyup の流れでイベントを発生させるのだが、しかし高速にキーボードを捌いたりすると、複数のキー入力の各サイクルがオーバーラップしてしまうことがある。

といっても Opera に問題があるわけではなく、つまりキーボードの打ち方の問題で、例えばあるキーを離す前に次のキーを押したりした場合だ。そうすると keydown -> keydown -> keyup -> keyup みたいな感じでイベントが発生する。

これを考慮しないと、正しく composition を認識できない。ので、考慮するようにした。

 * * *

keydown および input イベントの引数 e を蓄えておくキュー、keyup イベントの引数 e を蓄えておくキューを保持する。前者のキューの 1 項目は、

{
keyDownEvent: { ... },
currentString: '',
inputEvent: [ ... ]
}

てな具合。keydown イベントハンドラで、keyCode が 229 だったら IME に対する入力とみなし、ハンドラ本体の本来の処理はせず、キューに必要な情報を埋めるだけにする。keyup イベントハンドラで、keydown キューの長さ – 1 == keyup キューの長さではない場合、つまり keydown イベントが複数回連続して発生した状況である場合は、keyup キューに必要な情報を埋めるだけにする。

keydown キューと keyup キューのバランスが正しい場合は keyup ハンドラ本来の処理を行うが、それに先駆けて溜まったキューを処理する。イベントの引数 e をもって keydown、input イベントハンドラを直接呼ぶ。

 * * *

composition 文字列が更新されるごとに直前の文字列の長さを覚えておくようにし、暗黙的確定のようであれば直前の compositon 文字列のほうを採るようにした。つまり暗黙的確定にも対応した。

しかしだからといって Opera でも IME に対応しました! とはとても言えない。だいたい IME に対する入力時の keyCode、229 って何なのかさっぱりわからない(IE に準じているっぽいが)し、明示的確定では input イベントが連続して 2 回、暗黙的確定では 3 回とかどこにも文書化されてもおらずそもそもバグの産物なのかも知れず未来のバージョンでもそれが維持されるかもおぼつかない。ダメダメすぎる。Opera が悪い。

Opera is bad #2

しかしそうも言っていられない。Opera が対応している keypress、keydown、keyup、input イベントでなんとか擬似的に composition events を再現できないか。

ということでそれっぽくできた。一応このエントリも、今まさに wasavi で書いている。しかし問題は山積している:

  • 結局のところ、例の暗黙的確定には対応できない。
  • 上記のイベントの、Opera 12.12 上での動作にきわめて依存した構成なので、安定してるともいえない。

つまり人様にお出しするにはちょっと……という出来である。どうしよう。

ちなみに擬似 composition events の実現手法だが、Opera で IME の仮入力は、keydown、input、keypress の順でイベントが発生する。このとき、input イベントは場合によっては複数回発生する。仮入力中は、keydown 時の keycode は 229 で固定。keyup 時の keycode は本来押されたキーのコードが入っている。

IME がアクティブな時の様々な動作の特徴は以下の通り:

  • 明示的確定: 仮入力後変換候補を出している状態で、enter キー押下により確定する動作。keydown(229)、input が 2 回、keyup(13)
  • 暗黙的確定: 仮入力後変換候補を出している状態で、変換に使用しないキーの押下により暗黙的に確定する動作。たとえば「漢字」と変換した状態でそのまま続けて HENKAN と打つと、「漢字」は自動的に確定され、かつ確定に使用した文字「H」が次の仮入力に持ち越される。この動作は composition events でなければ正しく認識できない。keydown(229)、input が 3 回、keyup(暗黙的確定に使用した文字のキーコード)
  • 仮入力の取り消し: 取り消しには 2 パターンの方法がある。escape キーを押すか、backspace で仮入力文字を全削除するか。前者は keydown(229)、keyup(最後に入力した文字のキーコード)。つまり input イベントが発生しない。後者は発生する。さらに、IME がアクティブで、かつ仮入力文字列が空かどうかで判断する
  • その他の仮入力、変換操作: keydown(229)、input が 1 回、keyup(最後に入力した文字のキーコード)

keydown、input、keyup をリスンし、keydown 時のキーコードおよび input が発生した回数を覚えておく。上記の場合分けはすべて keyup のハンドラで行う。

その他の仮入力、変換操作で、IME がアクティブ(これは実際の IME の状態ではなく、javascript 上のフラグ)でなければ、compositionstart イベントを発火し、IME がアクティブ状態であるとする。それから、compositionupdate イベントを発火する。

2 種の確定動作で確定された文字列を取り出し、compositionend イベントを発火し、IME が非アクティブ状態であるとする。

Opera is bad

UAX #14 に準拠した段落の再フォーマットを実装したということは、つまり日本語を初めとする latin-1 以外の文字を含んだ段落もそれなりに処理してくれるということなのだが、さて wasavi と日本語の入力という点を考えてみると、要するにというか例によってというか DOM3 Composition Events の話になるのだが、一応実装してあるものの、あまり詰めているわけではない。これはいまだに Composition Events を実装していない Opera が悪い。

Composition Events の他に、エクステンションからシステムのクリップボードを読み書きすることも Opera 版 wasavi ではまだ不可能だ。Opera が悪い。

辛うじて、Opera も input イベントは実装しているので、擬似的に compositionend イベントをエミュレートすることはできなくもない。しかし前にも書いた気がするが、これだと仮入力して変換候補を出している状態で、変換に使用する以外のキーを押して暗黙的に確定をした際の正確なタイミングをキャッチできない。Opera が悪い。

あまりエラソーなことも言いたくないが、Opera の中の人は IME を使用する言語圏のことはどうでもいいの? やる気ないの? 特に Opera の日本支社のヒトは何をしているの? と思わざるを得ない。みすみすニーズを逃している。Opera が悪い。

Composition Events が実装されるのを待って、かれこれ 3 年になろうとしている。そういえば XMLHttpRequest#overrideMimeType が実装されるまでにも相当待ったのを思い出した。もうちょっと開発者の声に耳を傾けていただきたいものだ。

Opera が悪い。

join to the world #7

だいたいできた。Selenium で全体的なテスト中。

 * * *

テストおわたー

Tests run: 626, Failures: 0, Errors: 0, Time elapsed: 3,323.127 sec

改めてみるとテスト項目が626というのはあまり多くない気がするが、ひとつのテストで複数の assert を行っていることは珍しくない。assertion は計2678件になっている。github によると、現在の wasavi の様々なソースの sloc は 35000 程度ということである。このソースの規模とテストケースの規模の比が適正かどうかはよく分からないが、個人的には最低限度は辛うじてクリアしてるかな? という程度だ。

join to the world #6

いよいよ gq コマンドを実装する。

g プリフィクスがついているが、基本的にはオペレータだ。つまり gq{motion} または gq{range-symbol} という形式で使う。もちろん、gqq という形式もありうる。ちなみに vi 使いなら誰でも知ってることであるが、オペレータとモーションが同じであるというのは、内部的には “_” モーションが実行されたことと等価である。

ただし gq コマンドには他のオペレーションと決定的に違う点がいくつかある。d や y といったオペレーションは基本的に字・行単位なのに対し、gq は操作の単位がパラグラフなのだ。これはものすごく重要だ。

それから、コマンドが完了したとき、カーソルは連続して gq コマンドを実行できるように、処理したパラグラフの次行へ置かれる(ただし gw コマンドは違う)という点も興味深い。

vim では ops.c でやっている。

 * * *

「もちろん gqq」などと書いたが、実は結構面倒だ。gqq では先行する gq がオペレータになるので、オペレータ = モーションとなるのは正確には gqgq と打たなければならないのだ。gqq だと、q コマンドをモーションとして実行することになる。しかし q コマンドはキーボードマクロの記録開始コマンドであり、オペレータではないがオペレータが空でなければ実行できない。

どうするかというと、q コマンドのハンドラの先頭でオペレータが gq だったら何もせずに返るという実に泥縄的な方法で逃げた。ダメすぎる……!

 * * *

textwidth を 1 以上の値にした場合、以下のような感じで垂直線をガイドとして表示するようにした。
wasavi_vertical_line
エディタとしては非常にありがちな絵。この垂直線とか、カーソル行の水平線とかを実装し始めるとなんだかいかにも、そこそこ機能を備えたテキストエディタっぽくて逆に面白みがないな。よくわからない我侭だが。

join to the world #5

ひとまず input モードでの自動改行が動くようにした。改行できない場合は、無理に改行せずに長いままにするようにした。

あとは overwrite モードでの整合性をとるのと、Unicode.org 謹製のテストパターンがあるのでこれを通すようにしよう。6311 パターンくらいある。それから、gq コマンドを実装する。

とりあえずこれで、一通り pure vi の機能を実装し終えたということになるのかな。だいたい 1 年かかった。週末に一気に書き上げたという伝説はともかく、Bill Joy さんはせいぜい数ヶ月で(しかも BSD 本体やら Pascal コンパイラと並行して)vi を書いたというのだからおそろしい(参考: Bill Joy’s greatest gift to man – the vi editor および vi – Wikipedia, the free encyclopedia)。

 * * *

とりあえずテストを通してみたところ、87.59% 通った(テストデータは基本的な実装に加えて UAX #14 の 8.2 Examples of Customization の Example 7、つまり数値文字列に対する拡張を施した上での状態を前提にしている。したがって基本的な実装のままでは 100% にはならない)。

しかしなんかしょうもないバグが残っているような気がしないでもないのでもう少し追ってみることにしよう。……うわーん、案の定。修正したら 95.5%。

残りは上記の Example 7 の件だろう。たぶん。そういうことにしてこの状態で進める。

 * * *

といいつつまだやっている。CR や LF などの処理を付け足したりして 99.5% まできた。残りの半分は、Contingent Break Opportunity (CB)、具体的には U+FFFC Replacement Character にまつわるテストケースなのだが……これが UAX #14 を見てもよくわからない。CB は LB1 で処理することになってたり、文脈に応じて適当に扱うことになっていたり、UAX #14 を通してどうするか明示されてるわけではない。うーん。U+FFFC を含んだテストケースでは、FFFC については LB1 のルールに従う、みたいな事が書いてあるのだが LB1 の定義にも U+FFFC の取り扱いは明示されていない。うーん。

まあいいか?

join to the world #4

順を追って考えてみる。

Lorem ipsum dolor sit amet,
consectetur adipisicing elit,
sed do eiusmod tempor
incididunt ut labore et
dolore magna aliqua.

というような、長い 1 行がある。次に [cci]:set tw=30[/cci] として、[cci]Aa^[[/cci] する。すると、”a” が打ち込まれる前に、行の先頭からカーソル位置までの折り返されている行全体が処理され、適切な位置に改行が挿入される。……ということは、今まで、自動的な改行挿入はカーソル位置から逆向きに走査すればいいと考えていたが、そうではなさそうだ。ループ処理が必要になりそうだし、行の先頭から正順に走査することになる。

  1. まず行の先頭から、”L” の幅を算出し、line break 情報を得、textwidth を超えてないかを判定する。超えてなければ、次は “Lo” で……というループを繰り返す。
  2. いずれ “Lorem ipsum doloer sit amet, con” あたりで textwidth を超える。取得した line break 情報のうち、最後の改行挿入可能ポイントで改行する(このとき、ポイントの左側の空白文字列を削除する処理が必要かもしれない)。今、ループ中で着目している位置は 2 行目の con の次の文字だ。これは挿入が行われる位置(5 行目の最終桁)と同じかを判定する。同じではなければ、1. へ戻る。ちなみに挿入が行われる位置の絶対的な行、桁は改行を挿入するごとに変化するので、それを考慮する必要がある

ということになる、と思う。

2. で、「最後の改行挿入可能ポイント」が存在しない場合、つまりその行に改行できるポイントがない場合はどうするか。

  • textwidth を満たす位置で強制的に改行する。改行禁止であっても
  • 改行しない

さて、どちらがいいのか。vim では formatoption で選択できる(たぶん)ようだが。

join to the world #3

UAX #14 を実装した。

UAX #14 が提示するサンプル実装は、基本的にはさまざまなスクリプトに対して最大公約数的な動作をする仕様だと思う(実際、最後の方にさまざまなカスタマイズを行う例とかが載っている)。が、一方で UAX #14 は Unicode のアップデートに従ってけっこう拡張されているようなので、とりあえずサンプル実装のまま組んだ。また、Unicode 関連の関数やクラスが増えてきたので、くくりだして独立したソースにした。

これを利用して、textwidth に応じて自動的に改行する処理を組み込む。

[cci]:set tw=10[/cci] とした上で

abc def ghi

と打つと、i を打った時点で自動的に改行され

abc def
ghi

となるわけだ。内部的には、i を打ってバッファが更新される直前に line break の情報を取得する。すると 4 文字目、および 8 文字目のスペースが INDIRECT_BRK とマークされる。これはつまりその文字の直後で改行してもよいということだ。したがって、line break 情報を後ろからサーチし、最初に見つけた改行可能マークで実際に改行すればよい。

ここで

  • 改行できない場合。たとえば

    abcdefghijk

    などと打った場合はどうなるのか?
  • Undo 情報はどのような構造になるべきなのか?

を明確にしておく必要がある。これは vim のソース(edit.c)を見るしかないかな。

join to the world #2

textwidth に値を設定すると、input モード時、ある文字 C を入力しようとしたときにその行(の先頭からカーソル位置までの部分文字列の表示上の幅に C の表示上の幅を足した値)が textwidth を超えるであろう場合、先んじてリフォーマット処理が行われ、その後に実際に C が入力される。リフォーマット処理は、カーソル位置から後方に改行挿入可能位置を走査し、最初に見つかった箇所で行を分割する。

このときとても面倒なのはつまり、改行挿入可能位置の走査というやつだ。Latin-1 だけで考えるならば、アルファベットの列の中では改行してはいけないとか、せいぜい数種の特定の約物の前では改行してはいけないとか、その程度のことに気をつけるだけでよかった。が、javascript で動く web ページ上のプログラムとして見るとどうしても Unicode のことを考えなければならない。

めんどくさいねぇ Unicode。ノムラススムばりに時空を超えて何度も我々の前に立ちはだかってくる。

で、Unicode の文脈で、文の中の改行挿入可能位置を算出する処理というのは UAX #14 にまとめられている。その仕様に沿って書けばいい。くらくらする長さの仕様だが。

join to the world

vim の textwidth オプション、および gq コマンドまわりに対応するものを実装したい(vi の wrapmargin はたぶん作らない)。まずは、とりあえずその前フリとして、J コマンド(と :join)と Unicode の関係を考える。おそらく gq の処理を行う過程で join を何度も呼び出すことになるので。

J コマンドは、カーソル行と次の行を連結する。このとき、以下のプロセスで連結が行われる:

  1. 連結される行の先行する空白文字を削除する
  2. 連結される行の現在の状態が空行ならば、以下の 3. ~ 5. の処理を飛ばす
  3. カーソル行の末尾が空白文字であるか、または、連結される行の最初の文字が ‘)’ であるならば、何の加工もせずに 2 行を連結する
  4. そうではなく、カーソル行の末尾が ‘.’ であるならば、2 行の間に 2 つの空白文字を挟んで連結する
  5. そうではない場合は、2 行の間に 1 つの空白文字を挟んで連結する

wasavi では、3. が ‘)}]’ のいずれか、4. が ‘.!?’ のいずれか……に拡張してあるほかは、この通りの動作をする。これ、Unicode のさまざまな文字について考えるとどうなんですかね。Unicode な文字で考えると 3. はむしろ一般カテゴリ Pe (a closing punctuation mark) に属する文字、というのがふさわしいだろう。Pe に属する文字は Unicode 6.2.0 で 71 字。

一方 4. は、つまり文の終わりを示す約物ということになる。これは Unicode でいうと LineBreak.txt で STerm (Sentence Terminator) と定義されている文字に対応する。こちらは 83文字。

それから、空白を挟んで連結するかどうかは、言語(というより書記法的なもの?)によると思う。例えば漢字とその亜種(ひらがな、カタカナ、あるいはボポモフォみたいなの)どうしが隣り合う場合、空白は不要だ。おそらく隣り合う両方ではなく、片方だけが漢字とその亜種だったとしても不要だ。ところで「漢字とその亜種」っていう種別は Unicode にはない気がする。East Asian Scripts としてまとめられてるもの(漢字、ひらがな、カタカナ、注音符号、および全角・半角形あたり)を全部突っ込めばいいのかな。

 * * *

上記の修正を行った。漢字部分は、

'\u3100-\u312F', // Bopomofo
'\u31A0-\u31BF', // Bopomofo Extended
'\u4E00-\u9FCF', // CJK Unified Ideographs (Han)
'\u3400-\u4DBF', // CJK Extension-A
//'\u20000-\u2A6DF', // CJK Extension-B
//'\u2A700-\u2B73F', // CJK Extension-C
//'\u2B740-\u2B81F', // CJK Extension-D
'\uF900-\uFAFF', // CJK Compatibility Ideographs
//'\u2F800-\u2FA1F', // CJK Compatibility Ideographs Supplement
'\u2F00-\u2FDF', // CJK Radicals / KangXi Radicals
'\u2E80-\u2EFF', // CJK Radicals Supplement
'\u31C0-\u31EF', // CJK Strokes
'\u2FF0-\u2FFF', // Ideographic Description Characters

/* Korean */
'\u1100-\u11FF', // Hangul Jamo
'\uA960-\uA97F', // Hangul Jamo Extended-A
'\uD7B0-\uD7FF', // Hangul Jamo Extended-B
'\u3130-\u318F', // Hangul Compatibility Jamo
'\uFFA0-\uFFDC', // Halfwidth Jamo
'\uAC00-\uD7AF', // Hangul Syllables

/* Japanese */
'\u3040-\u309F', // Hiragana
'\u30A0-\u30FF', // Katakana
'\u31F0-\u31FF', // Katakana Phonetic Extensions
//'\u1B000-\u1B0FF', // Kana Supplement
'\uFF65-\uFF9F', // Halfwidth Katakana
'\u3190-\u319F', // Kanbun

/* Lisu */
//'\uA4D0-\uA4FF', // Lisu

/* Miao */
//'\u16F00-\u16F9F', // Miao

/* Yi */
'\uA000-\uA48F', // Yi Syllables
'\uA490-\uA4CF', // Yi Radicals

という感じ。LisuMiao は、Latin 文字ベースの中国語? という不思議なソレなのでよくわからないがとりあえず対象外。ところでハングルはどうなんだろう。単語間にスペース入れて記述するような気がするけど。

次は textwidth。