fftt, again

fFtT コマンドについて、従来からある拡張をしていた。カタカナ・ひらがな・漢字のローマ字読みのデータを wasavi 内部で持って、Latin-1 の文字でそれらを指定できるというものだ。つまり「カ」にカーソルをジャンプするには [cci]fk[/cci] すればよい。

今回この対象をカタカナ・ひらがな・漢字以外にも広げたい。特に、Latin-1 ではないラテン文字に対する拡張が中心になる。

元となるデータは、UnicodeData.txt の Decomposition_Mapping である。たとえば U+00C0 (À) のそれは
U+0041 U+0300
という形式に分解される。U+0041 はつまり単に A であり、それに U+0300 が付随している書記素クラスタということだ。このデータの中から最初の Latin-1 内の文字を抜き出して fFtT の代替表現とする。

前述の通りこの拡張はラテン文字が中心なのだが、日本語に関してもいくつか拡張される。

  • U+330D (㌍) のようなアレについて、最初のカナに対応するローマ字の読みでジャンプできるようになる
  • U+FF01 から始まる全角形の文字群について、対応する半角形でジャンプできるようになる
  • 半角カナについても同様

一方、気になる点もいくつか出てきた。

  • Decomposition_Type = <fraction> である文字、例えば U+00BC (¼) は分解すると 1/4 となって最初の Latin-1 は 1 である。しかしそうすると 1/4 だろうが 1/2 だろうが 1/3 だろうがその Latin-1 代替表現は全部 1 だ。これは直感的なんだろうか?
  • Ⅰ Ⅱ Ⅲ Ⅳ といった文字についても、単純に Decomposition_Mapping に従うとその Latin-1 代替表現は全部 I(Latin-1 のアイ)。これは対応する 1 〜 4 のほうが良いのではないか?(そうすると U+2169 (Ⅹ) 以降に何を対応させるかという別の問題が出てくるが)
  • U+2469 (⑩) から始まる 10 〜 20 の囲み数値についても何を代替表現にするかという問題が。全部 1 でいいのか?

* * *

とりあえず何の変更も加えないことにした。文句が上がってきたらその時考える。

ちなみに、どのコードポイントがどの Latin-1 の文字に代替されるかは github 上の wikiに書いてある。

Surrounding the world #9

ひと通り実装が終わった。

ただし以下のものは surround.vim から移植していない。また、surround.vim を読んでそういう機能があると把握してないものがあればそれも当然移植されていない。

  • [cci]/[/cci] を包囲文字列に指定すると C コメントが対象になる機能
  • 包囲文字列に数値を前置すると、カウンタに乗算される機能
  • [cci]l[/cci] などを包囲文字列に指定すると、TeX の [cci]\begin[/cci] と [cci]\end[/cci] が上手いこと付加される機能
  • [cci]f[/cci] などを包囲文字列に指定すると、[cci]関数名([/cci] と [cci])[/cci] が上手いこと付加される機能
  • [cci]surround_[/cci] で始まる各種変数で動作をカスタマイズする機能
  • input モード中における [cci]^S[/cci]、[cci]^GS[/cci]、[cci]^Gs[/cci] マッピング

とりあえず作業はテスト漬けへ移る。

Tagging a text

Text Object の [cci]it[/cci] および [cci]at[/cci] を Range Symbols へ持ってくる。

これは何かといえば、カーソル位置の前後のタグを認識する処理だ。つまり単純に考えれば、[cci]<[/cci] で始まり [cci]>[/cci] で終わっているのがタグである。そして 2 文字目が [cci]/[/cci] であれば終了タグであり、そうでなければ開始タグだ。これを利用しつつ、開始タグと内容と終了タグの全体を選択するのが [cci]at[/cci] であり、内容だけを選択するのが [cci]it[/cci] だ。

