前言

Vim motion 的核心之一就在於直觀且可高度客製化的快捷鍵設定,要想如臂使指般地使用 Neovim,一套精心調教過的快捷鍵絕對是不可或缺的,尤其無論是在 Vim 還是 Neovim 中,都遺留了一些老舊且稍微有些反人類的操作設定,以及缺少了現代人習以為常的方便功能。

所幸, Neovim 的快捷鍵設定相當直觀、便利且強大,你可以透過它來簡化一些繁瑣的操作、使用額外外掛的功能、你甚至可以直接實現一些組合功能,而不需要另外下載功能外掛,我將會在後續的介紹中進行示範。

在本篇裡,我將介紹如何設定 Neovim 的快捷鍵,並分享我的設定。如果想直接複製我的設定文件,可以到這裡(我的 github)來複製完整版的內容。

另外切記,在 Neovim 中,千萬不要去強記任何的快捷鍵,而是將你的習慣告訴 Neovim,讓它成為你的形狀。


建立設定文件與引用

還記得我們在上一篇: Neovim 設定文件結構與基礎功能設定 中提過的文件結構嗎,首先,到 imp/ 中建立這次要使用的設定文件 keymaps.lua

接著,編輯 init.lua,加入以下的程式碼來引用它:

require("imp/keymaps")

Leader Key

在 Neovim 中,Leader key是一個通用的快捷鍵前綴,用來做為大部分快捷鍵的起頭,預設為 \ ,有些人喜歡使用 ;, ,而我則比較喜歡使用空白鍵。

編輯 keymaps.lua,加入以下內容:

vim.g.mapleader = " "

這樣,我們就設定完成了。


如何設定快捷鍵?

在 Neovim 中,我們主要依靠以下兩個 api 來設定快捷鍵:

  • vim.keymap.del():刪除快捷鍵。
  • vim.keymap.set():設定快捷鍵。

不過,有鑑於 vim.keymap.set 本身就會覆寫原有的快捷鍵,所以其實我沒怎麽用過 vim.keymap.del,以下將著重說明 vim.keymap.set 的使用,如果想看更詳細的說明,也可以使用指令 :h vim.keymap 來查看官方的說明文件。

vim.keymap.set

vim.keymap.set 包含了四個參數:

  1. {mode}:快捷鍵作用的模式,包含 n、i、v、t、c 五個基本模式,可以傳入單個字元如 “n",或一個 list 如 {"n", "i"} 來讓快捷鍵作用在複數模式中。
  2. {lhs}:你想設定的快捷鍵。
  3. {rhs}:快捷鍵觸發的操作,可以是一系列的按鍵組合、一條指令或是一個 expression。
  4. {opts}:快捷鍵的選項表,主要包含:
    • noremap:設定為 true 時,表示這個快捷鍵的映射是非遞迴的。例如,當你同時有快捷鍵 A->BB->C 時,如果你將這項設為 false ,則 A 就會被映射為 C,反之則為 B
    • silent:設定為 true 時,執行快捷鍵時不會在命令行中顯示訊息。
    • expr:設定為 true 時,表示 {rhs} 的內容將做為 expression 求值。
    • nowait:設定為 true 時,表示立刻執行映射,不等待更多输入。
    • desc:為快捷鍵添加描述,用於讓額外外掛(如 which-key)取用來顯示提示訊息。
    • buffer:設定為 true 時,表示這個快捷鍵只會對當前 buffer 作用。

以上是我有使用過的一些 {opts},其他更多內容可以參考 :h maparg

來舉個例子吧:

1
2
3
4
5
-- Add empty line without get into Insert mode
vim.keymap.set("n", "<CR>", "o<Esc>k", { noremap = true, silent = true, desc = "Add empty line" })

-- Press jj to exit insert mode
vim.keymap.set("i", "jj", "<Esc>", { noremap = true, silent = true, desc = "Exit insert mode" })

在第一個例子中,我將 Normal mode 中的 <CR> 鍵映射成了一段連續的操作 o<Esc>k,如果你很熟悉 Vim 指令的話,應該馬上就能反應過來它的意思:o 新增一行、<Esc> 返回 Normal mode、然後 k 將遊標移回原本的所在行。

也就是說,它會維持遊標的位置,並在下方插入一個空白行。

而在第二個例子中,我將 Insert mode 中的 jj 輸入映射成 <Esc> 鍵,也就是只要我在 Insert mode 時輸入 jj ,就可以退出 Insert mode。另外僅供參考,也有些人會將其設為 jk

