最初に僕がこいつを直そうと思ったのは、cs の挙動が明かに変だからでした。まずはオリジナルのこの挙動を見てください(カーソルは |)。
if (cond) {
| ...
}
cs}]
if (cond) [| ...
]
Crazy!! なんだこれは! 'Change' 言うからにはこうなるはずですよね。
if (cond) {
| ...
}
cs}]
if (cond) [
| ...
]
こんなのが許されていいはずがない。そんな正義感にあふれていた頃が俺にもありました。
surrounding とは
しかしたいていのプログラムは、往々にして少し直すと全部直したくなるものです。コードを直していて気がついたのは、プラグインの内部において surrounding というものの定義が曖昧で、「何を削除」して「何を追加」するか、という基準が場当たり的に見えたことです。だから場合によっては必要なものを消してしまったり、不要なものを残してしまったりする。
だからまず初めに surrounding というものを定義しなおす必要がありました。ということで私は surrounding というものを再定義するところから始めました。私は surrounding を以下のように定義します。
- 削除時の surrounding
- outer object と inner object の差分とする。すなわち outer = inner + surrounding という関係が常に成り立つ。
- 追加時の surrounding
- 「何で囲む」かはプラグイン内で別途定義する。
ここでもっとも重要なのは、削除時と追加時の surrounding の定義が場合分けされていることです。つまり「何を削除」すべきかは、surround.vim が知らなくても明らかなのです。
例えばブレース {} で囲まれたブロックがあったとして、da{ は {} 含めてすべて削除し、di{ はブレースの中身のみ削除します。であればこの差分 {} こそ surrounding ではなかろうか、と考えるのは極めて自然でしょう。
しかしまあ改行を含むブロックに置いては di{ で改行が残ったりという場合もあり正確な差分を取るとおかしくなるので、適度に空白文字の調整は必要なのですが、内部的なことにはとりあえず触れません。
anyakichi 版 surround.vim
はい、ということでできあがったものがGitHubにあります(オリジナルから fork しています)。これを使って最初の例をやり直してみましょう。
if (cond) {
| ...
}
cs{[
if (cond) [
| ...
]
Great!! 最高だ! ・・・というか、これが普通だろ?
そして anyakichi 版 surround.vim にとって、削除する場合の surrounding object は outer と inner の object の差分です。先のエントリの textobj-xbrackets.vim を使用して、結果を確認してみましょう。
printf("%f\n", (s|qrt(f) + 1));
dsxb -> printf("%f\n", (f + 1));
csxbf -> function: log
printf("%f\n", (log(f) + 1));
dsxb は \k\+\s*(_) というオブジェクトにマッチします。アンダースコアが inner object です。逆に surrounding object はアンダースコア以外の場所全部なので、今回の例では関数の引数以外を剥がすような動作になります。
csxbf は、まず初めに dsxb 相当の動作で関数の引数以外を剥がします。さらにその上で、surrounding object の f を使用して、function で包みます(function っていう考え方は正直微妙なんだけどとりあえずそれはいいや)。そうすると裸になった f が log(_) で包まれて、結果は log(f) となります。
ここで () は見かけ上は change の前後で変わっていませんが、この () は change の前後で別のものです(一回消してから付け直している)。このことは非常に重要ですが、結果は同じなので忘れましょう。
anyakichi 版のマッピング
直している途中でいろいろオリジナルとの互換性を諦めたので、anyakichi 版はマッピングから何からかなり違います。主なマッピングは以下の通りです。
- ds
- surrounding object の削除。オリジナルと同じ。
- cs
- surrounding object の変更。オリジナルと同じ。
- ysg@
- text object のインライン surrounding。オリジナルと同じ。
- ygsg@
- text object のブロック surrounding。オリジナルの ySg@ 相当。
- yss
- カーソル行のインライン surrounding。オリジナルと同じ。
- yS
- カーソル位置から行末までのインライン surrounding。オリジナルでは ys$
- ygss / ygsgs
- カーソル行のブロック surrounding。オリジナルでは ySS。
- ygS
- カーソル位置から行末までのブロック surrounding。オリジナルでは yS$
概ね yS を ygs に引っ越しして、S を D などと同様に行末までのアクションに当てたという感じです。むしろこの方が自然に使ってもらえるのではないかと思います。もしもオリジナルのマッピングの方が良いのであれば、.vimrc で以下を設定してください。
let g:surround_old_mappings = 1
ただしマッピングが同じなだけで、挙動は違う場合があります。
anyakichi 版の surrounding object 設定
vim7 以降を対象として、辞書で設定します。
let g:surround_objects = {
\ "j[": "「\r」", "j]": "「 \r 」",
\ "j{": "『\r』", "j}": "『 \r 』",
\}
マルチストロークにも対応しています。デフォルトでは g:surround_objects でユーザが設定したものに、surround.vim が自身のデフォルト値をマージします。もしもデフォルト値のマージをやめたい場合には、以下を設定してください。
let g:surround_no_default_objects = 1
なお、オリジナルでは開きカッコでカッコの内側に空白が入り、閉じカッコで空白なしとなっていましたが、これは逆にしています。これは良く使う上に動作として単純な空白なしの方を、近いキーにマップするというコンセプトです。
さらに、\1 などの機能が拡張されて、パターン変換だけではなく任意のコードが実行できるようになっています。
let g:surround_objects['a'] = "<a href=\"\1\es:geturl()\1\">title</a>"
のように \1 などのあとに \e を入れておくと、そこから次の \1 までが eval() されます。eval() の戻り値で \1 内が置換されます。
anyakichi 版の削除時 surrounding object 設定
surround.vim の内部ではできません。自身で text object を定義しましょう。例えば日本語のカッコに対応する削除時 surrounding object が欲しい場合は、textobj-user.vim を使う場合は以下のようにすれば良いでしょう。
scriptencoding utf-8
call textobj#user#plugin('kakko', {
\ 'kakko': {
\ '*pattern*': ['(', ')'],
\ 'select-a': ["aj(", "aj)"],
\ 'select-i': ["ij(", "ij)"],
\ },
\ 'kagikakko': {
\ '*pattern*': ['「', '」'],
\ 'select-a': ["aj[", "aj]"],
\ 'select-i': ["ij[", "ij]"],
\ },
\ 'kagikakko2': {
\ '*pattern*': ['『', '』'],
\ 'select-a': ["aj{", "aj}"],
\ 'select-i': ["ij{", "ij}"],
\ },
\})
これで dsj[ などが動くようになります。
anyakichi 版の拡張機能
拡張と言うか、変更かもしれません。そもそも僕は今回の変更を改良ではなく改造と位置づけています(つまり、良くなったかどうかは知らんということです)。
まずは次の例を見てみましょう。
( abc )
dsb ->
abc
ds<Space>b ->
abc
ds および cs においては、オリジナルの surround.vim においても <Space> が入ると surrounding object を囲む空白を除去します。しかし anyakichi 版においては、さらにこれを推し進めて、囲んでいるものの前後の空白も(場合によって)除去します。これが有用なのは以下のような場面です。
if (cond) {
| return NULL;
} else
dsB ->
if (cond)
return NULL;
else
ds<Space>B ->
if (cond)
return NULL;
else
赤で示した空白が、<Space>つきだと削除されています。なお、オリジナル版の挙動は <Space> の有無にかかわらずやっぱりおかしいです。
また逆に、前後にスペースを足す機能もつけました。
if (cond)
stateme|nt.
else
statement.
ygss{ ->
if (cond)
{
statement.
}
else
statement.
ygss<Space>{ ->
if (cond) {
stateme|nt.
} else
statement.
より正確に言うと、ブロックで囲んだ上で前後の行を :join してしまう機能です。閉じカッコのつく位置がちょっと微妙に見えるかもしれませんが、これをどこにつけるべきか(インデントを浅くすべきか変えないべきか)を判断するのは難しい問題で、どうするべきかはよくわかりません。g:surround_indent = 1 にしておくか、適当なキーに `[=`] をマッピングして変更部分の reindent を行えるようにしておいた方が便利でしょう。私は後者を <Space>= にマップして使っています。
その他もろもろバグフィックスやら挙動の変更やらが大量にありますが、概ね直感的に使用できる方向に変更しているつもりです。もしもよろしければお使いください。