もちろん実際にはそう単純ではなく、

  • カウントが指定された場合は、カウントの分だけいわゆる parentNode を辿った先の要素のタグを対象にする
  • タグの内部は改行も含まれることを意識しないといけない
  • vim は短縮タグ([cci]
    [/cci] みたいなの)は、タグとみなさない
  • 対応する終了タグが見つからない開始タグは、タグとみなさない。これはなかなかヒューリスティックに働く。vim の該当処理は XML 宣言も SGML の注釈宣言も知らないが、このルールにより結果的になんとなくうまく動く。
  • 走査の結果出た選択領域が、走査前と同一であった場合は親のノードを対象に再度やり直すといったつじつま合わせが必要。この処理は visual モードで連続して [cci]it[/cci] した場合に実行される

といった感じのことを念頭に置く必要がある。

念の為書いておくが、毎度書いてる気がするが、こういったことは本来 SGML なり XML なり HTML のパーサが保持しているノードの木構造を参照すべきことなのであって、テキストベースでカーソル位置の前後をチラ見してどうこうする vim の手法は根本的に間違っているのだが、まあ仕方がないのです。

* * *

作った。

Surround the world #8

Range Symbol [cci]t[/cci] を vim から移植する。

前にも書いたがこの Range Symbol は本来ちゃんとした SGML パーサーが文書を解析した上での結果を利用すべきものであって、Range Symbol 内にその簡易版を持つのは無駄なのだが。しかしタグ周りを実装しないと surround.vim の便利さ半減なのでまあ仕方がないのであります。

とりあえず vim の search.c を読む。current_tagblock() あたりを読めばいいんだよねきっと。

Surround the world #7


{
abc
def
}

という感じのブロックがあったとして、カーソルが波括弧の内部、つまり [cci]{[/cci] と [cci]}[/cci] の中にあったとき、

  1. [cci]ds{[/cci]
  2. [cci]ds}[/cci]

したとき微妙に動作が異なり、またそれがいまいち文書化されてない件。

