↕️

Vim でも VSCode のように Alt + 矢印 (j/k) で行を移動させる(解説付き)

2023/04/11に公開

はじめに

teratail でこんな質問に答えたのですが、興が乗って結構細かく説明を書いたので記事にしておきます。

結論

  • Vim script
nnoremap <M-j> <Cmd>move .+1<CR>==
xnoremap <M-j> :move '>+1<CR>gv=gv
nnoremap <M-k> <Cmd>move .-2<CR>==
xnoremap <M-k> :move '<-2<CR>gv=gv
  • Lua (Neovim)
vim.keymap.set("n", "<M-j>", "<Cmd>move .+1<CR>==")
vim.keymap.set("x", "<M-j>", ":move '>+1<CR>gv=gv")
vim.keymap.set("n", "<M-k>", "<Cmd>move .-2<CR>==")
vim.keymap.set("x", "<M-k>", ":move '<-2<CR>gv=gv")

解説

まず、現在 Lua からキーマップを定義するなら vim.keymap.set() を使った方が良いです。
wrap されているので nvim_set_keymap() を直接触るより嵌ることは少ないでしょう。

次に :move コマンドの help はこちら。

:[range]m[ove] {address}			*:m* *:mo* *:move* *E134*
			Move the lines given by [range] to below the line
			given by {address}.

{address} として指定した行の下に、[range] で選択した行を移動させます。[range] に何も指定しなければカーソル行1行を選択しているのと同じ動作になります。
{address} の説明はこのようにあります。

Line numbers may be specified with:		*:range* *{address}*
	{number}	an absolute line number  *E1247*
	.		the current line			  *:.*
	$		the last line in the file		  *:$*
	%		equal to 1,$ (the entire file)		  *:%*
	't		position of mark t (lowercase)		  *:'*
	'T		position of mark T (uppercase); when the mark is in
			another file it cannot be used in a range
	/{pattern}[/]	the next line where {pattern} matches	  *:/*
				also see |:range-pattern| below
	?{pattern}[?]	the previous line where {pattern} matches *:?*
				also see |:range-pattern| below
	\/		the next line where the previously used search
			pattern matches
	\?		the previous line where the previously used search
			pattern matches
	\&		the next line where the previously used substitute
			pattern matches

つまり、:move . は、「現在カーソルのある行を、現在の行の下に移動させる」という意味の Ex コマンドになり、何も起きません。
そして {adress}.$ のような特殊な文字を絶対行に変換したのち、式として評価されます。
ですので、:move .+1 は、「現在カーソルのある行を、{現在の行+1}の下に移動させる」ことになります。
実際に試してみるといいでしょう。

少し分かりにくいかもしれないので、具体例を出しておきます。
現在4行目にカーソルがあるとします。
このとき、:move .+1:move 5 と等価です。
これの意味は、「現在の4行目を、現在の5行目の下に動かす」です。
なので、結果として現在カーソルのある行が一つ下に動いたように見えます。
上に動かす場合は -2 になることに気を付けて下さい(2行目の下に動かさないといけない)。

つまり、ノーマルモードでのマッピングは以下のようになります。

nnoremap <M-j> <Cmd>move .+1<CR>==
nnoremap <M-k> <Cmd>move .-2<CR>==

ノーマルモードやインサートモードから Ex コマンドを呼ぶ場合は、基本的に <Cmd> を使うべきです。これについても語るとさらに長くなるので省略します。

また = オペレータを使い、移動後にインデントを揃えるようにしています。
これは VSCode の動作に合わせるためですが、個人的には無くてもいいかなと思います。
どうせ移動中は文法的に不正な、崩れた状態になるでしょうし、適宜 prettier などのフォーマッタをかければいいでしょう。

次にビジュアルモードに移ります。
基本的には変わりませんが、range'<, '> について少し触れておきます。
ビジュアルモードで : を打ちコマンドモードに入ると、勝手に :'<,'> と入力されますよね。
これは謎の呪文ではなく、意味があります。
先程の :move コマンドの help をもう一度引用しましょう。

:[range]m[ove] {address}			*:m* *:mo* *:move* *E134*
			Move the lines given by [range] to below the line
			given by {address}.

さて。適当なファイルを開き :3,5move 6 というコマンドを実行してみてください。
どうなるでしょうか。
3-5行目が6行目の下に移動しましたね?
rangeで複数行を指定するときは、:{start},{final} という記法を使います。
つまり :'<,'> は、'< から '> までという意味になるわけです。
'<, '> はそれぞれ、直前にビジュアルモードで選択していた範囲の先頭と最後を意味します。
ですから :'<,'> は選択範囲全域を表します。

というわけで、ビジュアルモードでのマッピングは以下のようになります。

xnoremap <M-j> :move '>+1<CR>gv=gv
xnoremap <M-k> :move '<-2<CR>gv=gv

モード指定には v ではなく x を使いましょう。歴史的経緯により v はビジュアルモード+セレクトモードを意味します。
<Cmd> ではなく : なのは '<,'> を機能させるためです。<Cmd> はモードの変更を引き起こさないため、直前の選択範囲が更新されません。
gv は、直前の選択範囲を再び選択します。これにより連続での移動が可能になります。

GitHubで編集を提案

Discussion