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] コマンドを実装していない。これも本来はファイルタイプに応じたインデント処理を実装するのが先のはずなのだけど。

number of users

Chrome 版の wasavi についてはユーザー数をストア上のページで知ることができる。

いままでたまに調べる程度ではだいたいユーザー数は 300 人とかそんなオーダーだったのだが。

今見てみたら 3,581 人いるというのである。なにそれこわい。前にも書いた気がするがユーザー数が増えたところで特に嬉しいことはない。かえって、アップデートの際のテストを真面目にしないといけないなぁ的なプレッシャーがのしかかってくる。いや今でも真面目にやってますけどね!

Surround the world #2

surround.vim の正確な動作仕様が知りたいのだけれど、それっぽい文書が見つからないのでソースと動作を見ながら目コピーせざるを得ない。

先立って用語がある。

  • 包囲領域: カーソル位置を含む選択範囲。前方に開始文字列、後方に終端文字列がある
  • 開始文字列: 包囲領域の先頭にある文字列。”(” “‘” のようにたいていは 1 文字だがタグの場合は複数の文字になりうる
  • 終端文字列: 包囲領域の末尾にある文字列。”)” “‘” のようにたいていは 1 文字だがタグの場合は複数の文字になりうる
  • 包囲識別子: 開始文字列、終端文字列へマッピングされる 1 文字の識別子:
    識別子 開始文字列 終端文字列 備考
    / /* */ C コメントを対象にする
    次のうちのいずれか: !#$%&*+,\-.:;=?@^_|~ 識別子そのもの
    次のうちのいずれか以外のテキストオブジェクトの末尾の1文字: pswW それぞれのテキストオブジェクトの先頭文字及び末尾文字。さらに、識別子が以下のいずれかである場合は開始文字列の末尾および終端文字列の先頭に U+0020 が付加される: ([{
    a < >
    r [ ]
  • 包囲識別文字列: 開始文字列、終端文字列へマッピングされる 1 文字以上の文字列。以下の例外を除き、包囲識別子と同じく 1 文字の入力をユーザーに求める。以下の場合はモードは line_input へ遷移する:
    • 最初の文字が [cci]tT^T<,[/cci] のいずれかであった場合、[cci]>[/cci] が入力されるまでは通常の line_input と同じ動作。初期値は [cci]<[/cci]。[cci]>[/cci] が入力されたら自動的に line_input モードを抜ける(これは一時的な cnomap により実現されている)
    • 最初の文字が [cci]l[/cci] または [cci]\[/cci] であった場合、…
    • 最初の文字が [cci]f[/cci] または [cci]F[/cci] であった場合、…
    • 最初の文字が [cci]^F[/cci] であった場合、…
    • 最初の文字が上記以外であった場合、その文字が入力されたら自動的に line_input モードを抜ける。
      以下の文字に関しては特別なマッピングが施される:

      文字 開始文字列 終端文字列
      b ( )
      B { および U+0020 U+0020 および }
      r [ ]
      a < >
      p \n \n\n
      s U+0020 (空文字)
      : : (空文字)
      上記以外の英字 (空文字) (空文字)

      またこのとき、U+0020 を前置することができる。U+0020 を前置した場合、包囲を新しく行う場合に開始文字列の直後、終端文字列の直前に U+0020 が追加される

* * *

surround.vimは大きく分けて normal モード、input モード、visual モードのそれぞれに特定のマップを定義する。まず normal モードから見ていく。normal モードに定義されるマップは

  • [cci]ds[/cci]: ds + 包囲識別子 を入力する。包囲領域から開始文字列・終端文字列を削除する
  • [cci]cs[/cci]: cs + 包囲識別子(from) + 包囲識別文字列(to) を入力する。領域を囲んでいる from を to に変更する
  • [cci]cS[/cci]: cs と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]ys[/cci]: ys + (モーション | テキストオブジェクト) + 包囲識別文字列を入力する。モーションによって生成された範囲の先頭に開始文字列、末尾に終端文字列を追加する
  • [cci]yS[/cci]: ys と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]yss[/cci]: これは ys の Operation Alias に相当する。つまり暗黙的に [cci]_[/cci] モーションが指定されたことになる。従ってカーソル行全体が対象になるのだが、開始文字列が追加されるのは対象文字列の最初の非空白文字の直前、終端文字列が追加されるのは対象文字列の最後の非空白文字の直後である。なお、この機能はカウント n を前置することができ、その場合はカーソル行から n 行分が操作対象になる
  • [cci]ySs[/cci]: yss と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]ySS[/cci]: ySs と同じ

