🫵

俺的にはずせない【Vim】こだわりのmap(説明付き)

2024/11/25に公開

Vimはホントに素敵なエディターでして、mapをゴソゴソ設定するだけで自分好みに機能を拡張できます。ちょっとした機能なら、わざわざプラグインをインストールしなくてもmapだけで実現できます。今回は私的に外せない&ちょっとマニアックなmap設定を紹介したいと思います。Vim初心者の方もわかりやすいように説明も付けてますので、Vimの勉強にもなると思います。

1. カーソル下の単語をハイライトする

nnoremap <silent> <Space><Space> :let @/ = '\<' . expand('<cword>') . '\>'<CR>:set hlsearch<CR>

ノーマルモードでスペースを2回押すと、カーソル下の単語がハイライトされます。

カーソル下の単語をハイライトする

Eclipseのカーソル下の単語を光らせる機能が欲しくて作りました。すごく便利で気に入ってます。Vimデフォルトの*キーで近いことは実現できるのですが、カーソルがジャンプしてしまうのが嫌なので独自にmapしました。

ではmapについて説明していきます。mapコマンドの書式はmap [左辺] [右辺]となっています(参考:h map)。バラしてみると次のようになります。

nnoremap ・・・ ノーマルモードのmapを定義(再マップなし)
[左辺] <silent> <Space><Space>
[右辺] :let @/ = '\<' . expand('<cword>') . '\>'<CR>:set hlsearch<CR>

左辺ですが、<silent>は一旦 無視してください。<Space><Space>としているのは、スペースキーを2回連続で押した場合に[右辺]の内容が発動するようにmapを定義しているということになります。

右辺ですが、少しややこしく見えます。しかし落ち着いて読んでいくと、実は難しいことはありません。処理の塊にバラして見てみましょう。

1| :let @/ = '\<' . expand('<cword>') . '\>'<CR> ・・・ /レジスタにカーソル下の単語を代入して
2| :set hlsearch<CR> ・・・ hlsearch(ハイライト検索)ONだ

