brushing up, input mode #5

input モードのショートカット ^T、^D をなんとかしよう。

これらの機能、実はあまり利用したことがない。とりあえず ^D の posix の仕様は以下の通り:

^D
^D は、行指向コマンド(/, ?, :, !)によって開始されたテキスト入力モードでは特別な意味を持たない。またこのコマンドは、ブロックモードの端末ではサポートされてなくてもよい。

カーソルが字下げ文字に続いていない、または字下げ文字、’0′ または ‘^’ に続いていない場合:

  1. カーソルが 1 桁目にある場合、^D は無視され何も起こらない
  2. そうではない場合、^D は何の意味も持たない<(^D そのものが挿入される)

最後に入力した文字が ‘0’ であった場合、カーソルは 1 桁目に移動しなければならない。
そうではなく最後に入力した文字が ‘^’ であった場合、カーソルは 1 桁目に移動し、加えて、次の入力行の自動字下げレベルは現在行の(もともとの)字下げ量をもたらした行から同様にもたらされなければならない。

そうではない場合、カーソルは shiftwidth 単位で前に戻される。

現在行: 変更されない。
現在桁: ^D の前に ‘^’ または ‘0’ があった場合は 1。そうでない場合は (column -1) -((column -2) % shiftwidth)。

という感じ。結局のところはインデントを制御するショートカットということだ。なお vi では ^D はカーソル行が /^\s+[^0]?/ な状態のときのみ効力を持つのだが、vim では拡張されいつでも実行可能になっている。

この中で、異質なのはもちろん ‘0’ や ‘^’ を前置した上で ^D すると特別扱いされるという点だ。これらの文字を打った直後に ^D を押すと、カレント行のインデントが削除される。0 と ^ の違いは、インデントの削除が一時的かどうかだ。[cci]:set ai[/cci] な状態で、前者はインデントを削除した行で enter を押した場合、次の行もインデントなしになる。つまり、カレント行が次の行のインデント量を算出する基準になる。一方後者の場合、カレント行は次の行のインデント量を算出する基準から外される。これは例えば、C ソースでラベルを書く場合とかに有用:


foo();
loop:
bar();

なんてコードだと、1 行目で改行した時点で自動的に 1 レベルのインデントが挿入されるが、そこで ^^D し、1 桁目から loop: と打つ。で、さらに改行すると、新しい行のインデントは 2 行目ではなく 1 行目から派生し、1 レベルインデントが生成される。ちなみにインデント量が補正された場合、’0′ や ‘^’ は自動的に削除される。

というわけで、そうなるように実装した。

さて、vi の input モードで定義されている特別な機能はこれで一通り実装したのだけど、実はもうひとつだけ残っている。input モードに入った直後に NUL、つまりコードポイント 0 の文字を入力すると、最後に input モードに入力した文字が自動的に再度入力され、かつ input モードも自動的に抜ける。

この機能はどうなんだ。まずキーボードから NUL って入力できていいのか? 一般的にコントロールコードは Ctrl + A とかで打つ。そうすると、A のコードポイントを 0x1f で論理積を取った値をコードポイントとした文字が打たれたことになる。そう考えるとコードポイント 0 は Ctrl + @ になる。しかし US 配列のキーボードの場合 @ は Shift + 2 で打つので、実際には Shift + Ctrl + 2 ということになるが、これだと制御キーのためのショートカットだとキーイベントハンドラで判別できない。javascript ではキーボードレイアウトを意識するのは非常に難しいのだ。

とりあえず、ctrl+space を特別扱いし、それが押されたら wasavi の中では NUL が押されたことにする。

というわけで、その機能も作った。

brushing up, input mode #4

いろいろ逡巡したが、vim と「だいたい」同じ動作をするようにする。たとえば入力開始位置は ^W の動作に影響を与える。

それから、入力開始位置を保持するのにテンポラリ的なマークを設定しているのだが(wasavi のマークは vim と違い、桁方向についてもテキストの編集による増減に追従するので流用できる)、それにより副作用的に gi コマンド、つまり最後の入力終了位置へジャンプする機能も実現可能になるので、付けた。これは割りと便利ですよね。