visual/select モードに定義されるマップは

  • [cci]S[/cci]: (v|V|^V) + S + 包囲識別文字列を入力する。normal モードの ys に相当
  • [cci]gS[/cci]: (v|V|^V) + gS + 包囲識別文字列を入力する。normal モードの yS に相当

いくつか気になる点がある。

  • d/c オペレーションが認識する包囲領域は複数行に対応していないようだ。いいのかな?
  • [cci]ys[/cci] はちょっと動作とコマンドが乖離しすぎて違和感がある。なぜ y に割り当てたんだろう
  • [cci]ys[/cci] が包囲識別文字列を最後に入力させるのは実装もめんどうそう。ys + 包囲識別文字列 + (モーション | テキストオブジェクト) ならちょっとだけ楽だったんだけど
  • 言うまでもなく、vi コマンドのオペレーションはオペレータとモーションの組み合わせで構成され、そこには直交性がある。が、surround.vim の場合決め打ちで最低限のマップ定義しかしてないので直交性が崩れている。いいのかな? ただし、たとえば [cci]>s”[/cci] とかできてもあんまり意味はないのは確かではある

しかし surround.vim 自体がすでにほぼ10年の歴史のあるプラグインであるので、世の中のユーザーはこの仕様にすっかり馴染んでいると思われる。なのでできるだけ尊重してそのまんま移植するのが正解であろう。

Surround the world

issue の中に、surround.vim をサポートしてほしいというものがある。

最初に告白しておくと、個人的には surround.vim 以前に vim のこの手のプラグインはまったく使っていない(別に使わない主義とかそういうわけではない)。なので、便利であれば実装することにやぶさかではないのだけれど、プラグインってなに? というところから調査しないといけない。

vim のプラグインは、本質的には vimscript のソースで、これは indent や syntax や colors で使うファイルも同様だ。つまり結局のところ全部 vimscript なのだが、それぞれで何を行うかが暗黙の了解的に区分けられていたり読み込まれるタイミングが違ったりはする。

まあそれはそれとして、とりあえず surround.vim を読んでみる。このファイルを ~/.vim/plugin/ に置くと、テキストオブジェクトが結果的に拡張されて i なんちゃらとか a なんちゃらの他に s なんちゃらと S なんちゃら、および ss/Ss/SS なんちゃらが使えるようになる。また visual モードでもいくつかの拡張が行われる。

結果的にというのは、本来テキストオブジェクトを拡張するとしたらコマンドディスパッチャへのフックとかそういう話になると思うのだが、surround.vim はそれを単に map で処理しているからだ。これはこれでなるほどという感じはする。ただ、これを wasavi に組み込むとなると、やはりコマンドディスパッチャに融合させる形になると思う。map で処理してもいいけどタイムアウト処理とかを意識する必要が出てくる。

0.6.410 released

各エクステンションストアにリリースした。ただし例によって Firefox 版は beta 扱い。

それはさておき。

wasavi の設定って、エクステンションの localStorage もしくは SimpleStorage に保存してある。つまりローカルに持っている。一方で、例えば Chrome には、chrome.storage.sync のように自動的に複数のデバイス間で同期が行われるストレージもある。もしかして、wasavi の設定もそういうストレージに置いたほうが便利なのではなかろうか?

と思って実装してみようとしたのだが、やっぱりやめた。同期が行われるということは wasavi の設定が google 先生のサーバとやりとりされるということだ。もし exrc にムフフな情報を書いていた場合、それが漏れないという保証ができない。いやまあそうそう漏れることはないんでしょうけど。

実装するとしたら自動的ではなくてユーザーのアクションをトリガーにするような半自動的な物のほうがよいのかもしれない。よくないかもしれない。