Vimの検索オフセットで、検索後のカーソル位置を指定する
検索してマッチした文字列の末尾にカーソルを移動させたいのに、毎回/wordのあとにeやlを何度も押して調整していた時期があります。検索コマンド自体に「マッチ位置からどれだけずらすか」を直接指定できる仕組みがあります。
この指定を検索オフセットと呼びます。書くのは検索キーワードの後ろに数文字だけで、移動先が変わるだけでなく、オペレータと組んだときの範囲まで変わります。この記事では着地位置と範囲を実測し、繰り返したときに何が引き継がれるのかまで見ていきます。検索そのものの打ち方は文字列検索の基本にまとめてあります。
-u NONE(設定を読まない状態)で打ち込んで確かめました。両者で結果が違った項目はありません。行と桁の数値はline(".")とcol(".")が返した値です。
目次(8項目)
基本の書き方
/{検索キーワード}/{オフセット}<Enter>
?{検索キーワード}?{オフセット}<Enter>
検索キーワードのあとにもう一度区切り文字(/または?)を挟んでオフセットを書きます。上方向検索の?を使う場合は、オフセット側の区切りも?にする必要がある点は間違えやすいので注意してください。区切りをもう1つ置くのは、パターンがどこで終わるのかをVimに伝えるためです。区切りが無ければ/bravoeという1つの単語との区別が付きません。
区切りを混ぜたときの挙動も確かめました。4行のバッファの最終行から?bravo?eを打つと3行9桁、つまり3行目のbravoの末尾に止まります。同じ位置から?bravo/eを打つとbravo/eという7文字のパターンとして扱われ、一致が無いのでカーソルは4行1桁から動きませんでした。このときの表示はE486: Pattern not found: bravo/eで、飲み込まれた区切りがそのまま出ます。パターンの側に区切り文字を含めたいならエスケープが必要で、path a/b hereという行を足したバッファで/a\/bと打つと、その行の6桁目、a/bの先頭に着きました。
オフセットの3系統と着地位置
オフセットは3系統に分かれます。数字だけ、あるいは+nと-nを書く行単位のもの。eを頭に付ける、一致の末尾を基準にした文字単位のもの。sまたはbを頭に付ける、一致の先頭を基準にした文字単位のものです。基準点が違うだけで書き方の形はそろっているので、3つの基準点を覚えれば残りは組み合わせで出てきます。
1行目がalpha bravo charlie、2行目がdelta echo foxtrotのバッファを作り、1行1桁にカーソルを置いた状態から/bravoに各オフセットを付けて打った結果が次の表です。1行目のbravoは7桁目から11桁目にあります。
| 打ったもの | 着地(行,桁) | その位置の文字 |
|---|---|---|
/bravo | 1,7 | b(一致の先頭) |
/bravo/e | 1,11 | o(一致の末尾) |
/bravo/e+2 | 1,13 | c |
/bravo/e-1 | 1,10 | v |
/bravo/b | 1,7 | b |
/bravo/b+2 | 1,9 | a |
/bravo/b-1 | 1,6 | 空白 |
/bravo/s+2 | 1,9 | a |
/bravo/1 | 2,1 | d |
/bravo/+1 | 2,1 | d |
/bravo/+ | 2,1 | d |
s+2とb+2が同じ1行9桁に着いているのは偶然ではありません。ヘルプの一覧表でb[+num]はs[+num]と同一だと明記されていて、sはstart、bはbeginの頭文字という違いしかありません。他人の設定や解説で両方の表記が出てきても、探し分ける必要はない2文字です。
行単位のオフセットで注意したいのは、着地が必ず1桁目になることです。[section]の次の行が半角4文字ぶんインデントされているバッファで/section/+1を打つと、2行1桁、つまりインデントの空白の上で止まりました。+のように最初の非空白へ寄る動きを期待していると、そこからiで入力を始めたときに位置がずれます。
数字は省略できて、/bravo/+は/bravo/+1と同じ意味になります。設定ファイルの見出しを探して次の行から書き始めたいときは、この2文字がいちばん短い書き方です。
function foo() {}
/foo/efunction foo() {}
行頭にあったカーソルが、マッチしたfooの末尾である最後のoまで一気に移動します。ここでaを押せば単語の直後から入力できます。オフセット無しの/fooだと着地は先頭のfなので、同じことをするにはiではなく末尾まで移動し直す手間が挟まります。
オペレータと組み合わせると範囲が変わる
検索オフセットは移動先の指定なので、オペレータと組み合わせるとその区間がまとめて操作対象になります。ここで効いてくるのが、eを付けたときだけ範囲の数え方が切り替わるという仕様です。
When including an offset with 'e', the search becomes inclusive (the character the cursor lands on is included in operations).
検索モーションは通常、一致の手前で止まる排他的な範囲です。eを付けると包括的に変わり、カーソルが乗った文字まで巻き込みます。1行目がalpha bravo charlieのバッファの1行1桁から、オペレータ付きで打った結果が次の表です。
| 打ったもの | 削除した結果 |
|---|---|
d/bravo | bravo charlie |
d/bravo/e | charlie(先頭は空白) |
d/bravo/e-1 | o charlie |
d/bravo/b+2 | avo charlie |
d/bravo/+1 | 1行目と2行目が行ごと消える |
d/bravoがalpha までしか消さないのに対して、d/bravo/eはalpha bravoまで消えています。単語ごと消したいのに末尾が残る取りこぼしは、eの1文字で無くなります。オペレータと検索モーションの組み合わせで手前までしか消えない動きに戸惑ったことがあるなら、足りなかったのはこの1文字です。
表の最後の行だけ結果の性質が違います。行オフセットを付けると範囲が行単位に変わり、カーソルのあった行と着地した行の全体が対象になります。文字単位で消すつもりだったのに行が丸ごと消えたときは、オフセットに数字だけを書いていないか確かめてください。
/foo<CR>
c//e<CR>bar<Esc>
最初にfooを検索し、c//eで直前の検索パターンをオフセットe付きで再利用してbarに書き換えます。//はパターンを省いた書き方で、直前の検索パターンがそのまま使われます。同じ単語を何箇所も書き換えるときに、毎回パターンを打ち直さずに済みます。
手元では/bravoのあとc//eで書き換え、//で次の一致へ進み、もう一度c//eという流れで2箇所とも置き換わりました。似た作業はcgnとドットコマンドの組み合わせでもできて、打鍵数はそちらのほうが少なくて済みます。差が出るのは置き換える文字列が箇所ごとに違うときで、.が使えないぶんこちらの形が残ります。
文字オフセットは行をまたいで数える
文字単位のオフセットに大きめの数字を書くと、行の終わりで止まらずに次の行へ続きます。e+nが「同じ行の中でn文字」ではなく「バッファをn文字ぶん進む」という決まりだからです。
1行目のalpha bravo charlieは19文字で、bravoの末尾は11桁目です。ここで/bravo/e+9と打つと2行1桁に着きました。11桁目から19桁目まで8文字進み、9文字目で行の境界を越えた形です。/bravo/e+20では2行12桁、2行目のfoxtrotのfの上でした。
戻る側も同じ数え方です。最終行から/delta/b-3を打つと、折り返して2行目のdeltaを見つけたあと3文字ぶん戻り、1行17桁に止まりました。1文字戻った時点で前の行の末尾である1行19桁へ移り、そこからさらに2文字ぶん左へ動いた位置になります。
行き先がバッファの外に出るときは、はみ出したぶんが切り捨てられます。最終行のend of fileを/end/e+50で探すと、50文字先はバッファのどこにもないので最終行の最後の文字である4行11桁で止まりました。エラーにはならないので、大きな数字を書いて狙った場所に来なかったときはこの打ち切りを疑ってください。
この数え方があるので、文字オフセットの数字は一致のすぐ近くを狙うときだけ使うのが現実的です。数行先へ飛ぶなら行オフセットのほうが素直で、途中の行の長さに結果が左右されません。
n と N はオフセットごと繰り返す
オフセット付きで検索したあとnを押すと、パターンだけでなくオフセットも一緒に繰り返されます。/bravo/eで1行11桁に飛んだあとnを押すと、3行目のbravoの末尾である3行9桁に着きました。Nも同じで、逆向きに動きながら一致の末尾に止まります。行オフセットの/bravo/+1からnを押した場合も、次の一致の1行下である4行1桁でした。
ヘルプには最後に使ったパターンとオフセットが記憶されると書かれています。ただし検索レジスタ@/を覗くと中身はbravoだけで、オフセットは入っていません。一方histget("search", -1)はbravo/eを返します。オフセットはパターンとは別に持たれていて、@/を読み書きするスクリプトやプラグインからは見えない値だということです。
履歴に丸ごと入っている性質は、コマンドラインウインドウをq/で開くと目で確かめられます。過去の検索がオフセットごと1行として並ぶので、そこで選んで実行すれば着地位置も当時のまま再現されます。オフセットだけを外したいときは、//と打って空のパターンで検索します。パターンは直前のものが使われ、オフセットは付きません。/bravo/eで1行11桁にいる状態から//を打つと、3行5桁、つまり次の一致の先頭へ移りました。
//<Enter> " 同じパターンをオフセット無しで検索
n で進めてから c//e を打つと巻き込む
ここまででnがオフセットを引き継ぐこと、そしてc//eが連続した書き換えに効くことを見ました。この2つを素直につなげると壊れます。手元で実際にバッファを潰した手順が次のものです。
/bravo/e<Enter> " 1行11桁(1つ目の末尾)へ
n " 3行9桁(2つ目の末尾)へ
c//e<Enter>BAR<Esc> " ここで巻き込む
3行9桁は2つ目のbravoの末尾で、一致の内側です。ここからc//eを打つと、前方検索はカーソルより先を探すので3行目の一致は候補から外れます。そしてwrapscanが効いてバッファの先頭へ折り返し、1行目のbravoを拾います。オペレータの範囲は打つ前のカーソルと着地点のあいだなので、1行11桁から3行9桁までが丸ごと変更対象になりました。4行あったバッファはalpha bravBAR twoとend of fileの2行に潰れています。
:set nowrapscanにしてから同じ手順を踏むと、折り返しが起きないので検索が失敗し、バッファは変わりませんでした。wrapscanを切って使っている場合、この事故は起きようがないことになります。切り方と副作用は文字列検索の基本で扱っています。
安全に次へ進むには、nではなく//を使います。//ならオフセットが外れて一致の先頭に止まるので、続けてc//eを打ったときの範囲がその一致の中だけに収まります。ヘルプに載っている連続書き換えの手順も、nではなく//で次へ進む形になっています。
この機能を知る前は、ただfoo-barのような名前だとハイフンで止まってしまい、手元で確かめたらeは3桁目までしか進みません。ここで毎回1回ぶん余計に考えていたのが、/foo-bar/eに切り替えてから消えました。行オフセットのほうは当初まったく使わないだろうと思っていましたが、数万行のログを開いて特定のキーワードの2行下だけ見たいときに/ERROR/+2と打つ形で定着しています。
まとめ
検索オフセットは、一致からどれだけずらすかを検索コマンドの中に書き込む仕組みです。移動だけが目的ならeを覚えれば足りますが、値が出るのはオペレータと組んだときで、d/foo/eのように範囲が包括的に変わる点だけは押さえておくと取りこぼしが減ります。連続して書き換えるときはnではなく//をはさんで進めます。