Vim でも VSCode のように Alt + 矢印 (j/k) で行を移動させる(解説付き)
はじめに
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
は、直前の選択範囲を再び選択します。これにより連続での移動が可能になります。
Discussion