另外,你可能會注意到,<CR> 鍵是什麼?以及,如果我想加入 Ctrl 或 Shift 鍵時,我應該怎麽表示?放心,我也很常忘記,所以在這裡我列出了一些常見的功能鍵的表示方法,以供查詢:

符號 說明
Leader <leader> 用於部分快捷鍵的前綴,預設是 \,可自行更改。
Enter <CR> Enter 鍵。
Control <C- Ctrl鍵,例如 <C-n> 表示 Ctrl + n。
Shift <S- Shift 鍵,例如 <S-h> 表示 Shift + h。
Alt <A- Alt 鍵,例如 <A-x> 表示 Alt + x。
Meta <M- Meta 鍵,一般就是指 Alt 鍵。
Escape <Esc> Esc 鍵。
Backspace <BS> 退格鍵。
Space <Space> 空白鍵。
Tab <Tab> Tab 鍵。
Arrow Up <Up> 向上箭頭,可以用類似 <C-UP> 的方式與功能鍵組合。
Arrow Down <Down> 向下箭頭。
Arrow Left <Left> 向左箭頭。
Arrow Right <Right> 向右箭頭。
Delete <Del> Delete 鍵。
Function Key <F1> to <F12> 功能鍵 F1 到 F12。

keymaps.lua 完整說明

至此,想必你已經可以開始調整屬於你的快捷鍵了,而接下來我將對我的設定內容進行分類介紹,同樣地,完整的設定檔可以到這裡(我的 github)查看,你可以直接複製再根據需求進行調整,但就像我說的,這是我習慣的快捷鍵組合,它不一定適合你,不要去強記任何人的快捷鍵,而是要讓 Neovim 來配合你的習慣。

最好的驗證方法就是,當你設定完快捷鍵之後,你是不是能馬上想起大部分的內容,而不需要一再重新去查看。我個人的習慣是,盡量使用與動作單字相關的字母,例如 Delete 就用 d,Replace 就用 r 之類的。

preliminary setup

首先是一些基本設定:

1
2
3
4
5
6
-- Set Leader key to Space --
vim.g.mapleader = " "

-- Shorten function name --
local keymap = vim.keymap.set
local opts = { noremap = true, silent = true }

這裡我首先將 Leader key 設為了空白鍵;接著,因為接下來我們會設定很多的快捷鍵,為了讓文件簡潔一些,我將一些會重複使用的內容保存為本地變數,包含將 vim.keymap.set 存為 keymap,以及將我常用的 {opts} 選項保存為 opts

這樣,我們就可以透過 keymap("n", "快捷鍵", "映射", opts) 的方式來設定大多數的快捷鍵了。

cursor jumping

在 Neovim 的預設中,<C-u><C-d> 分別會將遊標向上或下移動半頁,再將遊標當前行捲到頁面頂部(會保留 scrolloff 的行數)。而這個「半頁」的行數是根據你當前視窗的可視行數來動態決定的,例如當你的視窗可以顯示 50 行,那半頁就是 25 行。

像這樣不固定的行數對我來說很煩,所以我決定將它固定在 15 行。以及,我喜歡在每次遊標進行例如翻頁、或搜尋關鍵字等大幅跳行的行為時,將遊標固定在特定位置,以避免找不到遊標在哪。

1
2
3
4
5
6
7
8
9
10
-- Normal --
-- Page up and down
keymap("n", "<C-u>", "15kztzv", opts)
keymap("n", "<C-d>", "15jztzv", opts)
keymap("n", "G", "Gzzzv", opts)
keymap("n", "gg", "ggzzzv", opts)

-- Keep cursor at the middle after search jumping
keymap("n", "n", "nztzv", opts)
keymap("n", "N", "Nztzv", opts)

說明:

  • 固定翻頁行數:設定 <C-u><C-d> 的移動行數為 15 行。
  • 移動後的遊標位置:
    • zt:將遊標所在行捲到頂部。
    • zz:將遊標所在行捲到中間。
    • zb:將遊標所在行捲到底部。
  • zv:展開遊標所在行的程式碼摺疊。

move in warpline

在 Neovim 中,當一行文字過長而被換行顯示時,在使用普通模式下的 jk 鍵移動時,會跳到實際的下一行或上一行,而不是視覺上的換行。要在視覺行內移動,則必須使用 gjgk 來移動。為了方便,我決定把它們的功能互換。

