visual in vi #6

gq
command モードでの [cci]gq[/cci] はオペレータとして扱われ、後続するモーションによって移動したカーソルとの間の領域に「textwidth の幅に収まるよう適切に文字を充填したり適切に改行を挿入する」という処理を行う。ここで、カウントを指定し、かつモーションが [cci]_[/cci] またはそのエイリアス(つまり [cci]gqgq[/cci] または [cci]gqq[/cci])だった場合、カウントは [cci]gq[/cci] 自体に作用し、相当する行数が再フォーマットの対象になる。

何を言いたいのかというと command モードでは [cci]3gqq[/cci] とか指定した場合、カーソル以下の 3 行を再フォーマットしろ、という意味になる。カウント := 縦方向の行数。

vim では、これが visual モードになると、カウントは textwidth オプションを一時的に上書きする。カウントの意味合いが垂直方向から水平方向に完全に切り替わる。カウント := 横方向の文字数。

これ、良いインターフェースか? と問われればもちろん全然良くないんだけどどうしたものかなー…。

gv
vim では、command モードでの [cci]gv[/cci] は、最後に使用した選択領域を再びアクティブにしつつ visual モードへ遷移する。ということで wasavi にも実装したのだけど、では visual モード中に [cci]gv[/cci] したらどうなるんでしたっけ? と思いつつやってみたら、やはり command モードでのそれと同じ動作をした。つまり最後の選択領域の端点と、アクティブな端点は個別に管理しているようだ。

それに倣い、wasavi でも bound モードでのアクティブな選択領域の端点はプライベートなマークを使用するようにした。

visual in vi #5

選択範囲の指定方法を最適化したい。

今の実装では、wasavi のバッファは DOM そのものであり、バッファ内の行は div 要素であり、選択範囲は span 要素でくくられた領域となる。その状況下で選択範囲が拡張・縮小された場合、当然 span の境界も移動しなければならないのだが、とりあえず一番簡単なのは、再設定に先駆けていったん span 要素を脱皮させ、それから新しい選択範囲をくくりだすことだ。

これはもちろん賢いやり方ではない。選択範囲が複数行で構成されている場合、行ごとに span のくくりだしというループを経なければならないのだ。もっと最適化して、拡張・縮小された領域だけを操作するようにしないといけない。

ということで、そうした。この最適化により、bound モード中のモーション全体が恩恵を受けるのだけど、特に [cci]/[/cci] や [cci]?[/cci] でインクリメンタルサーチを行った際に顕著かもしれない。

visual in vi #4

スクロール系のコマンドを実装した。実装したというか、command モードのマッピングに移譲するだけ。

次にオペレーションから独立している編集コマンドを実装する。vim では、これに該当するのは [cci]: r s C S R x D X Y p P J U u ^] I A[/cci] であるので、wasavi でもだいたいそれに準拠する形になる。

ただし、とりあえずとして、矩形選択はめんどくさそーなので実装しない。またタグ検索は全然 wasavi には実装していない。つまり [cci]^] I A[/cci] は除外する。

とりあえず簡単なところから埋めていきたい。まず [cci]:[/cci]。これは他の編集コマンドとはちょっと異なり、bound だからといって直接的な影響は受けない。数日前、command モードにおいてカウントを前置した状態で [cci]:[/cci] を押すと自動的に [cci].,.+10[/cci] みたいなアドレスが ex コマンドラインに入力されるようなナイスな機能を付けたのだが(といっても例によって vim のパクリである)、 それと同様、bound モードから line-input モードに遷移した場合は自動的に初期値が [cci]’<,'>[/cci] となるようにした。つまり選択中の範囲が対象になる。

ただし基本的に、ex コマンドは操作する要素の単位は「行」である。文字単位で選択し、そのマークを指定したからといって文字単位で編集できるわけではない。[cci]foobarbazxyz[/cci] の bar を選択した状態で [cci]’<,'>ya[/cci] すると、無名レジスタに保存されるのは [cci]foobarbazxyz^J[/cci] と行全体である。

visual in vi #3

bound モードで range symbol(vim でいうところの text object)を指定できるようにした。

その辺を実装してみて初めて気づいたのだけど、vim では visual モードで連続して aw aw aw と押していくとそれに従って選択範囲が拡張していくのだね。aw ごとに選択範囲が再設定されるわけではない。というわけでそういうふうに動くようにした。

次は operation、つまり y c d < > の眷属を実装する。

* * *

実装した。

visual in vi #2

まずモード名だが、vim のように visual モードではなく、bound モードと呼ぶことにしたい。もしかしたら region モードにするかもしれない。

これを実装するにあたって、内部的には、本質的には単に選択領域の 2 つの端点だけを管理するだけの話だ。bound モードではカーソルが行末の改行にも乗れるようにする、とかカーソル(選択の終了点)が選択の開始点の前へ行った時のつじつま合わせとか、細々としたものはあるけれど、内部的には結局は 2 つの端点だ。

一方で、vim で visual モードというだけあって、見た目の部分が重要だ。つまり選択中の領域を反転表示させないといけない。DOM でこれをどう実現するか?

まずブラウザが持つ標準の範囲選択の仕組みをブラウザから制御するために selection がある。これだとまさに選択領域の両端点を指定するだけでいい。