入力開始位置は、基本的には input モードの 1 セッション中は変化することはないが、backspace や ^W でどんどん前方へ削除していった場合、カーソル位置が入力開始位置より前に来てしまう。この overrun な状況は、以下の動作に影響を与える。

  1. R コマンドで overwrite モードに入っている場合。入力開始位置より前方にカーソルがある場合、
    • 元のテキストの上で backspace や ^W を押しても、カーソルは移動するものの削除は行われない
    • 元のテキストを上書きしたテキストの上で backspace や ^W を押した場合は、元のテキストが復活する

    wasavi では元のテキストの復活処理はまだ組み込んでいない。

  2. ^U の処理。^U は入力開始位置からカーソル位置までを削除する。入力開始位置より前方にカーソルがある場合、次の候補
    • カーソル行の行頭
    • [cci]:set ai[/cci] してある場合は、カーソル行の最初の非空白文字のある位置

    が選択され、いずれかのうち、カーソルより前方にありかつカーソルに最も近い位置が最終的に残る。この位置からカーソル位置までが削除される。

  3. 文字を入力した場合。入力開始位置はそこに更新される

overwrite モードでの overrun はそもそもそれが起こらないようにクリップするだけにとどめるかもしれない。ちゃんと作ろうとするとかなり大変だ(しかも大変な割にめったに使わない)。

 * * *

overwrite モードでの overrun 状態。入力開始位置より前方へのカーソルの移動は可能だが、編集しようとするとエラーになるようにした。

brushing up, input mode #3

input モードの動作と「.」レジスタ、「.」コマンド、および undo ログとの整合性をとる作業。だいぶ良くなってきた。

次に、^W あたりを片付けよう。これは input モード中、カーソル位置の直前の単語を削除する。

  • input モードに入った入力開始行・桁位置までは、undo ログではなくて現在の入力文字列を操作する(undo ログは backspace/delete/escape などのキーが押されたときに生成されるので)。入力開始位置をさらにさかのぼる場合は、undo ログを生成する。単語の両端が入力開始位置の前、後に亘っている場合はどうするか?
  • ちなみに、^W が入力開始位置をさかのぼれるのは([cci]:set bs=2[/cci] した場合の)vim の機能であって vi や nvi では入力開始位置より前には戻れない。wasavi は戻れるようにする。つまり vim 互換にする
  • ^W の動きはコマンドモードで db した場合に似てるのだが、微妙に違う。たとえば ^W は 各行の 1 桁目で必ず止まるとか。これはなんで?

この辺に留意しつつ vim のソースを眺めてみる。

 * * *

