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。

wasavi 0.5.244 released

リリースした。

サマリ

変更点
  • 入力モードのさまざまなバグを修正
  • 入力モードに ^W、^U、^D、^T、^@(ctrl+space) を実装
  • f/F/t/T がカタカナ、ひらがな、および漢字の読みに対応する latin-1 の文字を認識するようにした
  • Firefox 版の Add on SDK を 1.12 に更新
ダウンロード

vi and Japanese #5

やはり fftt(f/F/t/T 系の横方向の検索機能を一言でなんと言い表してよいか思いつかず、そのまま fftt と呼んでいる)の辞書を非同期にしかアクセスできないのはちょっと不便なので、サイズ的に大したこともないのを前提に、wasavi 起動時にバックグラウンドからフロントエンドに転送することにする。

現状では、辞書は ArrayBuffer とか Uint8Array とか、そういう系で保持している。これをバックグラウンドとコンテントスクリプト間でやり取りできるのか? というと、Opera 以外はできない。なぜか Opera はできる。Opera なのに……。

しかしこのへんはおそらく、html5 のメッセージング API でも議論中のトピックだった記憶が。つまり仕様もころころ変わりそうなので、無難にバイナリ文字列か単純な配列に落とし込む方がいいかもしれない。

あるいは、コンテントスクリプト側から明示的にエクステンションのアーカイブ内の辞書を読み込んでもいい。chrome.extension.getURL() や、opera.extension.getFile() が使えると思う。しかし Firefox のコンテントスクリプトはそのへんの自由度がかなり低い上にコンテントスクリプトに対してどういう API が公開されているかすらドキュメント化されていない適当さなので 3 ブラウザとも似た方法で、としにくい。というと、この点から考えてもやはりバイナリ文字列か配列ということになる。

というわけで、そうした。

unawares

別に頼んだわけでもないのだが、softpedia を初めとする、海外のいくつかのアプリ紹介サイト(日本で言うところの vector とか窓の杜みたいなサイト?)に wasavi が登録されていた。ほー。

しかしそれと関係あるのかないのかわからないのだけど、ここ数日、このブログにくる spam とか、怪しげな勧誘メールの量がめちゃんこ増えてるんですけど。仕事中に大量にメールするのやめてください。