Recently Posted Articles:
blog を移転することにした。
新しい blog は
http://appsweets.net/blog/ に作った。
ここに新しい記事を書くことはもはやない。
そんなわけで地道にテストをしているのであるが、現在 undo/redo にさしかかっている。
素の vi、あるいは nvi においては、undo は 1 レベルしか許されていない。u で undo した状態でさらに u を押すと redo 扱いになる。undo/redo の仕組みを作ってしまえば、それを複数レベルに拡張するのは難しくないので、1 レベルに制限してあるのはまあ、当時のメモリ容量との兼ね合いなのだと思う。一方、vim は標準で複数レベルの undo の仕組みを利用できる。u は編集操作を過去へさかのぼり、逆に ^R は未来へ進めるという動作をする。さらに、vim が保持する undo リストは単なる1次元のリストではなくツリー構造になっていて、任意の枝へ進めたり戻ったりすることもできる、らしい(使ったことはない)。
wasavi はどうなのか。基本的には vim に準じているが、undo リストは 1 次元である。つまり、10 レベルの編集操作を蓄えた状態で 5 レベル戻り、そこで新しく編集操作を加えると、未来に起こったはずの 5 つの編集操作は完全に失われる…というような感じ。
内部的な話。vi のテキストへの編集操作はさまざまなのだが、結局のところ、指定のカーソル位置に対して
- 文字列 s を挿入
- 文字列 d の上へ文字列 s を上書き
- n 文字削除
- n 行右へシフト
- n 行左へシフト
という処理に収斂する。undo リストもこれらの処理を 1 要素として構成される。で、挿入/上書きと削除処理は表裏一体なので、処理をまとめられる。つまり挿入処理の undo は削除処理の redo であり、挿入処理の redo は削除処理の undo である。
しかし基本的にはそういう路線なのだが、シフト処理だけは若干特殊。さらに右シフトと左シフトが可換というわけでもない。どういうことかというと、以下のテキストを左シフトする場合を考える:
1
2
3左シフトした結果、こうなる:
1
2
3
これを undo した場合、単純に右シフトすると
1
2
3になってしまう。したがって何らかの付加的な情報を持たせないとならないわけだ。
wasavi においてはいわゆるステータスラインが常に表示される。通常、カーソルの位置と文書内の縦方向の位置のパーセンテージを表示する。
ところで現状、表示するカーソル桁位置は物理的な値である。たとえば タブ、foo という行があったとして、カーソルが f の上にあり、tabstop が 8 だとする。物理的な桁位置は 1 である。言い換えると行頭からカーソル位置までの「文字」数が物理的な値だ。
一方、論理的な値は 8 になる(表示の際は 1 オリジンになるので、9 になる)。vi をはじめ、普通のテキストエディタが表示する桁位置はこちらである。これは平均的な表示幅を 1 単位として、表示上で行頭からカーソル位置まで何単位分になるかという論理的な値だ。モノスペースなフォントでは、平均の表示幅はだいたい basic latin 部分が基準になると思う。一方漢字やひらがなカタカナはその倍になる。あるいはその他の言語のスクリプトの場合、もっと幅を取るかもしれない。
論理的な桁位置を html と javascript で取得するのは難しい。一発で取得するのは難しい。たとえば適当な span 要素(white-space:pre とスタイリングしてあるとして)に文字列を突っ込んで、その offsetWidth を取得し、平均文字幅(たとえば「0」の offsetWidth)で割れば、それっぽい値は出てくる。しかし、まあ、Opera でしか確認していないが、誤差がけっこうあるのである。除算の際に四捨五入するようにしてもやはり文字列に依存して正しい値を算出できなかったりする。
これをできるだけ正確に求めるのはなかなか面倒かも。文字列を書記素単位に分解し、それぞれに対し平均文字幅に対する整数倍率を取得し、合計を求める……みたいな感じになると思う。割と重そう。論理的な桁位置を考慮する必要があるのはとりあえずステータスラインの表示時と | モーションのときくらいで、重くてもまあ問題ないかもしれないが…。
というか書記素単位に分解、という時点で前に扱ったどでかい unicode ライブラリを組み込まないとダメな気がするんですが。むむむ。
すぐ手をつけるわけではないが、l10n について準備しておく。
Chrome、Opera、Firefox(Add-on SDK) のそれぞれの Extension システムで、l10n の仕組みは用意されていたと思う(うろ覚え)。くわしく調べる前に各 Extension の雰囲気で予想すると、
- Chrome: 最も実践的。でも標準技術からはちょっと外れている
- Opera: 最も簡単な仕組み。不安になるレベルで
- Firefox: 最も標準技術にコミット。でもそれゆえに使いにくい
さて実際はどうかな?
* * *
とりあえず各 l10n のリファレンス:
:set fullscreen
でフルスクリーン化するようにした。:set fs でもよい。解除する際は :set nofullscreen または :set nofs。
昨日の記事で wasavi のシフトは shiftwidth も tabstop も見ていないと書いた。が、それもなんなので、以下の通りにすることにする。
- > や < では、まず行頭のインデントの u+0009 が tabstop 字分の u+0020に展開される
- 次に shiftwidth で示される文字数の u+0020 が挿入・削除される
- 最後に行頭のインデントが tabstop 字ごとに u+0009 に置き換えられる
- wasavi においては、tabstop は 8 固定。
ということで、tabstop が常に 8 であること以外は、普通の vi と同じだと思う。
ところで html ページにタブを含む行を表示した際、タブが何文字分の空白に相当するかを判断することはできないのかというと、できないこともない。単に適当な span に空白を入れた場合と、タブを入れた場合の offsetWidth を比較すればいいだけだ(whiteSpace スタイルは 'pre' にしておく必要がある)。しかし……:
- Opera で実行するコンテキストか何かによってタブを入れた場合の offsetWidth が変な値になることがある。原因不明
- Opera で canvas#measureText('\t').width を参照すると、おかしな値、たとえば 3 が返される
ということで、やむを得ず 8 固定。
* * *
追記: css3 でタブ幅を指定できるようだ。
http://www.w3.org/TR/css3-text/#tab-size先行で実装しているのは Opera と Firefox だけで、それぞれ element.tyle.OTabSize と element.style.MozTabSize のようにベンダープリフィクスが付いている。
というわけで、それらのブラウザでは tabstop の値を使用するようにした。
正確には、nvi と vim の相違。
> でシフトすることを考える。posix での仕様は
これ。特に、シフト後のカーソルの位置の変化について着目する。
Current line: If the motion was from the current cursor position toward the end of the edit buffer, unchanged. Otherwise, set to the first line in the edit buffer that is part of the text region specified by the motion command.
Current column: Set to non- <blank>.
まず行位置。モーションがカーソルを前方に移動させる系であった場合、変更しない。そうでない場合、モーションによって示される領域の先頭の行へ置くこと。
vim はこの仕様にしたがっているが、nvi はそうではない。いかなる場合であっても、行位置は変化しない。
次に桁位置。決定された行の最初の非空白文字に置くこと。これもまた、vim はその通りに動作するが、nvi はそうではない。シフトによって変化した分だけインクリメントされる。つまりシフト前のカーソル位置と同じ文字を指すように調整される。
ということで、どういうわけか nvi はシフトに関しては完全に独自仕様の動作を行うのだが、しかしこれは nvi の動作の方がどちらかといえば使いやすい気がしないでもない。画面の行数を超える大きな領域をシフトしたりすると、シフト後に完全に画面が書き換えられて「あれ?ここどこ?」状態になりがちである。
でも……うーん……どうしよう。
ちなみにシフトすると shiftwidth で示される空白文字列が生成され、さらに tabstop 字ごとにタブに置き換えられ、最終的なシフト量が決定されるわけだけど、いまのところ wasavi ではシフトは直接タブを挿入するということにしている。shiftwidth も tabstop も見ていない。これは、html ページ上に行を表示する場合 1 つのタブが何文字分の空白に相当するかということを制御できないし、何文字分に相当するかすら取得できないからだ(css には tab-stop なるものがあったようななかったような気がするが)。この辺を完全に制御するには行の描画自体を自前でやんないといけない。
322 テスト、1258 アサート。
以前 T コマンドについて特定の場合に動作が特別扱いされるというエントリを書いたが、c オペレーションでかつ w モーションの場合にも特別な動作が用意されている。これは割と有名だと思う。
foo bar
f の上にカーソルがあるとき、w を押すとカーソルは b の上に行く。つまり移動範囲には f、o、o、空白が含まれる。一方、cw を押すと単語の変更となるのだが、変更される範囲に含まれる文字は f、o、o である。最後の空白は除外される。言い換えると、cw は実質的に ce とみなされる。どのファイルか忘れたけど vim のヘルプのどこかにもそういう置き換えしますよということは書いてあったと思う。
このタイトルにしたが、例によって vi と vim の違いではないというネタ。
foo
bar
まずカーソルが f の上にある状態で、w と入力する。カーソルは単語の境界を走査し、次の行の b で止まる。このとき、スキップした文字は f、o、o、改行、である。
次にやはりカーソルが f の上にある状態で、yw と入力する。つまり単語を無名レジスタにコピーするのだけど、微妙に w 単体とは動作が異なる。無名レジスタにコピーされるのは foo なのだ。後ろの改行は無視される。これはそういう風に動作するためのコードをわざわざ仕込まなければいけない。具体的には、走査の途中で改行が見つかったとき、オペレーションが指定されていてかつ指定されたカウントの最後のループであるときは即単語走査を抜ければいい。y は走査前のカーソル位置(f)から、走査後のカーソル位置(改行)の直前までを取り出すので、無名レジスタの内容は foo になる。
しかし実装の話は別にどうでもいいのだ。問題は、w と対になる逆方向の単語走査 b である。
カーソルが b の上にある状態で b と入力する。するとカーソルは前の行の f で止まる。では、カーソルが b の上にある状態で yb と入力したら?
この場合、yw のときのような小細工は nvi も vim も行わないようである。つまり単純に走査前と走査後のカーソル位置から、無名レジスタの内容は foo\n になる。後ろに改行がつく。
とこのように対称性が取れていないのだが、前述の通り nvi も vim もそういう風に動作するので、wasavi もそういう風に動作するようにする。
187 テスト、660 アサートになった。最終的に 200 弱のテストになるかと見積もっていたが、ぜんぜん足りなかった。あと c/y/>/< のオペレーション、undo/redo、ex コマンドのテストが残っている。これらを揃えると、400 テストくらいにはなるかもしれない。いつ終わるのだこれは。
テスト書いてる間に Opera も DOM3 composition event が実装されないかなあ……。Opera といえば、widget と unite はディスコンらしいですね。widget はともかく、unite はちょっともったいない。
ちなみに wasavi の開発は主に Opera 上で行っている。これは Opera の javascript デバッガである Dragonfly がなかなか使いやすくなっているからだ。具体的にはショートカットキーを自由に定義できるから。Firebug や Chrome の Developer Tools を含む大体の GUI 系のデバッガって VisualStudio の影響なのか step in/step out/step over がファンクションキーに割り当てられているが、step in を「i」、step outを「x」、step overを「n」、検索フィールドへのフォーカスを「/」、ソースのスクロールを「f」「b」とかにするだけでずいぶんと快適になるのである。ただこれだけのことだが、かなり違う。
ただ
- 再定義したショートカット、どうも Opera を終了するときれいさっぱり忘れるっぽい
- ときどきブレークポイントを張ってない行で一時停止することがある
- ときどき一時停止する箇所とソースの対応が取れていない(}だけの行で止まるとか)ことがある
あたりは直してほしい。あとは満足。Dragonfly の開発チームに感謝したい。
前の記事で vim -C で起動するとより vi 互換な状態で起動するとか書いてしまったが、:set compatible? すると互換になってない(vim 7.3 on cygwin)。
な なんだってー! と素で言ってしまったぞ。vim -c ":set compatble" で起動させたほうが確実か。
vi 互換の vim は、だいたい nvi と同じような動作をするようだ(nvi のバグっぽいものを除いて)。
* * *
d/foo<CR> と入力することを考える。つまり、カーソル位置から、前方にある "foo" の直前までの領域を削除する。この際、領域の左端と右端について、「それら端点から行頭までの間に、空白以外の文字があるか、またはないか」という条件を考える。したがって左端と右端との組み合わせで計 4 パターンが存在する。
== pattern 1 ==
def
foo
baz
== pattern 2 ==
abc def
foo
baz
== pattern 3 ==
def
ghi foo
baz
== pattern 4 ==
abc def
ghi foo
baz
いずれのパターンにおいても、1 行目の「d」にカーソルを置き、d/foo<CR> と入力する。
どうですか。それぞれのパターンでどうなるかわかりますか。 ...
vi/vim にはマークというものがある。
a から z、および A から Z までのマークがあり、任意のマークに行番号と桁番号のセットを覚えさせておくことができる。覚えさせるコマンドは m{mark}。覚えておいたマークにカーソルを移動させるのは `{mark} または '{mark}。
さて重要なことに、覚えたマークはその行番号と桁番号に依存するわけではない。
どういうことか。
foo
bar
baz
というテキストの 2 行目にマークをつけたとする。次に、テキスト先頭に新しい行を追加する。
FOO
foo
bar
baz
するとマークが記憶している行番号は自動的に補正されて 3 行目を指すようになる。
ということで、wasavi でもそういう風に実装してある。してあるのだが……勘違いしていた。この自動補正は桁位置に関しても行われると思っていた。つまり
foobar
^ここにマーク
という行(下線位置にマークがあるとする)に
fooFOObar
^ここにマークというふうに文字を挿入するとその分だけ桁位置も自動補正されると思ってたら、vi / vim とも桁位置に関してはそういうことはしないのね……。
でもまあとりあえず残しておく。
あと、ここまで書いて気付いたが、このネタは別に vim と vi 間の違いというわけではなかったがまあいいや。
barbaz と書いてある行の末尾にカーソルがあるとする。
vim 7.3 (-C オプション)- Tb と入力: barbz
- Tb と入力: barbz(カーソル位置は変化しない)
- ; と入力: arbaz
- $Tbd; と入力: bz(すなわち、baz の下線部分が削除される)
nvi 1.79- Tb と入力: barbz
- Tb と入力: barbz(カーソル位置は変化しない)
- ; と入力: barbz(カーソル位置は変化しない)
- $Tbd; と入力: bar(すなわち、barz の下線部分が削除される)
vim の場合、実質的に T である ; の結果(つまり、コマンド終了後のカーソルが置かれるべき位置。T の場合は見つかった文字の右となり)が、走査前のカーソル位置と同じであった場合は、それを無視する、というような特別扱いをしている、気がする。
nvi はそういうことはしない。さらに 4. は不思議な動作だ。削除される領域の左端、右端の両方について inclusive に動作している?
えーと……どうすればいいんだー。
* * *
vim にあわせることにした。また、; だけでなく , についても上記の特別扱いを施すようにした。
たとえば 10 行のテキストがある状態で 100j とか 100k とか入力すると、vi はテキストの先頭・末尾行を超える移動はエラーになり、カーソル位置は変化しない(posix でそう定義されている。また nvi 1.79 はそう動作する)のに対し、vim はカウントをクリップし、先頭・末尾行へ移動する。
vim というのは今でこそ vi よりも遥かに多くの機能を持っているが、そもそもの起源は Amiga 用の vi クローンだった、そうだ。今でも、vim -C としたり :set compatible としたりするとより vi 互換になる。
が、それでも違うところは違うということだ。
さて wasavi はというと、本質的には posix の定義にできるだけ従う vi クローンである。vim クローンではない。これは、vim の機能の再現なんかしだしたら何年たっても実装が終わらないからだ。と言っても pure vi にこだわるわけでも vim の機能を無視するわけでもなく、複数の undo/redo、レジスタ「"」「:」「*」「/」、iskeyword/searchincr/smartcase/undolevels の設定なんかは vim から輸入している。
で、テキストの先頭・末尾行を超える移動をどちらに合わせるかなのだが、vim に合わせようと思う。たぶんそのほうが使いやすい。また、任意の桁に移動する「|」モーションとの対称性もとれる(「|」モーションは vi であっても、行末でクリップされる)。
Advertisement