まず1行目ですが、これはいわゆるVimスクリプトをワンライナーで書いています。letはJavascriptのletと同様に、変数に値を代入するということです。ここでは@/に値を代入しているということになります。この先頭の@とはなんぞや?ということですが、こいつはレジスタを指し示す際の接頭辞という役割があります。つまりここでは/という名前のレジスタを指し示していることになります。なのでlet @/ = ...は「/レジスタに...を代入する」ということになります。なお、レジスタとは一時的な記憶領域で、Vimによってあらかじめ用意された変数という理解で問題ありません。(詳しくは :h registers

しかし、「/レジスタ」とは何でしょうか?実はこれは特殊なレジスタでして、最後に/コマンドで検索した際の検索文字列を格納する特殊レジスタなのです。つまり/レジスタに文字列を代入するということは、あたかも「ついさっき/コマンドで検索しましたよ〜ん」と嘘ぶくことなのです。(詳しくは`:h @/)

ちょっと試してみましょう。おもむろに適当なファイルを開いて、:let @/ = 'a'と入力します。続いて:set hlsearchと入力してみましょう。検索したこともないのにaの文字がハイライトされるはずです。

続いて見ていきましょう。/レジスタに代入している内容は'\<' . expand('<cword>') . '\>'です。まずexpand('<cword>')ですが、こいつがズバリ「カーソル下の単語」になります。詳しくは:h expandでヘルプを引いてもらうとして、これに関してはそういうもんだと覚えてしまってください。

では次にexpandの左右を見ましょう。まず.ですが、Vimスクリプトでは文字列の連結に.を使用します。なのでここでの連結は、expandの前後にそれぞれ\<\>をくっつけているというわけです。じゃあこの記号の意味は何なのかというと、これは「単語の境界」を意味しています(詳しくは :h \< :h \>参照)。正規表現の\bみたいなもんです。つまり連結した表現は「カーソル下の単語で単語一致で検索する」という意味になるわけですね。

こちらも試しておきましょう。例えば次のようなファイルを編集していて、

1| A barking dog seldom bites.
2| Every dog has his day.
3| Let sleeping dogs lie.

:let @/ = '\<dog\>'と入力してnを押して検索すると、1行目と2行目のdogにはヒットしますが、3行目のdogsの部分にはヒットしません。一方、:let @/ = 'dog'としてからnで検索すると、3行目のdogsのdogにもヒットします。

最期の<CR>はエンターキーと等価です。要はコロン:を押して、let @/ = '\<' . expand('<cword>') . '\>'と入力してからエンターキーを押しているのと同じことです。
これで1行目が完成しました。

2行目:set hlsearch<CR>は検索結果ハイライトをONにするコマンドです。1行目であたかも/コマンドで検索したかのように/レジスタに値を格納し、2行目でその結果をハイライト表示する、という流れになっています。

2. カーソル下の単語をハイライトしてから置換する

nmap # <Space><Space>:%s/<C-r>///g<Left><Left>

#キーを押すと、カーソル下の単語をハイライトしてから置換後文字列を入力する状態にします。

カーソル下の単語をハイライトしてから置換する

ステータスバーを見てください。ポイントはカーソルの位置が置換後文字列を入力する場所に来てるところ。井之頭五郎さん風に言うと「こういうの嬉しい」ってやつです。<Space><Space>は先程のハイライトmapを利用しています。

では定義を見ていきましょう。今回もバラして見ていきます。

nmap ・・・ ノーマルモードmap(再マップあり)
左辺: #
右辺:
  <Space><Space> ・・・ 前述のハイライトmap発動
  :%s/<C-r>///g<Left><Left> ・・・ 置換

最初にnmapについてですが、こいつはnnoremapと違い、右辺の再マップを行います。つまり右辺最初の<Space><Space>によって1.のハイライトmapを発動させるということです。通常mapはnoreを付けて再マップ無しでmapすることが一般的ですが、きちんと理解した上で再マップを利用するのは有効的です。

次に:%s/<C-r>///gの部分ですが、基本的には:%sの置換コマンドを呼び出しているだけです。置換対象文字列の箇所が<C-r>/となっていますが、これは/レジスタの内容をペーストするという意味になります。:h <C-r>でヘルプを読めばわかると思います。<Space><Space>でハイライトする際に/レジスタにカーソル下の単語を放り込んでいるので、そいつを利用するというわけです。

最後に<Left><Left>でカーソルを置換後文字列を入力する箇所に移動させています。すぐに置換後の文字列の入力ができるようにしています。地味に便利です。

3. ビジュアルモードでもハイライト・置換

しつこくハイライト・置換ネタですが、1. 2. でやったことをビジュアルモードでもできるように設定します。

xnoremap <silent> <Space> mz:call <SID>set_vsearch()<CR>:set hlsearch<CR>`z
xnoremap * :<C-u>call <SID>set_vsearch()<CR>/<C-r>/<CR>
xmap # <Space>:%s/<C-r>///g<Left><Left>

function! s:set_vsearch()
  silent normal gv"zy
  let @/ = '\V' . substitute(escape(@z, '/\'), '\n', '\\n', 'g')
endfunction

ビジュアルモードで選択中に<Space>を押すと、選択範囲の文字列をハイライトします。*を押すと後方検索、#を押すと2.と同様にハイライトしてから置換入力状態にします。

まず一つ目のmapから見ていきます。

xnoremap ・・・ ビジュアルモードでのマップ(再マップ無し)
左辺: <silent> <Space>
右辺:
1| mz ・・・ マークをzに付ける
2| :call <SID>set_vsearch()<CR> ・・・ スクリプトローカル関数set_vsearchを呼び出す
3| :set hlsearch<CR> ・・・ ハイライト実行
4| `z ・・・ マークzに戻る

えらく物々しくなってしまいましたが、ここでやっていることは大したことはありません。mzで現在のカーソル位置にマークを付けてマーカーzに格納します。その後後述するset_vsearch関数を呼び出してからハイライトさせ、最後に現在のカーソル位置まで戻しますので、そのためのマーキングになります。

肝になっている関数を見てみます。

function! s:set_vsearch()
  silent normal gv"zy
  let @/ = '\V' . substitute(escape(@z, '/\'), '\n', '\\n', 'g')
endfunction

vimスクリプトの世界です。まずfunction!ですが、関数を定義しますよーって意味です。!が付いているのは、もし定義されてても再定義しろッ!という意味です。.vimrcってリロードされることがよくあるので、基本的には付けておくべきでしょう。

続いて関数名に付いているs:という接頭子ですが、これが付いている関数のスコープはスクリプトローカルで定義されます。要するに(基本的には)このファイル内からしか呼び出せない関数を定義するということです。(こういうのも:h s:でヘルプが読めるのがVimのいいところ。ぜひヘルプを読んでみてください)

関数内に移ります。最初のsilent normal gv"zyの行から見ていきましょう。silentはそれに続くコマンドを静かに実行するコマンドです。normalはノーマルコマンドを実行します。ノーマルコマンドっていうのは、ノーマルモードで普段実行してるyyとかddとかのアレです。gvは最後にビジュアルモードで選択した範囲をもいちど選択するコマンド。そしておなじみの"zyでzレジスタにヤンク。要するに「最後の選択範囲を静かにzレジスタにヤンク」しているわけです。

続いて次の行let @/ = '\V' . substitute(escape(@z, '/\'), '\n', '\\n', 'g')ですが、先ほどヤンクしたzレジスタの内容をゴソゴソしてから/レジスタに格納しています。何をゴソゴソしているかというと、検索文字列のエスケープ処理を施しています。例えば/が検索文字列に含まれていた場合、そいつをエスケープして\/にしてあげてたりします。escapesubstituteもvimの標準関数ですので、詳しくはヘルプを見てみてください。

先頭にくっつけている\V"very nomagic"といって「very magicモードを使わない」という意味になります。こちらも:h \Vでヘルプを読めば分かりますが、very nomagicでは正規表現で使用する特殊文字が単なる文字として扱われるようになります。ビジュアルモードで選択した文字列で検索する場合に、正規表現を使うことなんてありえないので\Vを付けてます。

これで一つ目のmapが理解できるかと思います。続く2つのmap

xnoremap * :<C-u>call <SID>set_vsearch()<CR>/<C-r>/<CR>
xmap # <Space>:%s/<C-r>///g<Left><Left>

もこれまでの知識で理解できると思います。

4. 上下に空行を挿入する

Shift + Enterで下に、Shift + Ctrl + Enterで上に空行を挿入します。

imap <S-CR> <End><CR>
imap <C-S-CR> <Home><CR><Up>
nnoremap <S-CR> mzo<ESC>`z
nnoremap <C-S-CR> mzO<ESC>`z

blan_line.gif

Eclipseにもあるような空行を上下に挿入するやつです。挿入モード用とノーマルモード用に分かれています。imap使っているのは<CR>を再マップさせたいからです。<CR>ってneocompleteとか他のプラグインでmapしていることがあり、そちらのmapを活かすためです。

なお<C-o>oでも同じことできるじゃんと思うかもしれませんが、このmapの方が優秀です。ひとつは入力しやすいということ。さらにもうひとつ重要なのは、実際に<End>で行の末尾に移動し<CR>を入力するので、他のプラグインなどの自動挿入が働くということです。(例えばifに対するendの自動挿入とか)

ノーマルモード用のmapでmz -> `zしてるのは、元いた場所にカーソルを戻すためです。使い勝手を良くするために、こういうのすごく大事だと思います。

💡 CUI版のVimで実行するには

上記map<S-CR>,<C-S-CR>はMacVimのようなGUI版のVimでしか動作しません。なぜかというと、ターミナルから起動したCUI版のVimでShift+Enterを押しても、Vimに送られるコードはEnterのみだからです。

以下の手順でこの問題を回避できます。(Macでしか検証してません)

  • iTermなどのターミナルアプリの設定で、Shift+Enter/Ctrl+Shift+Enterが押されたら、ある特定の文字を出力するように設定する
  • その文字に対するmap設定を.vimrcに施す

私は次のようにmap設定してます。

if !has('gui_running')
  " CUIで入力された<S-CR>,<C-S-CR>が拾えないので
  " iTerm2のキー設定を利用して特定の文字入力をmapする
  map ✠ <S-CR>
  imap ✠ <S-CR>
  map ✢ <C-S-CR>
  imap ✢ <C-S-CR>
endif

iTerm2の設定画面

iTerm2の設定

↓これ↓を参考にしました。
http://stackoverflow.com/questions/5388562/cant-map-s-cr-in-vim

5. コマンドラインはemacsバインディングで

vimのコマンドラインのカーソル移動にemacsキーバインドを使用するようにします。

cnoremap <C-p> <Up>
cnoremap <C-n> <Down>
cnoremap <C-b> <Left>
cnoremap <C-f> <Right>
cnoremap <C-a> <Home>
cnoremap <C-e> <End>
cnoremap <C-d> <Del>

標準のバインディングだとカーソルキー使わなきゃなんないのが嫌なのでこれを設定してます。ちなみにMacのテキスト入力時なんかもemacsバインディングが使用できるので、Vimmerであってもemacsの基本的なカーソル移動はできたほうが良いと思いますよ。

6. 行ごと移動する

行ごと移動します。

" 行を移動
nnoremap <C-Up> "zdd<Up>"zP
nnoremap <C-Down> "zdd"zp
" 複数行を移動
vnoremap <C-Up> "zx<Up>"zP`[V`]
vnoremap <C-Down> "zx"zp`[V`]

行ごと移動

グリグリ行を移動させられるので、メソッド丸ごと上の方に持って行きたいなあ、ってな場合に便利です。Vimcastにあった例を少しいじっています。

" 行を移動のmapの方は特筆すべきことは無いです。俺的お約束のzレジスタを使って行を削除・ペーストしているだけです。

" 複数行を移動の方ですが、目新しいのは`[`]ぐらいです。これはそれぞれ「直前にヤンクした開始場所に移動」「直前にヤンクした終了場所に移動」という意味になります(これも:h `[でヘルプが読めます)。つまり、ビジュアルモードで選択した範囲をxでzレジスタにカットして、それをp,Pでペースト、その後さっきヤンクした開始位置にジャンプしてVで行選択モードにして、さっきヤンクした終了位置にジャンプして終わる・・という流れになります。マクロを組んでるみたいで楽しくありませんか?

7. Ctrl+tでタイポ修正

(マイナーかもしれませんが)Macに備わってるアレです。Ctrl+tでtehtheに直したりできます。

inoremap <C-t> <Esc><Left>"zx"zpa

英語を入力しているとtheと入力するつもりがtehと打ってしまう場合とかよくると思います。そういった入力順間違いを入れ替えます。(|がカーソルだとして)teh|の状態で<C-t>を入力するとthe|という状態になります。

8. ハイライトを消去する

ハイライトを消去しつつ画面も再描画します。

nnoremap <silent> <C-l> :<C-u>nohlsearch<CR><C-l>

もともとは

nnoremap <silent> <Esc><Esc><Esc> :<C-u>nohlsearch<CR>

ってやってたんですが、もともと用意されている再描画キー<C-l>を使用する例を見かけて、こっちのほうがスジがいいなあと思い、こっちにしました。

9. Delete, Backspace

挿入モードでのDeleteとBackspaceです。

inoremap <C-d> <Del>
imap <C-h> <BS>

入力中って文字DeleteしたりBackspaceすること多いじゃないですか。標準の<C-d>のマッピングは「インデントを減らす」ですが、個人的にはあまり使用しないので<Del>に当ててます。標準の<C-h>は「カーソルを行頭へ」(?)なのですが、これも<BS>の方が有用なので変えました。

わざわざimapにしているのは、neocomplete等のオートコンプリート系プラグインが<BS>をmapしている場合があるので、そちらの設定を無効化しないようにしています。

neocomplete用に<BS>を使用している例
inoremap <expr><BS> neocomplete#smart_close_popup().\<BS>

というわけで、<C-h><BS>を完全に等価にしたかったのであえてimapにしています。

なお、挿入モード中にインデントを正したい場合は<C-o>==押せばいいですし、行頭へ移動したい場合はちょっと面倒だけど<C-o>^もしくはちょい邪道でCMD + ←しちゃってます。

10. xsではヤンクしない

好みによりますが、xsで削除した内容でレジスタを汚したくないので、これらは闇に葬ってます。

nnoremap x "_x
nnoremap s "_s

"_は「_(アンダースコア)レジスタを使用する」という意味ですが、_レジスタは消去用レジスタといいブラックホールのように吸い込んだものを闇に葬り去ります。

なおcもヤンク不要だと言う方は同様に定義しておくことができます。

nnoremap c "_c

11. ビジュアルモードで連続ペーストできるように

あらかじめ文字列をヤンクしておき、ビジュアルモードで置換対象を選択してからペーストすると、置換対象がヤンクされてしまうため連続してペーストしていくことができません。というわけで、連続でペーストできるようビジュアルモードでpにマップを定義します。

xnoremap p "_d"0P

ペーストする前に選択範囲を消去用レジスタ(ブラックホールレジスタ)で消してから、ヤンク用レジスタ0からペーストするようにしています。

12. CTRL + ]で右にエスケープする

小技ですが。挿入モードからノーマルモードに戻る際にESCの代わりに<CTRL> + [で戻るかと思いますが、しばしばノーマルモードに抜けてからひとつカーソルを右に動かしたくなることがありませんか? そんなときに便利なマップです。

inoremap <C-]> <Esc><Right>

地味ですが、慣れると結構便利だったりします。

以上
俺的こだわりmapでした。お気に召すのがあればぜひ設定してみてください。
もっといい方法などあればコメントいただけると幸いです🙇

Discussion