たとえば [cci]ifoo^[[/cci] と打ち、続けて [cci]abar[/cci] 左矢印 [cci]^W^[[/cci] と打ったとき。つまり

foobar
^ ^カーソル位置
|
+挿入開始位置
という関係。ただし、^W を打つ前に左矢印キーを打っているのでそこで挿入開始位置は更新され(input モード中の矢印キーは、いうなれば [cci]^[i[/cci] と打つようなものである。挿入開始位置はその時点のカーソル位置に更新される)、

foobar
^カーソル位置、挿入開始位置
という関係になる。

  • 入力文字列: “^W”
  • 入力コマンド: “a^W^[“
  • undo ログ:
    1. 桁 0 へ ‘foo’ を挿入
    2. 桁 2 へ ‘bar’ を挿入
    3. 桁 0 から ‘fooba’ を削除

となる。つまりこのケースでは挿入開始位置と削除される単語の位置関係は、単語が完全に挿入開始位置の前方に位置しているということになる。すなわち、undo ログを生成する必要がある。

一方、[cci]ifoo^[[/cci] と打ち、続けて [cci]abar^W^[[/cci] と打った場合は、挿入開始位置との関係が変わる。

  • 入力文字列: “bar^W”
  • 入力コマンド: “abar^W^[“
  • undo ログ:
    1. 桁 0 へ ‘foo’ を挿入

vim では input モード中の単語削除を行うために、削除される領域の左端を走査するループを行うのだが、このループは挿入開始位置で必ず抜けるようだ。つまり、削除領域が挿入開始位置をまたぐことはない。単に領域全体が挿入開始位置より前方か後方かの 2 パターンで考えればいい。このケースでは削除領域は挿入開始位置より後方であり、現在の挿入文字列を操作だけで undo ログは生成しない。

この仕様が妥当なのかどうかは、よくわからない。この仕様だと途中で打った “bar” は undo ログから完全に失われる。もっとだだ長い単語だったら再利用したい場合もあるのではないだろうか? あるいは、[cci]:set bs=2[/cci] な vim で input モード中に挿入開始位置より前にも自由にカーソルを再配置できるというのは、つまりユーザに挿入開始位置を意識させないためのものであるはずだが、しかし単語削除時については挿入開始位置を意識させることを強いるのは変な話なのではないか?

^W が押されたときに必要ならそこまでに生成された挿入文字列から undo ログを生成し、挿入開始位置をカーソル位置に更新すれば、削除処理は常に undo ログを生成する backspace でまかなうこともできる。どちらがいいだろうか悩むところだ。ただし、この単純化した処理は ^U には適用できないので(^U は「挿入開始位置からカーソル位置までの入力文字列を取り消す」。暗黙的に挿入開始位置を参照するわけではない)、vim 互換の処理が完全に不要というわけではない。

vim では、このへんは edit.c でやっている。input モード中の backspace 処理は ins_bs() が担当している。この機能はコマンドモードで db した場合と確かに似ているのだが、処理は ins_bs() で完結している。

brushing up, input mode #2

まず backspace、つまりコントロールコード \u0008 を考える。キーボードから “ifoa^Ho^[” と打つと:

  • 入力テキストは “foa^Ho”
  • 入力コマンドは “ifoa^Ho^[“
  • undo ログは 3 つのアイテムを含んだクラスタ: “桁 0 へ ‘foa’ の挿入”, “桁 3 から後方に 1 文字削除”, “桁 2 へ ‘o’ の挿入”

また、キーボードから “i^H^H^[” と打った場合は:

  • 入力テキストは “^H^H”
  • 入力コマンドは “i^H^H^[“
  • undo ログは “桁 0 から後方に 2 文字削除”

となる。なお vim の場合、^H と backspace を内部的に区別しているのだが wasavi ではそこまではしない。どちらも \u0008 として扱う。

追記: カーソルが 1 行 1 桁、つまりバッファの先頭にあった場合に上記ストロークを打った場合は、

  • 入力テキストは “”
  • 入力コマンドは “i^[“
  • undo ログは生成されない

となる。この状態で . コマンドを実行するとカーソルが左に 1 文字移動するように見えるのは、つまり “i^[” の副作用だ。

次に delete。これに対応するコントロールコードは、wasavi においては ^_、つまり 0x7f だ。”d” の上にカーソルがある状態でキーボードから “i^_b^[” と打てば:

  • 入力テキストは “^_b”
  • 入力コマンドは “i^_b^[“
  • undo ログは 2 つのアイテムを含んだクラスタ: “桁 0 から前方に 1 文字削除”, “桁 0 へ ‘b’ の挿入”

となる。考え方は backspace と同じ。

一方、特殊キーはどうか。input モードにおける特殊キーというのはつまり、カーソルを移動させるキー: 矢印キー、Home、End、PageUp、PageDn のことだ。そして input モード中のカーソル移動というのは、実はいったん command モードへ抜け、vi コマンドによりカーソルを移動させ、再度 input モードに入るという手順と意味は同じなのだ。したがって最初の input モードで入力した文字列と次の input モードで入力した文字列はそれぞれ独立したものとなる。これは undo ログも同じ。”ifoo” 左矢印 “bar^[” と入力した場合、左矢印キーを押した時点でまず

  • 入力テキスト: “foo”
  • 入力コマンド: “ifoo”
  • undo ログ: “桁 0 から ‘foo’ を挿入”

という結果が生成されるが、直後に新しい input モードのセッションが開始する。undo ログはリスト構造であり、独立した 2 つのログが最終的に生成されるが、入力テキストと入力コマンドはそうではないため上書きされ、最後のセッションの結果が残る。つまり最終的に

  • 入力テキスト: “bar”
  • 入力コマンド: “ibar^[“
  • undo ログ: 2 つのクラスタ
    1. “桁 0 へ ‘foo’ を挿入”
    2. “桁 2 へ ‘bar’ を挿入”

となる。undo ログが独立しているというのは、u を押すとまず bar が削除され、さらに u を押すと foo が削除されるということだ。なお、undo ログで桁位置も記録しているが、挿入系と削除系で意味合いが違う。前者は入力を開始した位置、後者は現在のカーソル位置だ。入力開始位置は input モードのセッション中は不変だが、backspace / delete で新規セッションが強制開始した場合は入力テキスト・コマンドと共に初期化する必要がある。

brushing up, input mode

引き続き、input モードに不足している部分を補っていく。

ちなみに input モードというのは、i とか a とか押すと遷移する例のモードのことだ。vi が起動してすぐの状態は “command モード” だと一般的に浸透している(と思われる)のに対し、例のモードは “insert モード” とか “edit モード” とか、微妙に表記が定まっていない気がする。しかし posix の定義に倣うならば、例のモードは “input モード” で、とりあえずそう書くことにする。正確には input モードは総称で、実際には insert モードと overwrite モードがある。

input モードで何がめんどくさいかというと、backspace/delete の振る舞いおよび、一部の特殊なキー入力(矢印キーなど)がサポートされている点だ。

input モードでは、以下の情報が逐一更新される。

  • 入力開始位置
  • 入力したテキスト。これは input モードを抜けた後にレジスタ “.” に格納される
  • 入力したコマンド。これは input モードを抜けた後に “.” コマンドで再生されるべきもの
  • undo ログ

これらの更新と、backspace/delete および特殊なキー入力による機能が矛盾なく両立させないといけない。また、abbreviation も考慮する必要がある。”f” を “foo” に展開するような abbrev があったとき、キーボードから “if bar” およびエスケープキーを入力すると:

  • 入力したテキストは [cci]f^Hfoo bar[/cci] となる(vim では)
  • 入力したコマンドは [cci]if bar^[[/cci] となる

ちなみに map の展開はこの前の段で完了しているので、入力されたテキスト、コマンド共に格納されるのは map 展開後の何かだ。ところで abbrev の展開が ^H 付きの構造になってるのってどういう意味あるのかな。別に単に展開後の文字列に置き換えても構わない気がする……。

repository relocation

いろいろなプロジェクトのソースはすべて Subversion で管理している。実は Subversion を入れている Linux PC をリプレースしようかと画策しているので、とりあえず一時的に Windows PC にリポジトリを移動させておきたい。

実際には、Windows PC でも Apache を動かしているので、単にそれに svn を dav のバックエンドとして組み込んで、Linux でダンプしたリポジトリを Windows でロードするだけのことだ。

ただ、以前 www.apachehaus.com には x64 でコンパイルした Apache とか、それに対応した svn 一式とかあってすごいなあと紹介した割にまったく試していなかったので、せっかくなので使わせてもらおう。つまり Apache 自体の入れ替えから作業することになる。

というわけで、Apache を更新し、svn を組み込み、リポジトリを移動させた。PHP は FastCGI で動かしてるので Apache が 新しくなろうが x64 版になろうが基本的に影響を受けないのがうれしい。

minifying

Chrome 版と Opera 版 wasavi のエクステンションパッケージは、javascript ソースを minify して格納している。

そもそもローカルファイルシステムに展開されるソースを minify して効果あるのか? と考えると、ほとんどないわけだけど、前にも書いた気がするが、closure compiler のような変態的な変換を行う可能性もあるので、とりあえずそういうプロセスを経由してビルドするようにしている。ただし Forefox 版は、minify するとレビュアの人に怒られるので、そのまま。

で。

minifier はいろいろあるのだが、いまのところ Microsoft 製の Ajax Minifier を使っている。一方最近、wasavi のソースは strict mode に移行した。さて AjaxMinifier は strict mode に適合する出力を行うのか? というとどうもそうではないようだ。文字列リテラル中に \uxxxx のようなものがあると、AjaxMinifier はそれを可能なら(というより短くなるなら)\x + 8 進表記に直す。しかし strict mode では 8 進表記は禁止なのだった。したがって、そのソースを含んだ wasavi をインストールしても起動しない。

どうするか。まず AjaxMinifier に渡すオプションに -strict:true を明示すると……変化なし。そうではなく、-minify:false を含めるといいようだ。これを含めても、コメントと改行の削除は行われる。なんだか限りなくバグに近い仕様のような気もする。-strict を指定したなら出力も strict mode に適合させるようにしてほしいところ。

それはそれとして、とりあえず生成したパッケージがそれぞれのブラウザで動くところまでは確認した。この辺も自動化できるといいんだけどなー。

entering code point #2

コードポイントの入力モードに入った場合にそれを抜けるには、最大入力文字数に達するかコードポイントの構成文字以外を入力するか、いずれかを満たす必要がある、というのは前の記事の通り。

しかしこれはちょっと不便ではないか。コードポイントの入力を途中でキャンセルしたいとか、明示的に確定したい+余計な文字は打ちたくないといった要求に応えることができない。前者は esc の押下、後者は enter の押下あたりが自然だと思う。しかし意外なことに vim ではどちらもそういう動作をしない。

ということで、そういう風に動作するようにした。

entering code point

というわけで、wasavi.js を分割した。

  • extension_wrapper.js
  • classes.js
  • classes_ex.js
  • classes_search.js
  • classes_subst.js
  • classes_ui.js
  • classes_undo.js
  • init.js
  • utils.js
  • wasavi.js

の複数のファイルで成り立つようにした。

 * * *

挿入モードでテキストを入力する際、vi には以下の ctrl 併用のショートカットが定義されている。また、vim では [cci]:help ins-special-keys[/cci] で参照できるが、以下のリスト以外にも(それはもう膨大に)ショートカットが用意されている。

  • ^D: shift
  • ^H: カーソル左の 1 文字削除
  • ^J, ^M: 改行
  • ^T: unshift
  • ^U: 入力のやり直し
  • ^V: リテラルの入力
  • ^W: カーソル左の 1 単語削除

  • wasavi ではまだ完全に実装できていない。この中で、^V について考えてみる。

    ^V は後続する文字の特別な意味を打ち消し、単なる文字としてバッファに挿入する。ここまでは、wasavi でも実装済みなのだが vim ではこの機能が更に拡張されている(:help i_CTRL-V_digit)。すなわち、

    • ^V [0-9]{1,3}
      10進でバイトを入力
    • ^V [oO] [0-7]{1,3}
      8進でバイトを入力
    • ^V [xX] [0-9a-f]{1,2}
      16進でバイトを入力
    • ^V u [0-9a-f]{1,4}
      16進で Unicode コードポイントを入力
    • ^V U [0-9a-f]{1,8}
      16進で Unicode コードポイントを入力
    • ^V (上記以外の 1 文字)
      入力した 1 文字そのものを入力

    という感じ。これを wasavi に持って来たい。

    まず javascript アプリケーションなので、取り扱う文字は UTF-16 に固定される。したがってバイトの入力であっても Unicode のコードポイントとして扱う必要がある。つまり x/u/U の違いは最大入力文字数だけになる。

    ^V の次に [0-9oOxXuU] を入力しコードポイント入力モードに入った場合、それを完了させる方法は 2 つある。まずそれぞれのモードの最大入力文字数に達した時点で、自動的に完了する。次にそれぞれのモードが受け付けるコードポイントの構成文字以外の文字 c を入力すると、その時点で蓄えられたコードポイント文字列から文字を生成し、それがバッファに入力される(コードポイント文字列が空の場合は何も入力されない)。入力される場合は、abbreviation の展開処理を迂回する。その直後 c が入力される。こちらは abbreviation の展開処理を経由する。

    なお vim では U プリフィクスを使用した場合、最大 8 桁の 16進数(ただしヘルプでは最大値は 0x7fffffff とのことだ)を入力できるそうだが、でも Unicode って最大 U+10FFFF だよね。クリップしてエラーにしたほうがいいのかな?

    またもちろん、U+10000 以上のコードポイントを入力した場合は、サロゲートペアに分割して 2 文字を入力する必要がある。

    だいたいこんな感じの仕様でいいかな!

ant fix

build.xml を書くのがどうも苦手だ。

たとえば trunk/frontend/ の下の 10 個程度の javascript ソースを minify して、指定のディレクトリに置きたい。そういうタスクはないので、自分で書くか、既存の minifier を exec することになる。

とりあえず簡単なほう、つまり既存の minifier を呼び出す方で行ってみるのだけど、1 つの js ソースだけならともかく複数ある場合はどうしたら? 仮に jsminify みたいなタスクがあって、それが子要素として fileset を受け付けるならば、こんなイメージになるはず:





しかしそんなタスクはない。なので、直接 minifer を呼ぶのではなく、適当な php スクリプトを呼び出すようにして、その中でディレクトリを読み、見つけた js ソースを片っ端から minifier にかけるようにしている。

こんな調子で ant だけではできなさそうな箇所は全部 php スクリプトを挟んでいるのが今の状況なのだった。いやまあ php はいっぱしのグルー言語なのだから、そういう使い方は間違ってないといえば間違ってないと思うけど。なんかとても中途半端だ。

と思ったらこういうのがあるんだね。ant 上で foreach 的なことができるのか。あらやだ素敵!