swap.vim を書きました

思い返してみると、実際たいしたことはないうえに頻繁にあるわけでもないんですけど、関数の引数だったり配列の要素だったりの順番を入れ替えたくなることがあります。

call func(arg1, arg2, arg3)

これが例えば arg2arg3 を入れ替えるとかだったら問題ないんです。 , arg2 を delete して、

call func(arg1, arg3)

arg3 の後ろへ paste 、

call func(arg1, arg3, arg2)

できましたね。世の中には vim-textobj-parameter あるいは fork版 という便利なものがあるので、これらを使えばいっそう心の平穏が保たれます。ところが arg1arg3 を入れ替えるとなると、うってかわって心穏やかではいられません。 arg1 を切り取ったり、コンマを移動させたり面倒なわけです。この辺 Vim がうまいことやってくれるといいなー、とは思っていました。

ところが、上の例くらい簡単ならいいんですけど、関数が引数に関数を取ったり、

call func(arg1, func(arg1, arg2, arg3), arg3)

任意の文字列を含んだり、

call func(arg1, 'foo, bar')

してくると、なかなか複雑です。しかも、言語によって違う部分もあるので難しい。そんなわけでしばらく放置していたんですけど、まあそこそこいい感じに動く、ぐらいなら可能かもしれないと思いたって swap.vim を書きました。

このプラグインは三つのキーマッピング g<, g>, gs を定義します。g< はカーソル下のアイテムを直前のアイテムと入れ替えます。 g> はカーソル下のアイテムを直後のアイテムと入れ替えます。最初の例

call func(arg1, arg2, arg3)

でいえば、カーソルを arg2 に置いて g< を入力すると arg2arg1 が入れ替わり、

call func(arg2, arg1, arg3)

一方、 g> を入力すると arg2arg3 が入れ替わります、

call func(arg1, arg3, arg2)

gs はちょっと変わり種で、 swap mode と呼んでいるモードのようなものを開始して、インタラクティブに要素入れ替えを行います。 swap mode では h, l によって隣り合う要素の入れ替え、 j, k によるカーソルの移動、 u, <C-r> による簡単なアンドゥ・リドゥ、数字 1 から 9 による入れ替えアイテムの直接指定ができます。納得のいく順番になったらご存知のように <Esc> でノーマルモードに戻ります。

これらはドットリピートで繰り返すことができますが、注意する点が一つあります。 g<, g> は常にカーソル下のアイテムとの相対位置で働く一方で、 gs が繰り返される場合は何番目のアイテムか、という絶対位置で働きます。この違いを覚えておかないと予期しない結果を得るでしょう。

一応、非明示的な行継続がある場合でもいい感じにできるようにインデントをスキップするようにしています。また、設定次第では行継続文字を無視もできます。例えば Vim script であればデフォルトで設定されているので行頭の \ をスキップします。

demo

大きなブロック (移動しているもの) があるのは {} によるグループ化を考慮しているためです。

julialang v0.4

先週 julia の最新安定バージョンの 0.4 がリリースされたみたいですね。早速ビルドしてみました。Windows を使っているので msys2 を使ってここにあるとおりにビルドしてます。ちょっと前まで master をビルドしようとすると gmp のビルドでこけてたんですけど なんかいつの間にか治ってました。 issue 閉じなくていいのかな。

ビルドはなぜか openblas が cpu の自動認識に失敗したので OPENBLAS_TARGET_ARCH を直接指定してもう一度 make 走らせたらいけました。

make -j 4 OPENBLAS_TARGET_ARCH=HASWELL

こんな感じ。OPENBLAS_TARGET_ARCH に指定できる物は deps/openblas/TargetList.txt に列挙されてます。不思議なのですが openblas 単体でビルドしようとすると問題なく cpu 認識するようです。何が違うんだろうか。

もう一つ、なぜか usr/lib/sys.ji が生成されませんでした。多分これが原因だと思うんですけど、うちの環境だと起動に 5s 前後かかってしまう…。 0.3.11 のときはビルド時に自動的に生成されていたと思うんですけど。地味につらいのでいろいろ考えた結果、よく見ると usr/lib/sys.dll は生成されているようなので、起動時に --precompiled=yes つけるようにすると <1s で起動するようになりました。とりあえず常に --precompiled=yes つけるバッチファイル書いてパス通ったところにおいて満足しています。

