Skip to content

nvim guide keymaps

ES edited this page Jul 16, 2026 · 1 revision

Neovim 快捷键(Keymap)速查指南

vim.keymap.set({mode}, {lhs}, {rhs}, {opts})
  • mode:在哪些模式下生效,例如 "n"(normal)、{ "n", "v" }
  • lhs:按键本体(left-hand side),如 <leader>ff
  • rhs:执行的动作(right-hand side),可为字符串或 Lua 函数
  • opts:可选参数表,控制描述、是否静默、是否可递归等

1. mode —— 触发模式

取值 模式 说明
"n" Normal 正常模式
"i" Insert 插入模式
"v" Visual + Select Visual 与 Select 模式
"x" Visual 仅 Visual 模式(字符、行或块选择)
"s" Select 仅 Select 模式
"o" Operator-pending 操作等待模式,例如 dy 之后
"t" Terminal 内置终端 buffer
"c" Command-line 命令行模式
{ "n", "v" } 多模式表 数组形式可一次绑定多个模式
"" Normal、Visual、Select、Operator-pending 对应 :map 的多模式范围

2. lhs —— 触发键

  • 直接填写字符:"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>"


3. rhs —— 真正执行的动作

3.1 字符串形式

  • 直接传入可执行的 Vim 命令串,如:"<cmd>w<cr>"":echo 'hi'<CR>""<C-w>h"
  • 结尾务必加 <CR> 以执行命令
  • 也可以写纯按键序列,例如 " (用于复制)、"o<esc>"(在下方插入新行并退出)

<cmd>xxx<cr> vs :xxx<CR> 的区别

在 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>

3.2 函数形式

  • 传入 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,见下文


4. opts —— 选项表

选项 类型 默认值 作用 & 常见用法
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.setopts 表会原样存放,可自行扩展

4.1 remapnoremap 详解(递归映射 vs 非递归映射)

这是 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> 追溯


5. 实战示例

示例 1:最常见的保存快捷键

vim.keymap.set({ "i", "n", "v" }, "<C-s>", "<cmd>w<cr>", {
  desc = "Save File / 保存当前文件",
  silent = true,
})
  • 模式:插入、普通、可视模式都能用
  • 行为:执行 :w 保存
  • desc 会在 which-key 或通知中显示

示例 2:表达式映射(软换行友好)

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

示例 3:只在特定文件类型启用

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,
})
  • 通过 FileType autocmd + buffer = event.buf 实现 buffer-local 的 filetype 绑定

示例 4:Buffer 级别映射

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

6. 快捷键调试(查看按键与来源)

  • 看“当前输入的按键序列”: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

7. 终端键码限制与绕过

传统终端协议(xterm-256color)下,Ctrl+字母 只发送一个控制字符(如 Ctrl+L = ^L = 12),加不加 Shift 结果一样,Neovim 无法区分 <C-l><C-S-l>

当前配置的解决方案:发送 CSI-u 序列

Kitty、WezTerm、Ghostty 与 tmux 会为需要区分的组合键显式发送 CSI-u 序列。Neovim 因而可以直接识别原始组合键,不需要借用 F13F24 作为中间键

例如,Ctrl+Shift+L 使用类似 \x1b[108;6u 的序列,其中 108 是字母 l 的码点,6 表示 Ctrl 与 Shift 修饰键。排查时可在 Neovim 中执行 :lua print(vim.fn.keytrans(vim.fn.getcharstr())),再按目标组合键确认最终识别结果

CSI-u 需要终端及中间的 tmux 链路共同保留扩展键码。本仓库已经为支持的终端配置好这条链路

Clone this wiki locally