Edited at

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

More than 1 year has passed since last update.

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


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

nnoremap <silent> <Space><Space> "zyiw:let @/ = '\<' . @z . '\>'<CR>:set hlsearch<CR>

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

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

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

nnoremap ・・・ ノーマルモードのmapを定義(再マップなし)

[左辺] <silent> <Space><Space>
[右辺] "zyiw:let @/ = '\<' . @z . '\>'<CR>:set hlsearch<CR>

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

右辺ですが、とてもややこしく見えます。しかし先頭からじっくり読んでいくと、実はたいして難しいことはありません。処理の塊にバラして見てみます。

1| "z ・・・ 今からzレジスタ使いますよ

2| y ・・・ ヤンクするのは、、、
3| iw ・・・ カーソル下の単語(inner word)です
4| :let @/ = '\<' . @z . '\>'<CR> ・・・ /レジスタに代入して
5| :set hlsearch<CR> ・・・ hlsearch(ハイライト検索)ONだ

まず1行目の"コマンドは使用するレジスタを指定するコマンドです。(参考:h "

レジスタとはヤンクした内容などを保存しておく記憶領域のことで、普段みんなよく使っているyyなんかの内容もこのレジスタに保存されています。レジスタは色んな種類があるので、詳しくは この記事 とか読んでみてください。

このケースでは"のあとにzが続いているので、これからzレジスタを使用するぞってことです。つまり、次に行うヤンクの結果をzレジスタに放り込むことになります。

2行目のyでヤンク、3行目がパラメータiwなので、カーソル下の単語をヤンクすることになります。ここまで「カーソル下の単語をzレジスタにヤンク」することができました。

ちょっと試してみたいなって人は、適当な単語の上にカーソルを乗せて、"zyiwと入力してから、:regを実行してみましょう。レジスタの一覧が表示されて、zレジスタにカーソル下の単語がヤンクされているはずです。(実際にすぐ試せるのがVimのいいところ)

続いて4行目ですが、ややプログラムチックです。(まあプログラムなんですが)

4| :let @/ = '\<' . @z . '\>'<CR>

先に@zについてですが、こいつは「zレジスタの内容」という意味です。Vimスクリプト内では@の後ろにレジスタ名を付けると、そのレジスタを参照することができます。

次にletですが、これは変数に値を代入する際に必要なキーワードです。Javascriptのvarに近いのかな。let @/ = ...ということは、「/レジスタに...を代入する」ということになりますね。ちなみに<CR>はエンターキーの入力と等価です。

じゃあその「/レジスタ」とはなんぞや?ということになりますが、こいつは最後に/コマンドで検索した際の検索文字列を格納する特殊レジスタなのです。つまり/レジスタに文字列を代入するということは、あたかも「ついさっき/コマンドで検索しましたよ〜ん」と嘘ぶくことなのです。

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

最後に'\<', '\>'という奇妙な文字列の説明ですが、これらは単語の境界を表す特殊文字列になります。つまり、これらを連結しておくと単語一致モードでハイライトさせることができます。例えば次のようなファイルを編集していて、

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にもヒットします。

以上で "zyiw:let @/ = '\<' . @z . '\>'<CR>:set hlsearch<CR>の説明は終わりです。前から順番に見ていくと、意外と簡単に理解できると思います。


<silent>について

<silent>は左辺のオプションです。これは処理結果をステータスラインに表示させないためのオプションです。試しに<silent>を取り除いてmapを定義し、単語の上で<Space>を2回叩いてみましょう。ハイライト結果は変わりませんが、ステータスラインに:set hlsearchという表示が残ったままになっていると思います。<silent>を付ければこれを抑制することができます。


なぜzレジスタを使うの?

別にzレジスタじゃなくても問題ありません。ただ何かしらレジスタを指定しておかないと、無名レジスタ"(普段のydで使用されるレジスタ)が汚れることになるので、私はこういった場合はzを使用するように習慣づけています。


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

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

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

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

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

nmap ・・・ ノーマルモードmap(再マップあり)

左辺: #
右辺:
<Space><Space> ・・・ 1.のハイライト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>でカーソルを置換後文字列を入力する箇所に移動させています。もう一度言いますが、こういうの嬉しい。

ちなみに置換後文字列を入力する際、置換対象文字列をペーストしてからちょこっと修正して置換したい場合が結構あります。そういう場合は、<C-r>zを入力すると置換対象文字列がピタッとペーストできますよ。(1.でハイライトするときにzレジスタに格納しているので)


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に戻る

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

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

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> <Up><End><CR>

nnoremap <S-CR> mzo<ESC>`z

nnoremap <C-S-CR> mzO<ESC>`z

Eclipseにもあるような空行を上下に挿入するやつです。挿入モード用とノーマルモード用に分かれています。imap使っているのは<CR>を再マップさせたいからです。<CR>ってneocompleteとかでよく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の設定画面

↓これ↓を参考にしました。

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>

こだわりのmap・・・って感じじゃないですが笑。もともとは

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している定義があるからです。

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

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

ここから2017.07.21追記


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

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

xnoremap <expr> p 'pgv"'.v:register.'y`>'

連続ペースを実現するために、ペースト後に元々ヤンクしていた文字列を再度ヤンクするようにしています。初めて出てくる記述もあるので、例のごとく左辺・右辺にバラして説明します。

xnoremap ・・・ ビジュアルモードのmapを定義(再マップなし)

[左辺] <expr> p
[右辺] 'pgv"'.v:register.'y`>'

まず<expr>ですが、これは右辺を式として評価するという意味です。つまり右辺には、通常のキー入力を記述するのではなくて、Vim式を記述するということです。右辺をさらにバラして見てみます。なお、Vim式では文字列を'で囲み、文字列連結は.で行います。ちょっと読みやすく書くと、

①p + ②gv + ③"[v:registerの結果]y + ④`>

というように、4つのパートに分けることができます。要するにv:registerが返す結果を利用したいので<expr>しているんですね。

最初から見ていきましょう。①のpですが、これはいつも通りのペーストコマンドです。つまりこの時点で選択中の文字列が元々ヤンクされていた内容に置き換わります。

次に②のgvですが、これはさっき選択していた範囲を再度選択するコマンドです。試しにビジュアルモードで文字列を選択し、解除してカーソルを違う場所に持っていってからgvを押してみれば、よく挙動が理解できると思います。このケースではgvすると、置換後の文字列が選択されていることになります。

③のv:registerですが、ノーマルコマンドで使用するレジスタの名前が格納されているVim変数です。要は、なんにも考えずにヤンクしたりペーストしたりする場合に使用されるレジスタの名前で、通常は"とか*だと思います。例えばこのv:register*だったとすると③のコマンドは"*yになるわけで、これは噛み砕くと、

"* ... *レジスタを使用して

y ... ヤンクや!

ということです。これでさっきペーストした文字列を再度ヤンクすることができました。

最後の④ですが、これはオマケみたいなものなのですが、`で後に続くマーク位置にジャンプします。マーク位置は>ですが、これは「最後に選択した範囲の終端」です。③のヤンクで移動してしまったカーソル位置を通常のペースト後のカーソル位置まで戻してあげるというわけです。

一見ややこしそうに見えますが、噛み砕くと大したことないのがVimマップです笑


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

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

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

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


以上

俺的こだわりmapでした。お気に召すのがあればぜひ設定してみてください。