@echo off
path\to\julia --precompiled=yes %*

実行オプションに --output-ji とか system image 生成するためのものがあるっぽいんですけどうまくいかない…。

ERROR: could not open file boot.jl

とか言われる。

まだちょっと触っただけですけど、 Incremental code caching とかいう新機能のおかげで package の読み込みがかなり早くなっているのに驚きました。初回使用時にコンパイルのために待つことになるので、いっそのこと Pkg.add() のときにやってくれてもいいかなー、と思いました。コンパイルされたパッケージのイメージファイル (多分、~/.julia/lib/ 以下のファイル) が生成されてしまえば以降は速いですね。多分パッケージをアップデートしたらまた、コンパイルしなおす感じでしょう。一番最初に julia をさわったときは起動の遅さとパッケージ読み込みの遅さ(Gadflyとか)が引っかかっていたので、今回ので解消されてしまいました。いくつかコンパイルできなかったパッケージもあったみたいなので、もしかしたら将来もう少し早くなるのかも。

もともと数値計算を主眼に置いているので、息をするように複素数を扱えて、瞬きをするように行列計算できるうえ、多重ループ書いても速いとか夢みたいな言語なのでひそかに応援してます。

Vim のドットリピートとテスト

2018/8/29 追記
以下に書いている問題は v8.0.0548 以降解消されています。


たまに Vim の拡張としてオペレータを書くのですが、テストを書こうとすると困ることがあります。例として、オペレータを書いてみました。指定された領域の先端に a を、末端に b を入力するオペレータです。

function! OperatorInputAB(motionwise) abort
  let head = getpos("'[")
  let tail = getpos("']")

  call setpos('.', tail)
  normal! ab

  call setpos('.', head)
  normal! ia
endfunction

nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@

今回は、ビジュアルモードは考えません。またカウント、レジスター、 operatorfunc に渡される引数も使いません。詳しくは、 :help opfunc 及び :help map-operator を参照願います。

さて、簡単で、なんら役に立たないものではありますが、ひとまずオペレータができました。しかし、このオペレータのドットコマンド時の挙動をテストしようと思うとなかなかうまくいきません。 vim-themis を例にとりますが、次のようなテストを想定しています。

let g:assert = themis#helper('assert')
let s:suite  = themis#suite('test: ')

function! s:suite.before() abort
  function! OperatorInputAB(motionwise) abort
    let head = getpos("'[")
    let tail = getpos("']")

    call setpos('.', tail)
    normal! ab

    call setpos('.', head)
    normal! ia
  endfunction

  nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@
endfunction

function! s:suite.dotrepeat() abort
  call setline('.', 'foo')

  " test 1
  normal 0\aiw
  call g:assert.equals(getline('.'), 'afoob', 'failed after an operator command')

  " test 2
  normal .
  call g:assert.equals(getline('.'), 'aafoobb', 'failed after a dot-repeating')
endfunction

実行した結果を見ると、

not ok 1 - test:  dotrepeat
# The equivalent values were expected, but it was not the case.
#
#     expected: 'aafoobb'
#          got: 'aafoob'
#
# failed after a dot-repeating

# tests 1
# passes 0
# fails 1

test 2 のところでこけていますね。なぜか、 b が最後に入力されていません。詳しくはわからないのですが、どうも要約するとドットリピートで繰り返される単位の更新のタイミングの問題のようです。次の二つのスクリプトを :source してみると、一見同じことをしているように見えて別の結果が得られます。

  • a.vim
function! OperatorInputAB(motionwise) abort
  let head = getpos("'[")
  let tail = getpos("']")

  call setpos('.', tail)
  normal! ab

  call setpos('.', head)
  normal! ia
endfunction

nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@

call setline('.', 'foo')
normal \aiw
normal .
  • b.vim
function! OperatorInputAB(motionwise) abort
  let head = getpos("'[")
  let tail = getpos("']")

  call setpos('.', tail)
  normal! ab

  call setpos('.', head)
  normal! ia
endfunction

nnoremap <silent> \a :set operatorfunc=OperatorInputAB<CR>g@

function! s:operation()
  normal \aiw
  normal .
endfunction

call setline('.', 'foo')
call s:operation()

