Vim / Neovim 完全指南 / 13 - 代码补全
“Good completion is the difference between typing and thinking.”
13.1 nvim-cmp 概览
13.1.1 架构
nvim-cmp(补全引擎)
├── 补全源(Sources)
│ ├── cmp-nvim-lsp → LSP 补全
│ ├── cmp-buffer → 缓冲区单词
│ ├── cmp-path → 文件路径
│ ├── cmp-cmdline → 命令行补全
│ ├── cmp_luasnip → Snippet 补全
│ └── 自定义源
├── Snippet 引擎
│ ├── LuaSnip → 推荐
│ └── vim-vsnip
└── 扩展
├── cmp-nvim-lsp-signature-help → 函数签名
└── cmp-nvim-lsp-document-symbol → 文档符号
13.2 nvim-cmp 安装与配置
13.2.1 插件声明
-- ~/.config/nvim/lua/plugins/completion.lua
return {
{ "hrsh7th/nvim-cmp",
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-nvim-lsp", -- LSP 补全
"hrsh7th/cmp-buffer", -- 缓冲区补全
"hrsh7th/cmp-path", -- 路径补全
"hrsh7th/cmp-cmdline", -- 命令行补全
"L3MON4D3/LuaSnip", -- Snippet 引擎
"saadparwaiz1/cmp_luasnip", -- Snippet 补全源
"rafamadriz/friendly-snippets", -- 预制 Snippets
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
-- 加载 VS Code 格式的 Snippets
require("luasnip.loaders.from_vscode").lazy_load()
cmp.setup({
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }),
-- Tab / Shift-Tab 导航
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" }, -- LSP 补全
{ name = "luasnip" }, -- Snippet
{ name = "path" }, -- 路径
}, {
{ name = "buffer" }, -- 缓冲区单词
}),
formatting = {
format = function(entry, vim_item)
local source_names = {
nvim_lsp = "[LSP]",
luasnip = "[Snip]",
buffer = "[Buf]",
path = "[Path]",
}
vim_item.menu = source_names[entry.source.name] or ""
return vim_item
end,
},
experimental = {
ghost_text = { hl_group = "CmpGhostText" },
},
})
-- 命令行补全
cmp.setup.cmdline("/", {
mapping = cmp.mapping.preset.cmdline(),
sources = { { name = "buffer" } },
})
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources(
{ { name = "path" } },
{ { name = "cmdline" } }
),
})
end,
},
}
13.3 LuaSnip
13.3.1 Snippet 语法
-- ~/.config/nvim/lua/snippets/python.lua
local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
return {
-- def 函数模板
s("def", {
t("def "), i(1, "function_name"),
t("("), i(2, "args"),
t(") -> "), i(3, "None"),
t({ ":", "\t" }), i(4, "pass"),
}),
-- class 模板
s("class", {
t("class "), i(1, "ClassName"),
t({ ":", "\tdef __init__(self" }),
t(") -> None:", "\t\t"),
i(2, "pass"),
}),
-- if __name__ == "__main__"
s("main", {
t({ 'if __name__ == "__main__":', "\t" }),
i(1, "main()"),
}),
-- docstring
s("doc", {
t({ '"""', "" }),
i(1, "description"),
t({ "", '"""' }),
}),
}
13.3.2 Snippet 加载
-- 加载自定义 Snippets
require("luasnip.loaders.from_lua").load({
paths = "~/.config/nvim/lua/snippets/",
})
-- 加载 VS Code 格式 Snippets
require("luasnip.loaders.from_vscode").lazy_load()
-- 加载特定目录
require("luasnip.loaders.from_vscode").lazy_load({
paths = { "~/.config/nvim/snippets" },
})
13.3.3 Snippet 快捷键
-- 跳转到下一个占位符
vim.keymap.set({ "i", "s" }, "<C-l>", function()
if ls.expand_or_jumpable() then
ls.expand_or_jump()
end
end)
-- 跳转到上一个占位符
vim.keymap.set({ "i", "s" }, "<C-h>", function()
if ls.jumpable(-1) then
ls.jump(-1)
end
end)
-- 选择下一个选项(choice node)
vim.keymap.set({ "i", "s" }, "<C-k>", function()
if ls.choice_active() then
ls.change_choice(1)
end
end)
13.4 补全行为定制
13.4.1 补全触发
cmp.setup({
completion = {
autocomplete = { cmp.trigger },
completeopt = "menu,menuone,noinsert",
},
-- 自定义触发长度
-- 不配置则默认输入即触发
})
13.4.2 排序与过滤
cmp.setup({
sorting = {
priority_weight = 2,
comparators = {
cmp.config.compare.offset,
cmp.config.compare.exact,
cmp.config.compare.score,
cmp.config.compare.recently_used,
cmp.config.compare.kind,
cmp.config.compare.sort_text,
cmp.config.compare.length,
cmp.config.compare.order,
},
},
})
13.4.3 补全项样式
formatting = {
fields = { "kind", "abbr", "menu" },
format = function(entry, vim_item)
local kind_icons = {
Text = "",
Method = "",
Function = "",
Constructor = "",
Field = "",
Variable = "",
Class = "",
Interface = "",
Module = "",
Property = "",
Unit = "",
Value = "",
Enum = "",
Keyword = "",
Snippet = "",
Color = "",
File = "",
Reference = "",
Folder = "",
EnumMember = "",
Constant = "",
Struct = "",
Event = "",
Operator = "",
TypeParameter = "",
}
vim_item.kind = string.format("%s %s", kind_icons[vim_item.kind] or "", vim_item.kind)
return vim_item
end,
}
13.5 代码动作(Code Action)
代码动作由 LSP 提供,通过快捷键触发:
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "代码操作" })
vim.keymap.set("v", "<leader>ca", vim.lsp.buf.code_action, { desc = "代码操作" })
常见代码动作:
- 自动导入模块
- 修复类型错误
- 提取函数/变量
- 实现接口
- 添加缺失的字段
13.6 签名帮助(Signature Help)
{ "ray-x/lsp_signature.nvim",
event = "VeryLazy",
opts = {
bind = true,
handler_opts = { border = "rounded" },
hint_enable = true,
hint_prefix = "★ ",
},
}
或使用内置签名帮助:
vim.keymap.set("i", "<C-k>", vim.lsp.buf.signature_help)
13.7 命令行补全
-- 已在 nvim-cmp 配置中包含
-- : 命令行补全
cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources(
{ { name = "path" } },
{ { name = "cmdline" } }
),
})
-- / 搜索补全
cmp.setup.cmdline("/", {
mapping = cmp.mapping.preset.cmdline(),
sources = { { name = "buffer" } },
})
13.8 业务场景
| 场景 | 补全源 |
|---|---|
| 函数参数 | LSP + 签名帮助 |
| 变量名 | LSP + Buffer |
| 文件路径 | Path |
| 代码模板 | LuaSnip |
| 命令行 | cmdline |
| SQL 语句 | Buffer |
13.9 总结
| 组件 | 用途 |
|---|---|
| nvim-cmp | 补全引擎 |
| cmp-nvim-lsp | LSP 补全源 |
| cmp-buffer | 缓冲区补全 |
| cmp-path | 路径补全 |
| LuaSnip | Snippet 引擎 |
| friendly-snippets | 预制 Snippets |
下一步:第 14 章 - Tree-sitter → 增量语法解析、精确高亮和文本对象。
扩展阅读
- nvim-cmp 官方文档
- LuaSnip 文档
- Friendly Snippets
:h complete— 内置补全参考