Vimの検索コマンドラインで単語を補完する(Ctrl-r Ctrl-w)
長い変数名を検索したいのに、綴りを間違えないよう慎重に入力していました。カーソル下の単語をそのまま検索したいだけなら*で十分ですが、検索キーワードの一部だけ自分で入力し、残りを補完したい場面もあります。
目次(9項目)
| キー | これだけ覚える |
|---|---|
| Ctrl-r Ctrl-w | 検索/置換コマンドラインにカーソル下の単語を挿入 |
| * / # | カーソル下の単語をそのまま完全一致検索 |
この記事に書いた挙動は、macOS 26.4.1 上の Vim 9.1(Appleが同梱している/usr/bin/vim)と Neovim 0.12.4 で、一時ディレクトリに置いたサンプルファイルを開いて1つずつ打って確かめました。同じキーでもincsearchの設定で結果が変わるところがあるので、手元で試すときは:set incsearch?の値を先に見ておくと混乱しません。
基本の補完
/{検索キーワードの一部}Ctrl-r Ctrl-w
検索モードで/を押したあと、キーワードを途中まで入力してからCtrl-r Ctrl-wを押すと、カーソル位置にある単語がコマンドラインに入ります。完全一致で飛ぶ*と違い、入った文字列をその場で編集してから検索を実行できるのが利点です。ただし、入る文字列が「単語まるごと」とは限りません。ここが最初につまずくところでした。
同じ/seaまで入力してからCtrl-r Ctrl-wを押しても、incsearchがオフのときは/seasearchComplete、オンのときは/searchCompleteになります。Vim 9.1 を-u NONEで起動し、searchCompleteという語の上にカーソルを置いて打ち比べた結果です。オフのときは単語がそのまま後ろに足され、オンのときは入力済みのseaが二重にならないよう差し引かれます。
| 設定 | /seaのあとにCtrl-r Ctrl-w |
|---|---|
set noincsearch | /seasearchComplete |
set incsearch | /searchComplete |
この差は:help c_CTRL-R_CTRL-Wに書かれているとおりの動きです。incsearchがオンのときは「いま画面に出ている一致の末尾」が基準になり、すでに打った部分は挿入されません。基準がカーソルではなく一致の位置に変わるので、取り込まれる単語そのものも入れ替わります。1行目のletにカーソルを置いたまま/maiと打った場合、オンでは次の行のmain.jsへ一致が移っているので/main、オフではカーソル下のletが足されて/mailetになりました。
下のBEFORE/AFTERはincsearchがオンのときの見え方です。
/sea
/searchComplete
やっかいなのは、そのincsearchの既定値が環境によって違うことです。Neovim 0.12.4 は-u NONEで起動してもオンでしたが、macOS標準の Vim 9.1 はオフのままでした。-u NONEでオフなのは Vim の組み込みの既定がオフだからですが、オプションを何も付けずに素のvimで起動してもオフのままです。こちらの理由は/usr/share/vim/vimrcにlet skip_defaults_vim=1が書かれていることで、この1行があるとdefaults.vimが冒頭でfinishし、そこにあるset incsearchまで処理が届きません。vim -u DEFAULTSで起動するとincsearchはオンになります。手元の結果がこの記事と食い違うと感じたら、:echo &incsearchで値を見てからset incsearchを自分の設定ファイルに足すのが早道です。
単語の切れ目を決めているのはiskeyword
「カーソル下の単語」の範囲はiskeywordが決めています。VimもNeovimも既定値は@,48-57,_,192-255で、英字と数字とアンダースコアだけが単語の一部として扱われます。foo-bar.bazの先頭にカーソルを置いてCtrl-r Ctrl-wを押すと入るのはfooまでで、set iskeyword+=-してから同じ操作をするとfoo-barまで伸びました。CSSのクラス名やLispの識別子のようにハイフン入りの名前を日常的に触るなら、その言語のファイルタイプにだけiskeywordを足しておくと手数が減ります。
記号ごと持ってきたいときはCtrl-r Ctrl-aで、こちらは空白で区切られたWORDが対象なのでfoo-bar.bazが丸ごと入ります。カーソルが単語の途中にあっても入るのは単語全体で、searchCompleteの5文字目にカーソルを置いて押しても、まるごとsearchCompleteが入りました。逆にカーソルが空白の上にあると、その行で右にある最初の単語まで探しに行きます。foo-bar.baz ./src/main.jsの空白の上で押したときはCtrl-r Ctrl-wがsrc、Ctrl-r Ctrl-aが./src/main.jsを返しました。
記号を取り込むときは、それが検索パターンとして解釈される点を忘れないでください。foo-bar.bazをそのまま検索するとドットが任意の1文字と解釈されるため、foo-barXbazという行にも当たります。手元の5行のファイルで一致する行を数えたところ、foo-bar.bazでは2行、foo-bar\.bazでは1行でした。エスケープまで済ませて取り込みたいときは、expressionレジスタを使ってCtrl-r =のあとにescape(expand('<cWORD>'), '/\.*$^~[')と打つと、foo-bar\.bazの形でコマンドラインに入ります。
関連する補完キー
| キー | 挿入される内容 |
|---|---|
| Ctrl-r Ctrl-w | カーソル下の単語(iskeywordで切る) |
| Ctrl-r Ctrl-a | カーソル下のWORD(空白で切る。記号込み) |
| Ctrl-r Ctrl-f | カーソル下のファイル名をそのまま |
| Ctrl-r Ctrl-p | カーソル下のファイル名をpathで展開したもの |
| Ctrl-r Ctrl-l | カーソル行を丸ごと |
| Ctrl-r " | 無名レジスタの内容 |
Ctrl-r Ctrl-fとCtrl-r Ctrl-pの違いは、pathをたどって実在するファイルを探しに行くかどうかです。require main.js hereという行のmain.jsの上で試すと、前者はmain.jsをそのまま返し、後者はset path+=./srcを入れた状態でsrc/main.jsを返しました。探し方はカーソル下のパスを開くgfと同じなので、gfで開けるファイル名ならCtrl-r Ctrl-pでも展開されると考えて構いません。
紛らわしいのですが、ここで押しているCtrl-wやCtrl-fはレジスタ名ではありません。:let @w = 'REGISTER-W-CONTENT'と入れてからCtrl-r Ctrl-wを押しても、入ったのはsearchCompleteのままで、@wの中身は完全に無視されました。Ctrl-rのあとに"や0のような本物のレジスタ名を打ったときだけ、レジスタの中身が入ります。直前にヤンクした文字列を検索したいならCtrl-r "です。
そのレジスタ挿入には落とし穴が1つあります。yyのような行単位のヤンクをしたあと/Ctrl-r "とすると、Vim 9.1では末尾の改行が^Mとして付いてきます。histget('/', -1)で検索履歴を取り出して長さを数えると、画面に見える22文字に対して23と出ました。この^Mもパターンの一部なので、そのまま実行してもどこにも一致しません。Neovim 0.12.4 で同じ操作をすると長さは22で、改行は落ちていました。Vimの側ではBackspaceで1文字消すか、そもそもyiwやy$のように行単位にしないヤンクで取るのが確実です。
1文字ずつ伸ばすCtrl-lとの併用
Ctrl-r Ctrl-wが単語を丸ごと持ってくるのに対して、incsearchがオンのときはCtrl-lで一致の続きを1文字ずつ足せます。searchCompleteを対象に/seaから押していくと、sear、searc、search、searchC、searchCoと1文字ずつ伸びました。単語の途中で止めたいとき、たとえばsearchまでを共通の接頭辞として拾いたいようなときに向いています。
Ctrl-lは単語の境界で止まりません。14回押し続けたときはsearchComplete()まで伸び、括弧まで取り込みました。名前の似ているCtrl-r Ctrl-lのほうはカーソル行を丸ごと挿入する別物で、試すとlet searchComplete = 1が1行そのまま入ります。incsearchがオフのときのCtrl-lには注意が要ります。何も起きないのではなく、/sea^Lのように制御文字がそのままパターンへ入り、押した回数だけ増えていきました。
置換コマンドでも同じように使える
この補完は検索コマンドラインに限らず、:%s/のような置換コマンドを打っている最中にも同じキーで使えます。searchCompleteの上にカーソルを置いて:%s/seaまで打ってからCtrl-r Ctrl-wを押すと、incsearchがオンなら:%s/searchComplete、オフなら:%s/seasearchCompleteになりました。incsearchのハイライトは:substituteのパターン部分にも効くので、検索コマンドラインとまったく同じ理屈が働きます。
変数名の一括置換はこの組み合わせがいちばん速いです。名前の上にカーソルを置いて:%s/と打ち、Ctrl-r Ctrl-wで綴りを取り込み、区切りの/のあとに新しい名前とgcを付ければ、長い名前を一度も手で打たずに確認付きの置換まで進みます。範囲やフラグの指定は:substituteコマンドの活用集にまとめてあります。
単語検索(*/#)との使い分け
カーソル下の単語をそのまま完全一致で検索したいだけなら*/#のほうが手数が少なくて済みます。一方、単語の前後に何か文字を足したい、複数の単語を組み合わせて検索パターンを作りたい、といった加工が必要な場合はこちらの補完のほうが柔軟に対応できます。
もう1つの違いが単語境界です。*を押したあとに検索履歴を見ると、入っているのは\<searchComplete\>で、前後に境界の指定が自動で付いています。Ctrl-r Ctrl-wで取り込んだ場合はsearchCompleteだけなので、searchCompleteAllのような部分一致にも当たります。境界が要るなら自分で\<と\>を書き足します。境界を付けないg*の結果がCtrl-r Ctrl-wと同じsearchCompleteだったので、この2つを同じ仲間として覚えると整理しやすいです。
長い関数名を前方一致で検索したくて、*では完全一致になってしまうのでCtrl-r Ctrl-wを覚えました。ところがmacOS標準のVimで途中まで打ってから押すとseasearchCompleteのような二重の文字列になり、しばらくキーを覚え違えているのだと思っていました。原因がincsearchだと分かったのは、同じ操作をNeovimでやったときにすんなり通ったからです。設定を1行足してからは、名前を取り込んで末尾だけ削り、前方一致のパターンに仕立てる使い方が定着しました。
コマンド履歴の補完と組み合わせる
一度実行した検索はコマンドライン履歴に残るので、次からは/のあとに↑で呼び戻せます。数文字だけ打ってから押すと、その文字で始まる履歴だけをたどれます。alpha、searchComplete、betaの順に検索してから/seaまで打って比べたところ、↑は/searchCompleteを呼び戻し、Ctrl-pは打った文字を無視して最新の/betaを出しました。Ctrl-r Ctrl-wで組み立てた長いパターンを作り直さずに済むのは↑のほうで、取り込みで作るのは最初の1回だけになります。
履歴をまとめて見比べたいときはコマンドラインウインドウを開きますが、ここではCtrl-r Ctrl-wが効きません。q/で開くのはただのバッファなので、その中のCtrl-rはインサートモードのレジスタ挿入として解釈されます。実際に押すとレジスタ名の入力待ちになり、続けてCtrl-wを打っても何も入りませんでした。取り込みたい単語があるなら、元のバッファでyiwしておいてからCtrl-r 0で貼るのが確実で、この手順なら/^searchCompleteのような行をその場で作れます。
まとめ
どこまで取り込みたいかでキーを選ぶのが、いちばん迷わない覚え方です。単語ならCtrl-r Ctrl-w、記号込みならCtrl-r Ctrl-a、ファイル名ならCtrl-r Ctrl-f、pathで探させるならCtrl-r Ctrl-p、行ならCtrl-r Ctrl-l、1文字ずつならCtrl-lという並びになります。
そのうえで、incsearchの確認だけは省かないでください。この設定ひとつで、同じキーが「すでに打った部分を差し引く」動きにも「単語をそのまま後ろに足す」動きにも変わり、しかも既定値がVimとNeovimで揃っていません。思ったとおりに入らないときは、キーの覚え違いよりこちらを疑うほうが当たります。