ほぼ同じことをしているように見えますが、 a.vim の結果は意図通りの aafoobb 、しかし b.vim の結果は最後の b が欠けて aafoob です。違いは a.vim が関数の外でドットリピートしているのに対し、 b.vim は関数の中でドットリピートをしている点です。どうやら、ドットリピートの単位はすべての関数を抜ける時にまとめて更新されるらしく、何らかの関数の中でドットリピートした場合は OperatorInputAB() の中の最後の編集 normal! ia のみがドットリピートの対象になっているみたいです。

と、いうことは逆に考えると a.vim をもう少し工夫するとドットリピート用のテストが書けて、あとはバッチなりシェルスクリプトなりを用意すれば大勝利、となるわけですけど未だ肝心のテストはかけてないです。書かないと、はー。

Vimのpatch-7.4.849について

Vim の patch-7.4.849 欲しさに野良ビルドに手を出してしまいました。これは例えば括弧を自動的に閉じるような設定をしている場合に便利なのです。

inoremap ( ()<Left>

この設定自体には賛否両論あるらしいのですがその点はスルーします。ただ、賛だとしても Vim の操作として考えた時大きな短所があるのは認めざるを得なくて、今まではその点に目をつぶりながら似たような設定を使ってきました。

その短所がどういうものかというと、例えば上記のような設定をせずに、空のバッファで a(foo)<Esc>. と打ち込むと当然バッファの内容は次の通りになります。

(foo)(foo)

. は直近の入力を繰り返しているのでこうなりますね。期待通りの結果だと思います。しかし、上記のような設定をしていた場合(その場合は入力は a(foo<Esc>l.)はこうはならず、次のようになります。

(foo)foo

これは設定に含まれている <Left> キーが少し特殊な扱いになるからです。一部のキーは、挿入モードで押下されると、あたかもそこでいったん挿入モードを抜け、もう一度挿入モードに入ったかのように解釈されます。 <Left><Right> のような文字入力を伴わずにカーソルを移動するものや、 <C-o> のように挿入モードでノーマルモードコマンドを使うためのキーなどを含めいろいろあり :help ins-special-special に列挙されています。さて、この関係で上記のような設定をしていた場合、キー入力は a(foo<Esc>l. ですが、キーマップを展開すると a()<Left>foo<Esc>l. となります。 . が繰り返す直近の入力は <Left> より後の foo のみ、とみなされたというわけです。<Left> でインサートモードを抜けて、カーソルを移動、インサートモードに入りなおしている、と考えるとここでドットリピートの単位が区切られてしまっているのです。

小さいといえば小さいのですけど、これがなかなか入力を繰り返す時にネックになることが多いのです。私は smartinput を使って似たような設定を使っていたのですけど、この問題のために一時的にすべての設定を停止するキーマップをvimrcに書いてなんとか誤魔化していました。とはいえ、このキーマップも意図的に操作しないといけないので、先に繰り返すと決めていた場合にしか効果がないので気休めにしかなりません。件の挙動はヘルプにもしっかり書いてあるし、仕様だと諦めていたところに Patch-7.4.849 がきました。これはこのように使います。

inoremap ( ()<C-g>U<Left>

<Left><Right> の前に <C-g>U を挟むことでドットリピートで繰り返される単位を区切らないようにすることができるようになりました。()<Left>foo の入力を繰り返すということですね。すばらしい。ただし、制限がないわけではなくて、どうも同一の行内で編集が完結する場合のみしか有効ではないみたいです。

" | はカーソル
{|}

これを <Enter> 一発で

{
    |
}

こうするような設定とかだとダメなんでしょう。使ってないからいいかな。 <C-g>U はまだ使い始めたところだけどいろいろ便利そうです。単純に括弧の件だけでも十分すぎるほどに朗報で興奮しています。

完全に余談ではあるけどビルドにあたって、ずっと欲しいと思っていた このパッチ をあててみました。もやもやしていたのが解決されて、とても幸せです。これも早く本体に取り込まれてほしいです。

Vim の自動インデント機能について

必要になっていろいろ調べてみると、思いのほか分かっていなかったのでまとめました。上から調べて行って該当するものが使われるようです。


  1. 'equalprg' オプションが空でなければ指定された外部プログラムが使われる。

  2. 'lisp' オプションがオンなら組み込みで定義された lisp 標準(の一種)のインデントがなされる。

    • 'autoindent' をオンにする必要がある。
  3. 'indentexpr' が空でなければ 'indentexpr' の評価によってインデント深さを計算する。

    • ユーザーが Vim script によってインデントを定義することができる。
    • :filetype indent on によって有効になるインデントの多くは $VIMRUNTIME/indent/{ファイルタイプ名}.vim で定義されるインデント関数を 'indentexpr' に設定することによって実現される。
    • 'indentkeys' オプションによってトリガーとなるキーを設定できる。
  4. 'cindent' がオンなら組み込みのC言語向けインデントを行う。

    • 'cinkeys' オプションによってトリガーとなるキーを設定できる。
    • 'cinoptions' オプションによって細かい調整ができる。
    • 'cinwords' に登録されたキーワードに動作が依存する。
  5. 'smartindent' がオンなら組み込みのC言語風のインデントを行う。

    • 'cindent' のほうが厳密。
    • 'autoindent' をオンにする必要がある。
    • 'cinwords' に登録されたキーワードに動作が依存する。
  6. 'autoindent' がオンなら自動インデントが行われる。

    • 新しい行は常に元の行と同じインデント、という簡単なもの。
  7. 自動インデントをしない。


'lisp''smartindent''autoindent' を要求するのは知らなかったのですが 'smartindent''autoindent' なしでも動いているような? あと 'equalprg' って具体的にどういうプログラムを指定するんでしょう?

たまに見る

vimrc に set noautoindent 書いたのに自動インデントがオフにならない。

のようなのは、ファイル読み込みのときにバッファローカルな 'indentexpr' が設定されているような気がします。

:verbose setlocal indentexpr

を実行して 'indentexpr' が空でなければ vimrc に

augroup vimrc
    autocmd FileType {ファイルタイプ名} setlocal indentexpr=
augroup END

と書くか、あるいは ~/.vim/after/indent/{ファイルタイプ名}.vim あたりに

setlocal indentexpr=

と書くのがよいでしょう。 vimrc に書く場合 :filetype indent on より後に書かなければ有効になりません。

さらにそれでもまだ自動でインデントされるなら、

verbose setlocal equalprg?
verbose setlocal lisp?
verbose setlocal cindent?
verbose setlocal smartindent?
verbose setlocal autoindent?

を順に実行して犯人探しというのが筋ですが、 ~/.vim/after/indent/{ファイルタイプ名}.vim あたりに

setlocal equalprg=
setlocal nolisp
setlocal indentexpr=
setlocal nocindent
setlocal nosmartindent
setlocal noautoindent

と書くのが手っ取り早いですね。'equalprg' が空でなかったり 'lisp' がオンの場合はなかなかないので上二行はいらないかもしれません。

参考

  • :help =
  • :help 'equalprg'
  • :help 'lisp'
  • :help 'indentexpr'
  • :help 'cindent'
  • :help 'smartindent'
  • :help 'autoindent'

Vimの組み込み関数について

Vim の組み込み関数も結構変更を受けたりしていて、古い Vim だと動かない機能があったりする。これは仕方がない。どちらかというと今なお活発に開発が行われているということで、いいことだと思う。ただ、困るのはそれがいつからだったか調べるのが大変面倒な作業だという点だ。ひとまず組み込み関数の追加・変更履歴を調べた。大体、7.4以降の話でそれより前は大変なのでカット。どこかにこういうのがまとまってるとこがあると助かるんだけど。コマンド、オプション、オートコマンドイベントなどもまとまってるとありがたい。

追加・変更リスト

arglistid()

  • patch-7.4.312: 追加。

and()

  • patch-7.3.377: 追加。

byteidxcomp()

  • patch-7.4.057: 追加。

char2nr()

  • patch-7.3.780: 第二引数{utf8}の追加。現在のエンコーディングにかかわらず常に{expr}を utf-8 として扱う。 #32, #149

cursor()

  • patch-7.4.310: 第一引数{list}に第四要素curswantを指定できるように。

eval()

  • patch-7.4.574: eval('$')がエラーを返すように修正。 #687

executable()

  • patch-7.4.428: MS-Windowsで間違った値を返すのを修正。 #449

exepath()

  • patch-7.4.235: 追加。

exists()

  • patch-7.4.268: スクリプトローカル関数の存在を確認できないのを修正。

expand()

  • patch-7.3.065: <slnum>の追加。現在の行番号へと展開される特殊引数。
  • patch-7.3.465: 第三引数{list}の追加。返り値をリストとして受け取れるように。
  • patch-7.4.423: シェル変数や環境変数を展開できていなかったのを修正。
  • patch-7.4.465: 長い文字列でクラッシュするのを修正。 #649

feedkeys()

  • patch-7.4.601: 第二引数{mode}'i'を指定できるように。

getbufvar()

  • patch-7.3.831: 第三引数{def}の追加。タブ、ウィンドウ、バッファあるいは変数が存在しない場合は{def}に指定された値を返す。 #245

getchar()

  • patch-7.4.306: 端末で使用時に、第一引数{expr}01を与えた場合<Esc>キーがすぐに取得できないのを修正。
  • patch07.4.457: <expr>マッピングの中でgetchar()を呼ぶと、:autocmdイベント用の特殊なキーシークエンスをキャッチしてしまう。これを回避するために<Cursorhold>キーが定義された。 #607

getcmdwintype()

  • patch-7.4.392: 追加。

getcurpos()

  • patch-7.4.313: 追加。
  • patch-7.4.578: $コマンド使用後に返り値のcurswantが負数になっているのを修正。(パッチコメントでは空行でおこる、とあるが空でない行でも同様) #604

getpos()

  • patch-7.4.310: 第一引数{expr}に'.'を指定した場合、返り値のリストに第五要素curswantを追加するように変更。
  • patch-7.4.313: patch-7.4.310の変更を取消。代わりにgetcurpos()関数を追加。

getreg()

  • patch-7.4.242: 第三引数{list}の追加。返り値をリストとして受け取れるように。
  • patch-7.4.513: クラッシュを引き起こす恐れのあるバグを修正。詳細はよくわからず。リストを返す場合に起こる?

gettabvar()

  • patch-7.3.831: 第三引数{def}の追加。タブ、ウィンドウ、バッファあるいは変数が存在しない場合は{def}に指定された値を返す。 #245
  • patch-7.4.434: 第二引数{varname}に空文字を与えた場合スコープ変数を取得できるように。 #622
  • patch-7.4.442: (patch-7.4.434の修正?) #622

getwinvar()

  • patch-7.3.831: 第三引数{def}の追加。タブ、ウィンドウ、バッファあるいは変数が存在しない場合は{def}に指定された値を返す。 #245

glob()

  • patch-7.3.465: 第三引数{list}の追加。返り値をリストとして受け取れるように。
  • patch-7.4.654: 第四引数{alllinks}の追加。返り値にすべてのシンボリックリンクを含められるように。

globpath()

  • patch-7.4.279: 第四引数{list}の追加。返り値をリストとして受け取れるように。
  • patch-7.4.654: 第五引数{alllinks}の追加。返り値にすべてのシンボリックリンクを含められるように。

glob2regpat()

  • patch-7.4.668: 追加。

has()

  • patch-7.4.236: パッチがあてられているかをhas('patch-7.4.236')の形式で確認できるように。

input()

  • patch-7.4.047: マッピングから呼ばれた関数の中で動作しない。#469

invert()

  • patch-7.3.377: 追加。

luaeval()

  • patch-7.3.490: 追加。

map()

  • patch-7.4.525: {expr}が不正な場合にメモリリークするのを修正。
  • patch-7.4.708: 高速化。 #724

match()

  • patch-7.4.184: 第四引数{count}が正しく働かないのを修正。 #526, #527

matchadd()

  • patch-7.4.528: 'regexpengine'が0の時クラッシュする恐れのあるバグを修正。 #668

matchaddpos()

  • patch-7.4.330: 追加。
  • patch-7.4.343: (関連する?) #580
  • patch-7.4.362: 指定した長さよりも行が短い場合は行の最後までハイライトする(パッチコメントが逆のような?)。同時にマルチバイト文字に関連したバグも修正されている。 #586

matchdelete()

  • patch-7.4.343: (matchaddpos()によるハイライトを消す際に限り?)正しくハイライトを消せないのを修正。 #580

matchend()

  • patch-7.4.184: 第四引数{count}が正しく働かないのを修正。 #526, #527

matchstr()

  • patch-7.4.526: 長い文字列で失敗するのを修正。

mkdir()

  • patch-7.4.006: mkdir('foo/bar/', 'p')が末尾のパスセパレーターでエラーを吐くのを修正。
  • patch-7.4.010: patch-7.4.006 以降無効な引数でクラッシュするのを修正。

or()

  • patch-7.3.377: 追加。

screenattr()

  • patch-7.3.1164: 追加。

screenchar()

  • patch-7.3.1164: 追加。

screencol()

  • patch-7.3.748: 追加。

screenrow()

  • patch-7.3.748: 追加。

search()

  • patch-7.4.771: 第二引数{flags}に'bceW'、あるいは第二引数{flags}に'bce'と第三引数{stopline}を指定したときにカーソル直下のマルチバイト文字の検索に失敗するのを修正。 #747

searchpos()

  • patch-7.4.771: 第二引数{flags}に'bceW'、あるいは第二引数{flags}に'bce'と第三引数{stopline}を指定したときにカーソル直下のマルチバイト文字の検索に失敗するのを修正。 #747

setmatches()

  • patch-7.4.745: getmatches()の返り値をsetmatches()で復元できないのを修正。

setpos()

  • patch-7.4.310: 第二引数{list}が第五要素curswantを許容するように。

setreg()

  • patch-7.4.243: 第二引数{value}がリストを許容するように。
  • patch-7.4.249: 第二引数{value}に数値のリストを渡した場合に働かないのを修正。
  • patch-7.4.725: :call setreg('"', [])で内部エラーを出すのを修正。 #676

sha256()

  • patch-7.3.816: 追加。 #86

sort()

  • patch-7.3.224: 第三引数{dict}の追加。 "dict" 属性の関数が self を参照できる。
  • patch-7.4.341: 第二引数{func}'n'を指定できるように。要素を数値としてソートする(通常は文字列として扱われる)。
  • patch-7.4.351: 安定ソートに。
  • patch-7.4.358: 本当に安定ソートに。
  • patch-7.4.411: 長さの違う文字列のソートに問題があるのを修正。'foo bar'が'foo'よりも先にソートされていた。

strchars()

  • patch-7.4.755: 第二引数{skipcc}を追加。合成文字のカウント方法を指定できるように。

submatch()

  • patch-7.4.241: 第二引数{list}の追加。返り値をリストで受け取れるように。

substitute()

  • patch-7.4.045: \zeから始まるパターンで正しく動かないのを修正。
  • patch-7.4.158: (patch-7.4.045以降?) \zsを含むパターンが正しく動作しないのを修正。 #503
  • patch-7.4.323: ゼロ幅マッチがマルチバイト文字を破壊するのを修正。 #572
  • patch-7.4.499: 高速化。 #648

system()

  • patch-7.4.247: 第二引数{input}がリストを許容するように。
  • patch-7.4.427: :autocmdイベントInsertCharPreにて実行された場合に表示が崩れるのを修正。
  • patch-7.4.451: 空文字を引数に渡すとエラーを吐くのを修正。

systemlist()

  • patch-7.4.248: 追加。
  • patch-7.4.256: 正しく動いてなかったのを修正。 #548
  • patch-7.4.597: 返り値に変更を加えられないのを修正。

uniq()

  • patch-7.4.218: 追加。

wildmenumode()

  • patch-7.3.828: 追加。

winrestview()

  • patch-7.4.311: 第一引数の{dict}に全ての要素を指定する必要はなくするように変更。curswantのみ、toplineのみの指定などができる。
  • patch-7.4.491: toplineに負数を指定すると表示が崩れるのを修正。 #637

xor()

  • patch-7.3.377: 追加。

sandwich.vim を書きました

vim-sandwich という vim plugin を書きました。これは vim-surroundvim-operator-surround などのような、括弧やクオーテーションなどで囲まれた文字列を編集するためのプラグインと同種のものです。改行の扱いや矩形選択に対する挙動など、細かい点が自分好みなものが欲しくなったので書きました。

実のところ先駆者達と大きな違いありません。あえて挙げるとすれば、ドットリピートに外部ライブラリ(vim-repeat)を必要としない点、改行を含むパターンを扱えることなどが vim-surround と異なります。 vim-operator-surround との違いは補助用のテキストオブジェクトを同梱している点です。抱き合わせですね。ただ、補助用とは書きましたが単体でも使用できます。 vim-textobj-multiblockvim-textobj-anyblockvim-textobj-between (こちらは少し違う機能ですが)を想像していただければおよそそのままかと思います。このようなオペレータとテキストオブジェクトの必要とする情報/設定が似通ってきたので、まとめて管理したいと思ったのも書くきっかけでした。

オペレータは三種あります。それぞれ、囲みの追加/削除/置換を行います。削除あるいは置換される文字列は両端が同じ文字か、事前に設定された文字列のペア(例えば、 ())に一致した場合に編集されます。これは vim-operator-surround の定めているルールと同じです。

テキストオブジェクトは二種あります。一つはユーザーに入力を促し、入力に一致する設定があれば事前に設定されたペアに囲まれた領域を、一致するものがなければ入力された一文字に囲まれた領域を選択します。もう一つは事前に設定されたペアの中からカーソルに最も近いものを自動的に検索します。

囲みの削除/置換を行うオペレータはその性質上、補助用のテキストオブジェクトと常に一緒に使うのが便利なのでこれらの複合マッピングにバインドされています。まとめると操作は次のようになります。

  • 囲みを追加

sa にバインドされています。例えば saiw( と押下すると foo(foo) となります。

  • 囲みを削除

sd にバインドされています。例えば sd( と押下すると (foo)foo となります。また、 sdb と押下すると自動で検索するテキストオブジェクトが使われ、同じ結果が得られるでしょう。

  • 囲みを置換

sr にバインドされています。例えば sr(" と押下すると (foo)"foo" となります。また、上記と同様に srb" でも同じ結果を得られるでしょう。

  • テキストオブジェクト

ユーザーの入力を促すテキストオブジェクトは is 及び as に、自動で検索するテキストオブジェクトは ib 及び ab にバインドされています。 is 及び ib は囲みの内側のテキストを、 as 及び ab は囲みを含めた領域を選択します。

             |<----ib,is---->|
{surrounding}{surrounded text}{surrounding}
|<------------------ab,as---------------->|

いくつか特徴的な点を紹介します。

ハイライトについて

まず目に付くのがハイライト機能でしょう。囲みの追加/削除/置換において、挿入位置/削除される文字列/置換される文字列をハイライトします。囲みの削除においては削除の寸前に、ごく短い時間だけハイライトします。この時間は g:operator#sandwich#highlight_duration 変数で調節可能です。ハイライトの間もユーザーの入力をブロックせず、入力があれば即座に編集を終了します。しかし、それでもうっとうしいという方は次の行を vimrc に加えましょう。

 call operator#sandwich#set('delete', 'all', 'highlight', 0)

カウントについて

次にノーマルモードにおけるカウントの扱いが独特となっています。一般的にオペレータとモーションあるいはテキストオブジェクトの組み合わせは次のような順番で入力されます。

        [count1]{operator}[count2]{textobject}

通常のオペレータは [count1][count2] を区別しません。すなわち 3diwd3iw は全く等価です。両方を使った場合、そのカウントは二つのカウントの積になります。すなわち 2d3iw3d2iw は等しく、 6diw または d6iw にもまた等しくなります。これに対し、 sandwich.vim のオペレータは二者を区別します。 [count1] をオペレータに渡し、 [count2] をテキストオブジェクトへ渡します。すなわち 2sa3iw'' は二つの単語と間の空白を二度シングルクオーテーションで囲みます。2以上のカウントをオペレータに与えた場合デフォルトではカウントの回数だけユーザーに入力を促します。この挙動はオプションで制御可能です。

 call operator#sandwich#set('all', 'all', 'query_once', 0)

. コマンドで繰り返す際にカウントを与えた際には、カウントをテキストオブジェクトに渡し、オペレータは最後に与えられたカウントを使い続けます。

カスタマイズについて

さらに、 sandwich.vim には豊富なオプションがあります。カスタマイズをしようと思った時には大きな助けとなるでしょう。しかし、反面ドキュメントがそのために長くなっています。このためファイルを三つに分けています。 doc/sandwich.jax には共通部分について書かれており、これを眺めるだけでも使用には困らないでしょう。カスタマイズをしたくなったら doc/operator-sandwich.jax 及び doc/textobj-sandwich.jax をご覧ください。それぞれ分割してなお分量がありますが、 Vim にお詳しい方なら目次を眺めるだけでも何ができるかの見当がある程度つくかもしれません。


sandwich.vim