🕌

Vim で折り返し行を簡単に移動できるサブモード・テクニック

2022/05/05に公開

はじめに

先日 Twitter の vim-jp コミュニティで、Vim で折り返し行を移動する方法についての質問がありました。

Vim に詳しくない方の為に説明すると、Vim は行指向のテキストエディタです。j/k で移動するのは行単位の為、折り返されている行であっても次の行へと移動します。

視覚的に移動したい

しかし折り返されている行を視覚的な単位で移動したい事もあります。w80l の様に移動する方法もありますが、目的の場所に確実に移動できる訳ではありません。

その様な移動の方法として Vim には gjgk があります。

gj		or					*gj* *g<Down>*
g<Down>			[count] 表示行下に移動。|exclusive|。行が折り返されて
			いる場合とオペレータコマンドとともに使われた場合は 'j'
			と違う動作になります(行単位ではありませんから)。
gk		or					*gk* *g<Up>*
g<Up>			[count] 表示行上に移動。|exclusive|。行が折り返されて
			いる場合とオペレータコマンドとともに使われた場合は 'k'
			と違う動作になります(行単位ではありませんから)。

これで希望通りに視覚的な行で移動できる様になります。

毎回 g をタイプするのは面倒

これで上手く移動できるのですが、移動する度に g をタイプしなければならないのは面倒ですね。そこで一般的な回答として良く紹介されるのが以下のキーマッピングです。

nnoremap j gj
nnoremap k gk

この様に設定する事で、j/k の移動が常に視覚的な移動となるのです。

しかし問題がある

実はこの方法には問題があるのです。キーボードマクロに影響してしまいます。例えば1文字削除した後でカーソルを1行下(物理的に)に移動するキーボードマクロは以下の様になります。

qqxjq

これを上記のキーマッピング付きで登録し 3@q 等で再生してしまうと、折り返しのある行とない行で動作が変わってしまいます。

そこで便利なサブモード

まずは答えから

nmap gj gj<SID>g
nmap gk gk<SID>g
nnoremap <script> <SID>gj gj<SID>g
nnoremap <script> <SID>gk gk<SID>g
nmap <SID>g <Nop>

このキーマッピングは j/k はこれまで通りの動作ですが gj/gk の動作が変わります。これまでですと視覚的に3行下に移動する為に

gjgjgj

とタイプしていたはずですが、このキーマッピングを導入すると

gjjj

で済みます。また3行下に移動したけれど行き過ぎたので k で戻る場合にも

gjgjgjgk

ではなく

gjjjk

で済みます。

サブモードの解説

まずは最初のキーマッピングを見てみましょう。

パート1
nmap gj gj<SID>g
nmap gk gk<SID>g

このマッピングは gj がタイプされると、gj をそのまま再生し、その後に <SID>g を再生します。<SID> はこれを設定している Vim script 内で定義されたマッピングを指します。<SID>g は後で出てきます。

パート2
nnoremap <script> <SID>gj gj<SID>g
nnoremap <script> <SID>gk gk<SID>g

このマッピングはスクリプトマッピングと呼ばれます。

						*:map-<script>* *:map-script*
マップや短縮入力を定義するときに "<script>" 引数を指定すると、{rhs} の中の
"<SID>" で始まるスクリプトローカルなマップだけが再マップされます。別の場所で
マップが定義されていても (例えば mswin.vim で CTRL-V にマップが定義されていて
も)、その影響を避けることができます。その場合でも同じスクリプトで定義された
マップは使うことができます。
Note: ":map <script>" と ":noremap <script>" の動作は同じです。コマンド名より
"<script>" の効果が優先されます。再マップが制限されることが明確になるため
":noremap <script>" を使う方がいいでしょう。

難しい事が書いてありますが、要は <SID> を別の <SID> にマッピングする為に使います。

そして最後のマッピングを見てみましょう。

nmap <SID>g <Nop>

これは <SID>g がタイプされた場合、実際には何も再生しない事を意味します。

さて、ここでパート1のキーマッピングが <SID>g で終わっている事に注目して下さい。<SID>g で始まるキーマッピングは、このスクリプト内では <SID>gj<SID>gk だけになります。(他に無ければ)

つまりユーザが gj をタイプすると

  1. gj が再生される(視覚的に行移動)
  2. <SID>g が再生される(けれど何も再生されない)
  3. 続くマッピングは <SID>gj または <SID>gk だけなのでオペレータペンディングとなる

※ オペレータペンディングとは Vim が続くキーの入力を待っている状態の事

このまま続けて j または k を打てば、それは gj または gk に置き換えられるという事になります。パート3のキーマッピングはまた <SID>g で終わっていますから、続けて gjjjjjj という入力ができる事になります。

注意点

もちろんキーボードマクロで gjgk を入力する様なシーンがあれば、これもまた誤動作してしまいますが、これは別のキープレフィックスで置き換える事もできます。

nmap <leader>j gj<SID><leader>
nmap <leader>k gk<SID><leader>
nnoremap <script> <SID><leader>j gj<SID><leader>
nnoremap <script> <SID><leader>k gk<SID><leader>
nmap <SID><leader> <Nop>

用途に合わせ設定すると良いと思います。

おまけ

この応用としてウィンドウリサイズをサブモードにするという方法もあります。

nmap <C-w>+ <C-w>+<SID>ws
nmap <C-w>- <C-w>-<SID>ws
nmap <C-w>> <C-w>><SID>ws
nmap <C-w>< <C-w><<SID>ws
nnoremap <script> <SID>ws+ <C-w>+<SID>ws
nnoremap <script> <SID>ws- <C-w>-<SID>ws
nnoremap <script> <SID>ws> <C-w>><SID>ws
nnoremap <script> <SID>ws< <C-w><<SID>ws
nmap <SID>ws <Nop>

これを使うと今までウィンドウを下方向に広げる為に

<CTRL-w>+<CTRL-w>+<CTRL-w>+<CTRL-w>+

とタイプしていたのが

<CTRL-w>++++

だけで良くなります。

おわりに

Vim の視覚的な行移動 gj/gk を題材にサブモード・テクニックをご紹介しました。ぜひ皆さんも便利なサブモードを作ってみて下さい。

Discussion