强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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-lspLSP 补全源
cmp-buffer缓冲区补全
cmp-path路径补全
LuaSnipSnippet 引擎
friendly-snippets预制 Snippets

下一步第 14 章 - Tree-sitter → 增量语法解析、精确高亮和文本对象。


扩展阅读