1
2
3
4
5
-- Move in warpline
keymap({ "n", "v" }, "j", "gj", opts)
keymap({ "n", "v" }, "k", "gk", opts)
keymap({ "n", "v" }, "gj", "j", opts)
keymap({ "n", "v" }, "gk", "k", opts)

normal trick

以下是一些我在 Normal mode 中的自定功能:

1
2
3
4
5
6
7
8
-- Add empty line without get into Insert mode
keymap("n", "<CR>", "o<Esc>k", opts)

-- Delete into void
keymap({"n", "v"}, "<leader>d", '"_d', opts)

-- Replace all
keymap("n", "<leader>r", [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/gI<Left><Left><Left>]], opts)

說明:

  • <CR> 會在下方新增空行且不進入插入模式。
  • <leader>d:Neovim 中使用 d 來刪除文字時會同時將刪除的內容放入剪貼簿中,這有時很煩,所以我多設定了一個快捷鍵,來將不要的內容刪除且不放入剪貼簿中。
  • <leader>r:選中遊標當前所在的單字,並對文件中所有相同的單字進行替換操作。

window navigation

以下是一些自定的視窗管理快捷鍵,包含切換視窗,以及調整視窗大小:

1
2
3
4
5
6
7
8
9
10
11
-- Window navigation
keymap("n", "<C-h>", "<C-w>h", opts)
keymap("n", "<C-j>", "<C-w>j", opts)
keymap("n", "<C-k>", "<C-w>k", opts)
keymap("n", "<C-l>", "<C-w>l", opts)

-- Resize with arrows
keymap("n", "<C-Up>", ":resize +2<CR>", opts)
keymap("n", "<C-Down>", ":resize -2<CR>", opts)
keymap("n", "<C-Left>", ":vertical resize -2<CR>", opts)
keymap("n", "<C-Right>", ":vertical resize +2<CR>", opts)

說明:

  • 視窗導航:
    • <C-h>:移動到左側視窗。
    • <C-j>:移動到下方視窗。
    • <C-k>:移動到上方視窗。
    • <C-l>:移動到右側視窗。
  • 調整視窗大小:
    • <C-Up>:增加視窗高度。
    • <C-Down>:減少視窗高度。
    • <C-Left>:減少視窗寬度。
    • <C-Right>:增加視窗寬度。

insert and visual trick

以下是一些我在 Insert 和 Visual mode 中的自定快捷鍵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- Insert --
-- Press jj to exit insert mode
keymap("i", "jj", "<Esc>", opts)

-- Visual --
-- Stay in indent mode
keymap("v", "<", "<gv", opts)
keymap("v", ">", ">gv", opts)

-- Keep paste buffer after paste
keymap("v", "p", '"_dP', opts)
keymap("v", "<leader>p", "p", opts)

-- Move text up and down
keymap("v", "J", ":m '>+1<CR>gv=gv", opts)
keymap("v", "K", ":m '<-2<CR>gv=gv", opts)

說明:

  • 插入模式:使用 jj 來退出插入模式。
  • 縮排改進:在 Visual mode 時,輸入 >< 會增加或減少遊標所在行的縮排,並回到 Normal mode。這不太直覺,所以我將其改為縮排後仍保持原先的選取狀態,這樣就能進行連續縮排了。
  • 貼上改進:在 Visual mode 時,輸入 p 會將選取的內容與剪貼簿中的內容交換,這太反人類了,所以我將它改成在貼上後仍保持剪貼簿的內容不變,而如果今天我就是要交換內容,則改使用 <leader>p
  • 行移動:當在 Visual mode 時輸入 JK,可以將選取的行下移或上移,這超好用,個人強推。

後記

至此,就是本章的所有內容了,相信你也見識到了 keymaps 的便捷與強大,只要你想,你甚至可以將 h/j/k/l 的移動方式改成 w/a/s/d ,雖然我不建議這麼做就是了。

這份 keymaps.lua 可能會是你將來在使用 Neovim 時最常更動的設定文件,無論是不斷的調教,還是在安裝了眾多的額外外掛後,替外掛的功能設定新的快捷鍵,都可以透過編輯這個檔案,來將各式各樣的功能快捷統一整理在一個文檔當中,相當方便。

而在下一章裡,我們將開始進入到 Neovim 最深遠的巨坑:外掛的安裝與管理。

上一篇: Neovim 設定文件結構與基礎功能設定
下一篇: Neovim 外掛管理工具 —— lazy.nvim

參考資料