var r = window.getSelection().getRangeAt(0);
r.setStart(text_node_of_lower_side, column_of_its_index);
r.setEnd(text_node_of_higher_side, column_of_its_index);

ただ、それがどう表示されるかは全くブラウザ任せになる。これは実際楽なのだけど、wasavi 側のテーマによっては判別しづらいものになるかもしれない。

次に DOM レベルで選択範囲を span でくくりだす方法もある。こちらは選択領域の更新を自前でやらないければいけないのがネックになる。一番簡単なのは、更新前に一旦すべての選択領域をクリアし(つまり span でくくりだされた部分を脱皮させる)、再度選択範囲をくくりだすのである。これだと特に選択範囲が何十行にも及ぶ場合に明らかに遅くなりそうではあるので、実際はそのへんは差分で管理しないといけないはずだ。

そういうわけでこんなかんじになる。いたってふつー。

bounding

undo boundary #2

単純に undoboundlen によった文字数で判断すると、ラテン文字な文章では問題ないのだが東アジアなそれでなんか微妙だ。

つまり、最終的には表音文字と表語文字の性格の違いということになると思うのだけど、同じ 20 字分でも密度が違うのである。例えば日本語文とかのほうが、英文に比べて同じ文字数での情報量がずっと多い、気がする。というのも、英文が 20 文字ごとに undo されていく様子は「あー、そうそうだいたいそのくらいだよね!」と感じるのだが漢字かな交じりの文だと「えっそんなに undo しちゃうの? 多くね?」となるのだ。

というわけで、undoboundlen をもとにした表示上の幅で判断するようにした。つまりフォントに影響を受けるのだけど、日本語だとだいたい 12 文字前後で区切られるようになる。

こんなもんかなー!

undo boundary

wasavi へ新しい機能を追加するにあたって、これまではほぼ 100%、vim の機能を持ってくるというものだった。しかし今回追加する機能は、なんと emacs 由来のものである。

input モードに入ってだーっとテキストを入力し、esc を押す。すると、undo の単位は入力したテキスト全体になる。従って、たとえば入力した後「やっぱりテキストの半分は書き直したい」というケースでは、undo は役に立たない。

emacs は、連続して文字を入力している間であっても、(確か)20 文字ごとに関数 undo-boundary を呼び出す。この関数は undo リストに「区切り」を挿入する。すると、undo は連続して入力した文字列全体ではなく、その区切りで区切られた断片的なテキストの単位で行われる。賢い。実は vim にも似たものはあるのだが、手作業で [cci]^G u[/cci] を押させるのである。自動ではない。きっと vimscript でどうにでもなるのだとは思うけど、デフォルトの仕様はあまり賢いとは言えない。

そんなこんなで、wasavi に実装する機能は以下のとおりである:

  • オプション undoboundlen を導入する。これは整数を保持する
  • オプション undobound を導入する。これはundo の区切りとみなすことのできる文字列群を保持する。例えば [cci] ,.!?、。,.!?[/cci]
  • input モードで 1 文字入力するごとに次の処理を行う: 入力中の文字列が undoboundlen を超えていて、かつ最後に入力した文字が undobound に含まれる時、undo リストに区切りを挿入する

* * *

undobound として区切り文字を指定するのではなく、unicode の一般カテゴリが Zs であるか、プロパティが STerm であるか、または Terminal_Punctuation である文字とするようにした。

visual in vi

ついに恐れていた要望が訪れた。

issue #19。つまり、vim の visual モードがほしいというものだ。

visual モードというのは、一般的な GUI が持つテキスト選択の仕組みに似ていて、編集中のテキストの任意の選択範囲を指定するのに用いられるのだが、最大の特徴はその範囲が視覚的に明確に示されるという点だ。visual モードを開始した位置と、現在のカーソルとの間が選択範囲となり、それが例えば反転表示とかされるのである。カーソル移動のために visual モードでは command モードと共通のモーションを使用できるが、オペレーションに関しては visual モード専用の処理が実行される。

何を恐れているかといえば、まず visual モードというネーミングだ。そもそも、vi 自体が ed とか ex とかいったエディタの「visual モード」なのである。完全に被っている。次にそれが必要な決定的な理由がない(と思われる)のも微妙だ。visual モードがなかったとしても編集には基本的には全く問題ないのだ。

しかしまあこれがほしいという要望は多分来るだろうと思っていたし、実際来てしまったので作るわけなのだ。たぶんモード名は変えると思う。

expandtab #5

input モード中に backspace や ^H を押した際、カーソル位置より左にある直近のタブ位置からカーソル位置までの文字列がすべて U+0020 で構成されている場合、それらをまとめて削除するようにした。

これは expandtab の状態でタブの代わりに空白の塊が挿入された場合に、それをやはり塊として削除できるようになるので、expandtab によるタブ変換と対になっている機能といえる。この機能は expandtab の状態にかかわりなく、常に有効である(vim がそのようなので)。

ただ塊ではなく 1 つの空白だけを削除したいケースもあるはずで、そういうときは恐ろしくおせっかいな機能と化してしまう。なにか代替となる操作も用意した方がいいかもしれない。まあその辺を実際にどうするかはユーザのレスポンス次第である。

なにはともあれ、とりあえず expandtab 絡みの実装はこれで一段落ということにする。