1. の場合、バッファの内容に関しては何も起こらない。ただ、カーソル位置が [cci]{[/cci] の直前行に何故か飛ぶ。
2. の場合、期待したような動作をする。特に、[cci]{[/cci] および [cci]}[/cci] がそれ自身だけで構成されるような行であった場合、行自体が削除される

この違いって、どういう意図があってそうなっているのかよくわからないのだけど。

* * *

今試してみたら、なんか、同じ動作をした。…!? どういうことなの…。どうも、その時々のファイルタイプやカーソル位置やその他諸々の理由によって動作が安定しない感じがしないでもない。

うーんまあそれはそれとして、複数行のブロックの surrounding の削除を考えるとき、

  • 左側の包囲文字列のインデント量
  • 左側の包囲文字列が orphan であるかどうか。つまり、[cci]^\s*\{\s*$[/cci] みたいな状態であるかどうか。orphan であれば、行自体を削除できる。orphan でなければ、左側の包囲文字列と、その直後に並ぶ空白文字列を削除する
  • 右側の包囲文字列が orphan であるかどうか。orphan であれば、行自体を削除できる。orphan でなければ、右側の包囲文字列およびその直前に並ぶ空白文字列を削除する
  • 包囲されている内容のインデント。内容の各行について、左側の包囲文字列のインデント量で強制的に上書きする

といった点に気をつける必要がある。あるというか、surround.vim がそういう動作をするようになっている。

この中で不可思議なのは内容のインデント操作だ。というのは、包囲文字列を挿入する場合は、内容について右シフトを行うのである。そうすると、対応する削除の場合は左シフトじゃないの? と思うのだけど、そうではないのである。なんでだろうか。

Optimize cursor visibility

Surrounding と同時に進めているものがある。カーソルの表示・非表示について、従来はけっこう散らばった箇所でめいめいに制御していた。これを統一的に統合したい。

どんなキーストロークであっても wasavi.js の processInput() を必ず通る。特に、command モードであれば execCommandMap() を必ず通る。つまり各々のコマンドハンドラでカーソルの状態を場当たり的に制御するのではなくこれらの関数内でまとめて面倒を見るのが正しいやり方だ。

そういうわけでそういう方向で修正している。

Surround the world #6

やはり、Range Symbols に含まれる引用符と含まれない引用符で動作が異なるのは良くないと思ったので、Range Symbols の対象外の文字についても、quote タイプ、つまり左右の包囲文字列が同じであれば Range Symbols の引用符と同じ動作をするようにした。

これは surround.vim の動作とは違う。

亜引用符が Range Symbols 引用符に準ずるようになると、例えば前述の quoteescape オプションの考慮だけでなく、引用符の内外も認識されるようになる。つまり [cci]”abc” “def”[/cci] などといった文字列に関して、カーソルが 1 オリジンでの 6 文字目にある場合は引用符の中にないと判断するようなそれなりに賢い仕組みの恩恵を受けることになる。

それから、surround.vim の場合内容が空の引用符上での [cci]ds[/cci] は何も行わないようになっているのだが、これはなぜなのかわからない。wasavi ではその場合であっても引用符が削除されるようにした。したというか、Range Symbols の仕組みを流用した結果そうなったということなのだが。この点も surround.vim とはちょっと違う。

Surround the world #5

次に包囲文字列を削除する処理を書く。

この処理は、[cci]ds[/cci] + 包囲識別子というストロークによって起動される。また包囲識別子は、Range Symbols に含まれるか否かで 2 つに大別される。

包囲識別子が Range Symbols に含まれかつ [cci]pswW[/cci] のいずれでもない場合、つまり [cci]”‘`[]{}<>()Bbt[/cci] のいずれかである場合、Range Symbols のディスパッチャを先がけて呼び、選択範囲を構築する。

含まれない場合は、包囲の開始文字列及び終了文字列は包囲識別子それ自身の 1 文字になる。そして、自前でカーソル位置の前後を走査して包囲文字列の場所を発見しつつ、自前で選択範囲を構築しないといけない。

それ自体は特に問題はないのだけど。

ところで、Range Symbols のディスパッチャは quotechar オプションの値を認識する。quotechar の値は通常は [cci]\[/cci] だ。ディスパッチャは quotechar を前置している文字について、Range Symbol の境界ではないと判断する。つまり、[cci]”foo \” bar”[/cci] みたいな文字列に対して、[cci]i”[/cci] や [cci]a”[/cci] が正しく機能するようになっている。

しかし Range Symbols に含まれない文字の場合、そういう考慮はなされない。やろうと思えばできるんだろうけど、Surround.vim ではそういう考慮はなされない。なので、たとえば [cci]”#foo \# bar#”[/cci] なんて文字で [cci]ds#[/cci] は思ったような結果にはならない。

これどうしたものだろうか。Range Symbols ディスパッチャに特別に引用符を渡せるようなオプションを新設すれば、思ったような結果を得ることはできるが、Surround.vim の動作とは違ってしまう。

Surround the world #4

[cci]insertSurrounding()[/cci] の中身を埋める。

まず文字単位の場合。包囲文字列を挿入する方法として、2 つのアプローチが考えられる。まず、包囲文字列が挿入される左端と右端それぞれで囲まれた領域を選択し、包囲文字列を連結した文字列で上書きするというもの。こちらの方が undo クラスタのサイズが小さく、またコードもシンプルだ。

もう 1 つは、挿入される左端の位置にカーソルを移動させ、包囲文字列(左)を挿入。右端にカーソルを移動させ、包囲文字列(右)を挿入。最期に包囲文字列(左)の先頭へカーソルを戻す…というもの。コードは第 1 アプローチに比べるとやや冗長になる。

どちらを選択するのかといえば、第 2 アプローチになる。なぜなら選択領域内にマークが設定してある場合、第 1 アプローチだと選択領域を一旦削除した段階でマークが折りたたまれてしまうからだ。

次に行単位の場合。これが面倒。行単位の包囲文字列の挿入は、例えば

....abc
....def
....ghi....

のような状態([cci].[/cci] は U+0020 を表す)で 1行目にカーソルがある時 [cci]ySG”[/cci] すると

...."
........abc
........def
........ghi
...."....

という感じになる。

  1. 左の包囲文字列を挿入する位置は、選択範囲の左端点のある行の、最初の非空白文字の直前
  2. 右の包囲文字列を挿入する位置は、選択範囲の右端点のある行の、最後の非空白文字の直後
  3. 包囲される領域の内容は、左の包囲文字列のインデントレベルからさらに 1 つインデントさせる
  4. 右の包囲文字列の直前位置には、左の包囲文字列のインデントレベルと同じだけの空白文字を挿入する

くらいが気をつける点だ。前述の通り、本来は 3. については [cci]=[/cci] コマンドによって再インデントさせなければならないのだが、wasavi はそれをまだ実装していないので、代替として単に shift させることになる。

というわけでそういうふうに組んだ。マーク位置を壊さないために冗長なアプローチを選択する必要があるのは文字志向の場合と同じ。

* * *

レジスタの内容についてなのだけど、surround.vim の s:dosurround() で [cci]”[/cci] レジスタの退避と復帰をやっている。つまり番号付きレジスタや [cci]-[/cci] レジスタが更新されるのは考慮から漏れてるだけで、基本的にはレジスタの内容は包囲文字列の挿入によって変化させないという方向性なのかもしれない。

というわけでそういうふうにした。つまり一切レジスタの内容は更新しないようにした。

Surround the world #3

なんだか妙に時間が空いてしまったのだけど、続き。

ys 系で新しく包囲文字列を挿入するのを考える。このとき、モーションによって生成される内部的な選択範囲か、あるいは visual モードでの明示的な選択範囲が対象になるわけなのだが選択範囲が文字志向か行志向かで挿入される位置などが若干違う。

文字志向の場合

  • 基本の動作は、選択範囲の両端点に左右の包囲文字列を挿入する
  • レジスタの内容は、選択範囲の内容であり、やはり文字志向である。無名レジスタは変更されない。従って再利用できるとしたら [cci]”1[/cci] または [cci]”-[/cci] レジスタのどちらかになる
  • ys の前にカウンタが指定されている場合、無視される。モーションおよびテキストオブジェクトに対するカウンタはそれらに対してのみ参照され、surrounding に対しては使用されない
  • 挿入後、カーソルは選択範囲の左の端点に挿入した包囲文字列の先頭位置に置かれる
  • undo は両端点に対する挿入が一つのクラスタにまとめられた単位

行志向の場合

  • 基本の動作は、選択範囲の左端点には左の包囲文字列 + 改行、右端点には改行 + 右の包囲文字列を挿入する。選択範囲の内容は適切にインデントする
  • レジスタの内容は、選択範囲の内容であるが、何故か文字志向であるのを含めて ys と同様
  • カウンタの扱いは、ys と同様
  • 挿入後のカーソル位置は、ys と同様
  • undo の単位も ys と同様に挿入される一式が単一クラスタになる

というわけで双方で異なるのは選択範囲に実際に挿入される包囲文字列なのだが、「結果的に双方で同じ」なのがレジスタの内容だ。このへんの重箱の隅はそんなに合わせる必要はないかもしれない。

* * *

ところでさらにいくつか気がかりな点がある。vim のテキストオブジェクトには [cci]t[/cci] がある。これは SGML のタグを表す。しかし wasavi ではこれを実装していないのである。

wasavi の Range Symbols というのは、実は vim の search.c の目コピーなので単にそこから移植すればいいのだけれど、前にも書いたと思うが行中のタグを見つけるというのは本来シンタックス・パーザーの仕事なのである。Range Symbol クラスがそれのサブセットを持つのは無駄だ。なので、実装するにしても色分けその他諸々を先に片付けた上でその成果物を利用して…みたいに考えていた。

がしかし Surround.vim を実装するとなるとやはり [cci]t[/cci] がないと魅力半減な感じはすごくする。うーんどうしたものかな。

もうひとつ。複数行に対して包囲した場合、その内容がインデントされるのだが。このインデント処理とは具体的に何かといえば、vim で言うところの [cci]'[=’][/cci] なのである。wasavi は [cci]=[/cci] コマンドを実装していない。これも本来はファイルタイプに応じたインデント処理を実装するのが先のはずなのだけど。