Keybinding, leader key và workflow làm việc hiệu quả
Bạn đã có init.lua gọn gàng và một spec lazy.nvim với vài plugin đầu tiên. Bài trước dừng ở chỗ "cài được plugin", nhưng cài xong mà vẫn gõ :Telescope find_files bằng tay thì chưa phải workflow. Bài này biến phần config đó thành phản xạ của ngón tay: chọn leader key một lần, chia phím theo workflow, và viết keymap bằng Lua có mô tả để ba tháng sau bạn vẫn đọc được.
Sau bài này bạn sẽ:
- Đặt
mapleaderđúng chỗ tronginit.lua, trước khi plugin load. - Chia namespace phím theo hành động trong workflow, không theo từng plugin.
- Viết keymap bằng
vim.keymap.setvớidesc,<cmd>và map buffer-local. - Tự kiểm tra phím đã map bằng
:map,:verbosevà which-key.
Leader key: chọn một lần và đặt ở đầu file
-- init.lua, đặt ngay trên phần require("lazy").setup
vim.g.mapleader = " "
vim.g.maplocalleader = " "
Vì sao Space: đây là phím lớn nhất bàn phím, cả hai ngón cái đều với tới được mà không rời home row. Leader mặc định là \ nằm tít góc phải, mỗi lần dùng phải liếc mắt xuống. Space ở normal mode chỉ là motion sang phải, gần như không mất gì khi lấy nó làm prefix.
Điểm quan trọng hơn cả việc chọn phím nào: vị trí đặt. vim.g.mapleader phải được gán trước require("lazy").setup(...) và trước bất kỳ require nào của plugin, vì nhiều plugin đọc leader ngay lúc setup để bind phím của chúng. Nếu bạn set sau, keymap của plugin sẽ bind vào \ và bạn sẽ ngồi đoán xem config sai ở đâu.
Warning: Triệu chứng kinh điển của leader đặt sai thời điểm: bấm
<leader>ffkhông có gì xảy ra, nhưng bấm\fflại chạy. Đừng sửa keymap, hãy kiểm tra thứ tự dòng tronginit.luatrước.
Bài tập nhanh 1: Xác nhận leader hiện tại và xem nó đã resolve thành phím nào.
Gợi ý
Dùng :lua =vim.g.mapleader để in giá trị, rồi :nmap <leader> để xem Neovim đã expand <leader> thành gì. Nếu :nmap <leader> liệt kê các phím bắt đầu bằng \, leader của bạn đang là mặc định.
Chia namespace theo workflow, không theo plugin
Đây là phần quyết định hệ phím của bạn sống được bao lâu. Nếu map theo plugin, bạn phải nhớ "phím này của plugin nào", và mỗi lần thay plugin bạn phải học lại phím. Nếu map theo hành động, bạn thay plugin thoải mái mà tay vẫn chạy đúng.
| Nhóm workflow | Prefix | Ví dụ |
|---|---|---|
| File | <leader>f |
<leader>ff mở file, <leader>fr file gần đây |
| Search | <leader>s |
<leader>sg grep trong dự án |
| Buffer | <leader>b |
<leader>bd đóng buffer hiện tại |
| Code / LSP | <leader>c |
<leader>cr rename, <leader>ca code action |
| Git | <leader>g |
<leader>gs status, <leader>gd diff |
| Terminal / toggle | <leader>t |
<leader>tt mở terminal nổi |
Quy tắc đặt tên: ký tự đầu là phạm vi, ký tự sau là động từ. <leader>s là "tìm cái gì đó", g là grep. Giữ 5 đến 7 nhóm là đủ; nhiều hơn thì bạn phải nhớ hai tầng, mất lợi ích.
mindmap
root((Space))
f["📁 File"]
s["🔍 Search"]
b["🧱 Buffer"]
c["🛠️ Code"]
g["🌿 Git"]
t["💻 Terminal"]
Bài tập nhanh 2: Chọn 3 thao tác bạn làm nhiều nhất trong một ngày làm việc và gán phím cho chúng, biết rằng :w đã đủ nhanh nên bạn không bắt buộc phải map save.
Gợi ý
Ví dụ hợp lý: <leader>sg cho grep, <leader>bd cho đóng buffer, <leader>tt cho terminal. Lưu ý <leader>w thường được dành cho nhóm window; nếu bạn muốn giữ chỗ đó thì cứ để save là :w. Nguyên tắc: đừng chiếm một prefix có ý nghĩa khác chỉ vì hôm nay bạn thấy tiện.
Viết keymap bằng Lua: desc là bắt buộc, <cmd> là mặc định tốt
-- lua/config/keymaps.lua
local map = vim.keymap.set
local function nmap(lhs, rhs, desc)
map("n", lhs, rhs, { silent = true, desc = desc })
end
nmap("<leader>w", "<cmd>write<cr>", "Ghi file hiện tại")
nmap("<leader>bd", "<cmd>bdelete<cr>", "Đóng buffer hiện tại")
nmap("<leader>sg", "<cmd>Telescope live_grep<cr>", "Grep trong dự án")
Các option đáng nhớ:
| Option | Dùng khi | Lý do |
|---|---|---|
desc |
luôn luôn | nguồn dữ liệu cho :map, which-key, Telescope keymaps |
remap = true |
thật sự muốn map chồng map | vim.keymap.set đã mặc định noremap |
silent = true |
vế phải là command | không đẩy thông báo lên cmdline |
expr = true |
vế phải là hàm trả về chuỗi phím | dùng cho toggle |
Hai chi tiết nhỏ nhưng khác biệt lớn. Thứ nhất, <cmd>write<cr> không đi qua cmdline nên không đổi mode, không bị ảnh hưởng bởi mapping của :, và không nháy màn hình. Thứ hai, khi vế phải là một Lua function (như vim.lsp.buf.rename), bạn truyền thẳng function, không cần bọc <cmd>.
Map theo buffer thì để trong autocmd, vì phím LSP chỉ nên sống trong buffer có LSP:
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local buf = ev.buf
map("n", "gd", vim.lsp.buf.definition, { buffer = buf, desc = "Nhảy tới định nghĩa" })
map("n", "<leader>cr", vim.lsp.buf.rename, { buffer = buf, desc = "Rename symbol" })
end,
})
Trên Windows 11, hãy tránh lấy <C-s>, <C-q> hay <C-v> làm phím sống còn: một số terminal giữ lại các tổ hợp này trước khi Neovim kịp nhận. Phần xử lý triệt để nằm ở Module 3, còn ở đây chỉ cần biết để không thiết kế phím dựa vào chúng.
Bài tập nhanh 3: Viết map <leader>tt mở terminal nổi, có desc, và giải thích trong một câu vì sao bạn dùng <cmd> thay :.
Gợi ý
nmap("<leader>tt", "<cmd>split | terminal<cr>", "Mở terminal trong split")
<cmd> chạy command trực tiếp, không qua cmdline, nên không bị ảnh hưởng bởi các mapping của : và không đổi mode khi bạn đang ở normal mode.
Để config tự dạy bạn phím
Một hệ phím tốt phải tự giải thích được. :map <leader> liệt kê mọi phím bắt đầu bằng leader, :verbose nmap <leader>ff cho biết file nào đã định nghĩa phím đó. Nếu spec lazy.nvim của bạn đã có which-key, bấm leader rồi dừng lại sẽ thấy popup các nhóm phím, và nó đọc chính các desc bạn viết. Đó là lý do desc không phải trang trí.
Bài tập nhanh 4: Một phím bấm không có phản ứng gì. Nêu ba bước kiểm tra theo thứ tự.
Gợi ý
:verbose nmap <phím>xem phím có được map không và map ở file nào (nếu không có kết quả, phím chưa được map).:lua =vim.g.mapleaderxem leader có đúng không, và kiểm tra nó được set trướclazy.setupchưa.- Kiểm tra xung đột prefix: một map ngắn hơn có thể nuốt phím của bạn, hoặc plugin khác map đè sau.
Những quy tắc giữ cho keybinding không phản bội bạn
- Chỉ map thao tác bạn đã làm thật ít nhất vài lần trong tuần. Map trước khi dùng là cách nhanh nhất để có một bộ phím bạn không nhớ.
- Không map hai tổ hợp cho cùng một việc, trừ khi đang chuyển tiếp có chủ đích.
- Giữ
keymaps.luaphẳng cho phím global. Phím phụ thuộc filetype hoặc LSP thì để trong autocmd vớibuffer. - Viết
descngay lúc viết map. Ba tháng sau bạn sẽ không nhớ<leader>cxlà gì, nhưng:mapthì nhớ. - Đừng map lại
hjkl,x,p,u,.hay các motion bạn đã nhuyễn. Bạn đến đây để giữ tốc độ, không phải để học lại Vim. - Khi thêm plugin mới, nhét phím của nó vào nhóm có sẵn. Chỉ tạo nhóm mới khi workflow thật sự mới.
Chốt lại và bước kế tiếp
Bốn điều cần mang theo: leader được set trước lazy.setup, namespace chia theo workflow chứ không theo plugin, mọi keymap đều có desc, và <cmd> là vế phải mặc định cho command. Đây là bộ phím phản ánh công việc của bạn, không phải bản sao config của người khác.
Bài tiếp theo là checkpoint của Module 1: bạn sẽ tự dựng lại bộ khung init.lua, cài plugin qua lazy.nvim và chứng minh hệ phím của mình chạy đúng trước khi bước sang LSP, Telescope và Treesitter.
Front: vim.g.mapleader phải được đặt ở đâu trong init.lua? Back: Trước require("lazy").setup(...) và trước khi bất kỳ plugin nào được load.
Front: Vì sao Space là leader key được khuyên dùng? Back: Phím lớn nhất, hai ngón cái với tới mà không rời home row, và gần như không mất chức năng gốc.
Front: Triệu chứng của việc set leader quá muộn là gì?
Back:
Front: Quy tắc đặt tên một nhóm phím theo workflow?
Back: Ký tự đầu là phạm vi hành động, ký tự sau là động từ, ví dụ
Front: Vì sao chia phím theo workflow thay vì theo plugin? Back: Plugin có thể bị thay hoặc tháo, còn phím gắn với hành động thì tay vẫn chạy đúng.
Front: vim.keymap.set mặc định có bật noremap không? Back: Có, mặc định là noremap; chỉ đặt remap = true khi thật sự muốn map chồng map.
Front: Option desc trong keymap dùng để làm gì? Back: Mô tả phím, hiển thị trong :map, which-key và Telescope keymaps.
Front:
Front: Muốn một keymap chỉ có hiệu lực trong buffer hiện tại thì dùng option nào? Back: buffer = bufnr, thường đặt trong autocmd LspAttach với ev.buf.
Front: Lệnh nào cho biết keymap được định nghĩa ở file nào?
Back: :verbose nmap
Front: silent = true trong keymap có tác dụng gì? Back: Không hiển thị command hoặc thông báo trên cmdline khi phím được bấm, tránh cmdline nhấp nháy và giữ màn hình sạch.
Front: Vì sao nên map
Front: Điều gì xảy ra nếu hai keymap trùng
Chốt lại và bước tiếp theo
Ba ý cần mang theo vào buổi làm việc thật:
- Leader key phải được set trước khi
require("lazy").setup(), nếu không plugin sẽ bind phím với leader mặc định là\. - Mỗi keymap đi qua
vim.keymap.setvớidesc, và<cmd>...<cr>là dạng rhs mặc định cho lệnh Ex. - Phím được tổ chức theo workflow (nhóm hành động), không theo tên plugin, để sau này thay Telescope bằng thứ khác thì tay vẫn chạy đúng.
Cấu hình lua/config/keymaps.lua bạn vừa viết chính là thứ sẽ được kiểm tra ở bài checkpoint tiếp theo, nơi bạn ráp toàn bộ init.lua, spec lazy.nvim và keymaps thành một bộ khung chạy được, kèm vài lỗi cố ý để bạn tự sửa.