-
Notifications
You must be signed in to change notification settings - Fork 0
nvim guide keymaps
vim.keymap.set({mode}, {lhs}, {rhs}, {opts})
- mode:在哪些模式下生效,例如
"n"(normal)、{ "n", "v" }等- lhs:按键本体(left-hand side),如
<leader>ff- rhs:执行的动作(right-hand side),可为字符串或 Lua 函数
- opts:可选参数表,控制描述、是否静默、是否可递归等
| 取值 | 模式 | 说明 |
|---|---|---|
"n" |
Normal | 正常模式 |
"i" |
Insert | 插入模式 |
"v" |
Visual + Select | Visual 与 Select 模式 |
"x" |
Visual | 仅 Visual 模式(字符、行或块选择) |
"s" |
Select | 仅 Select 模式 |
"o" |
Operator-pending | 操作等待模式,例如 d、y 之后 |
"t" |
Terminal | 内置终端 buffer |
"c" |
Command-line | 命令行模式 |
{ "n", "v" } 等 |
多模式表 | 数组形式可一次绑定多个模式 |
"" |
Normal、Visual、Select、Operator-pending | 对应 :map 的多模式范围 |
- 直接填写字符:
"gg"、"jk"等 - 特殊键使用尖括号:
"<leader>ff"、"<C-s>"、"<A-j>"、"<Space>"、"<Tab>"…… - 支持
<localleader>(Neovim 默认\,可在options.lua中用vim.g.maplocalleader自定义) - 可组合:
"<leader><tab>]"、"<C-w><C-q>"等
若需要
<Esc>作为触发,请写"<Esc>"或"<esc>"
- 直接传入可执行的 Vim 命令串,如:
"<cmd>w<cr>"、":echo 'hi'<CR>"、"<C-w>h" - 结尾务必加
<CR>以执行命令 - 也可以写纯按键序列,例如
"(用于复制)、"o<esc>"(在下方插入新行并退出)
在 Neovim 中,执行命令有两种写法,它们有重要区别:
<cmd>xxx<cr> - Neovim 推荐方式
vim.keymap.set("n", "<C-s>", "<cmd>w<cr>", { desc = "保存" }):xxx<CR> - 传统方式
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "保存" })对比表格:
| 特性 | <cmd>xxx<cr> |
:xxx<CR> |
|---|---|---|
| 命令历史 | ❌ 不显示 | ✅ 显示 |
| 命令行事件 | ❌ 不触发 | ✅ 触发 |
| 执行速度 | ✅ 更快 | |
| 插件拦截 | ✅ 不易被拦截 | |
| 推荐度 | ✅ 推荐 |
推荐做法:
- 优先使用
<cmd>xxx<cr>方式 - 只有在需要命令历史或触发事件时才使用
:xxx<CR>
- 传入 Lua 函数,便于编写复杂逻辑或调用插件 API
- 函数内可调用
vim.cmd(),vim.notify()等
vim.keymap.set("n", "<leader>bd", function()
require('vv-utils.bufdelete').smart()
end, { desc = "Delete Buffer" })函数模式下默认
rhs不是表达式;若需返回字符串让 Neovim继续处理,需要expr = true,见下文
| 选项 | 类型 | 默认值 | 作用 & 常见用法 |
|---|---|---|---|
desc |
string | nil | which-key/: 显示的描述,强烈建议填写(支持中文) |
silent |
boolean | false | 是否隐藏命令回显;常用映射可显式设为 true
|
noremap |
boolean | true | 是否禁止递归映射。vim.keymap.set 默认 true
|
remap |
boolean | false | 与 noremap=false 相同,只是更直观 |
expr |
boolean | false |
rhs 返回一个字符串作为按键继续执行,常见于动态选择 gj/j
|
buffer |
number? | nil | 设为 buffer id 或 0,则仅对当前 buffer 生效 |
nowait |
boolean | false | 避免键位等待其它潜在按键(与 timeoutlen 相关) |
replace_keycodes |
boolean | true | 当 expr=true 时自动解析 <CR> 等特殊符号 |
| 其它 | any | - | 传给 vim.keymap.set 的 opts 表会原样存放,可自行扩展 |
这是 Neovim 中最容易混淆的概念之一。让我们用具体例子来理解:
核心概念:
-
noremap = true(默认):非递归映射 -rhs中的按键不会再次触发映射 -
remap = true(或noremap = false):递归映射 -rhs中的按键会再次触发映射
例子 1:理解递归映射的问题
假设你有以下配置:
-- 配置 1:将 h 映射为左移(这很危险!)
vim.keymap.set("n", "h", "l", { noremap = false }) -- 递归映射
-- 配置 2:将 <leader>h 映射为 h
vim.keymap.set("n", "<leader>h", "h", { noremap = true }) -- 非递归映射问题:
- 当你按
<leader>h时,它执行h - 但
h已经被映射为l(左移变成了右移!) - 所以
<leader>h实际上会执行l(右移),而不是原来的h(左移)
解决方案:使用 noremap = true
-- 正确做法:使用非递归映射
vim.keymap.set("n", "<leader>h", "h", { noremap = true }) -- 直接执行 h,不再触发映射你的配置文件中的实际例子
-- 你的配置:Alt + Left/Right 跳转历史
map("n", "<A-Left>", "<C-o>", { desc = "Previous jump", remap = true })为什么这里用 remap = true?
-
<C-o>是 Neovim 的原生按键(跳转到上一个位置) - 使用
remap = true意味着:如果<C-o>被其他插件或配置映射了,会触发那个映射 - 使用
noremap = true意味着:直接执行原始的<C-o>,忽略所有映射
推荐做法:
- 如果
<C-o>是原生功能,通常用noremap = true(更安全) - 如果希望
<C-o>能触发其他映射,用remap = true
本配置统一使用 vim.keymap.set(见 lua/config/keymaps/),确保所有映射都能被 :verbose nmap <lhs> 追溯
vim.keymap.set({ "i", "n", "v" }, "<C-s>", "<cmd>w<cr>", {
desc = "Save File / 保存当前文件",
silent = true,
})- 模式:插入、普通、可视模式都能用
- 行为:执行
:w保存 -
desc会在 which-key 或通知中显示
vim.keymap.set({ "n", "x" }, "j", "v:count == 0 ? 'gj' : 'j'", {
expr = true,
silent = true,
desc = "Down / 向下移动(兼容软换行)",
})-
expr = true表示rhs结果将作为新的按键执行 -
v:count读取用户输入的重复次数(例如5j) - 实现逻辑:没有输入数字时,用
gj;否则保持普通j
vim.api.nvim_create_autocmd("FileType", {
pattern = "lua",
callback = function(event)
vim.keymap.set({ "n", "x" }, "<localleader>r", '<cmd>source %<cr>', {
buffer = event.buf,
desc = "Source current Lua file",
})
end,
})- 通过
FileTypeautocmd +buffer = event.buf实现 buffer-local 的 filetype 绑定
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function(event)
vim.keymap.set("n", "<leader>mp", ":MarkdownPreview<CR>", {
buffer = event.buf,
desc = "Preview Markdown",
})
end,
})- 通过
buffer = event.buf让快捷键只在该 buffer 生效 - 适用于插件窗口或临时 buffer
-
看“当前输入的按键序列”:
:set showcmd -
查某个按键最终执行什么、由谁定义:
:verbose nmap <lhs>(按需替换nmap/imap/vmap/tmap) -
查某个前缀下有哪些映射:
:nmap <leader>或:nmap <leader>f -
确认终端实际发来的键码:
:echo getcharstr()(执行后按一次要排查的键) -
查看终端原始转义序列:在 插入模式 下按
Ctrl+V再按目标键,会原样插入终端发送的转义序列。例如:-
Ctrl+L插入^L(ASCII 控制字符 12) - 若终端支持 Kitty keyboard protocol,
Ctrl+Shift+L会插入类似^[[108;6u的 CSI 序列 - 若两者输出相同,说明终端没有区分这两个组合键
-
-
需要时看最近回显/报错:
:messages
传统终端协议(xterm-256color)下,Ctrl+字母 只发送一个控制字符(如 Ctrl+L = ^L = 12),加不加 Shift 结果一样,Neovim 无法区分 <C-l> 和 <C-S-l>
当前配置的解决方案:发送 CSI-u 序列
Kitty、WezTerm、Ghostty 与 tmux 会为需要区分的组合键显式发送 CSI-u 序列。Neovim 因而可以直接识别原始组合键,不需要借用 F13~F24 作为中间键
例如,Ctrl+Shift+L 使用类似 \x1b[108;6u 的序列,其中 108 是字母 l 的码点,6 表示 Ctrl 与 Shift 修饰键。排查时可在 Neovim 中执行 :lua print(vim.fn.keytrans(vim.fn.getcharstr())),再按目标组合键确认最终识别结果
CSI-u 需要终端及中间的 tmux 链路共同保留扩展键码。本仓库已经为支持的终端配置好这条链路