正确地把 Neovim 用起来
这份教程从零搭一套现代、模块化、可按需裁剪的 Neovim 配置:先讲心法,再讲插件体系,然后用 React 与 Python 两个完整实例把整条链路跑通,最后告诉你怎么扩展任意语言、怎么只加载想要的功能、怎么塞进你自己的配置。
00 心法与目标
很多人「用过很久 Vim/Neovim」却没进入高强度状态,卡点几乎都不在配置,而在两件事没建立起来:
- 模态编辑的肌肉记忆。真正的效率来自 motion + operator 的组合(
diw、ci"、ya}),而不是插件。配置只是放大器——肌肉记忆是本金。 - 「编辑器即配置」的心智模型。Neovim 的一切都是可被 Lua 读写的状态。理解「插件管理器 → 语言服务器 → 语法树 → 补全」这四层各自负责什么,配置就不再是抄来的黑魔法。
所以本教程刻意采用从零手搭、模块化的方式,而不是直接套用 LazyVim 这类发行版。发行版能让你五分钟有个 IDE,但你想要的是「能扩展、能裁剪、能看懂每一行」——那必须自己搭一遍。
① 插件管理器(lazy.nvim)决定装什么、何时加载 → ② LSP(vim.lsp + Mason)提供跳转/补全/诊断/重命名 → ③ Treesitter 提供精确的语法高亮与文本对象 → ④ 补全引擎(blink.cmp)把 LSP 的候选项渲染成菜单。后面所有内容都是在填这四个格子。
01 安装与目录结构
先确认版本。本教程依赖 Neovim 0.11+ 的原生 LSP API(vim.lsp.config / vim.lsp.enable),0.12 还自带了 vim.pack 插件管理器与 :lsp 命令组。你是 Arch 用户,直接上仓库版即可:
# Arch
sudo pacman -S neovim ripgrep fd git
nvim --version # 确认 ≥ 0.11,最好 0.12
# macOS(你的 M3 Max 那台)
brew install neovim ripgrep fd
ripgrep(rg)和 fd 是后面全局搜索/找文件的后端,强烈建议一起装。再装一个 Nerd Font(如 JetBrainsMono Nerd Font)让图标正常显示。
目录结构:一个文件一个职责
所有配置都在 ~/.config/nvim/。入口是 init.lua,但我们让它只负责「按顺序 require 各模块」,真正的内容拆进 lua/ 下。这样删一个文件就等于删一块功能。
~/.config/nvim/
├── init.lua # 唯一入口,只做 require
├── lua/
│ ├── config/
│ │ ├── options.lua # 选项 + leader 键
│ │ ├── keymaps.lua # 全局快捷键
│ │ ├── autocmds.lua # 自动命令
│ │ └── lazy.lua # 引导插件管理器
│ └── plugins/ # 每个文件 = 一个/一组插件,自动加载
│ ├── treesitter.lua
│ ├── telescope.lua
│ ├── lsp.lua
│ ├── completion.lua
│ ├── formatting.lua
│ ├── ui.lua
│ ├── lang-react.lua # 语言专属,可整文件增删
│ └── lang-python.lua
└── after/
└── ftplugin/ # 按文件类型的局部设置
└── python.lua
-- 顺序很重要:options 里设了 leader,必须早于插件加载
require("config.options")
require("config.keymaps")
require("config.autocmds")
require("config.lazy") -- 最后引导 lazy.nvim
02 基础配置(先不碰插件)
在装任何插件之前,先把裸 Neovim 调舒服。这能帮你分清「哪些行为是内置的,哪些是插件给的」。
-- Leader 键必须在加载插件前设置,否则插件按键映射会用错前缀
vim.g.mapleader = " " -- 空格作为 leader
vim.g.maplocalleader = "\\"
local opt = vim.opt
opt.number = true -- 行号
opt.relativenumber = true -- 相对行号(配合 5j / 3k 跳转)
opt.mouse = "a"
opt.clipboard = "unnamedplus" -- 与系统剪贴板打通
opt.expandtab = true -- Tab 转空格
opt.shiftwidth = 2
opt.tabstop = 2
opt.smartindent = true
opt.ignorecase = true -- 搜索忽略大小写……
opt.smartcase = true -- ……但含大写时区分
opt.termguicolors = true -- 真彩色,主题正常显示的前提
opt.signcolumn = "yes" -- 常驻标记列,避免诊断图标抖动
opt.scrolloff = 8 -- 光标上下保留 8 行
opt.undofile = true -- 持久化撤销历史
opt.splitright = true
opt.splitbelow = true
local map = vim.keymap.set
map("n", "<leader>w", "<cmd>w<cr>", { desc = "保存" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "退出" })
map("n", "<Esc>", "<cmd>nohlsearch<cr>", { desc = "清除搜索高亮" })
-- 窗口间移动
map("n", "<C-h>", "<C-w>h"); map("n", "<C-l>", "<C-w>l")
map("n", "<C-j>", "<C-w>j"); map("n", "<C-k>", "<C-w>k")
-- 可视模式下移动选中行
map("v", "J", ":m '>+1<cr>gv=gv")
map("v", "K", ":m '<-2<cr>gv=gv")
空格在普通模式下默认无用、位置居中、双手都能按,是社区事实标准。后面所有自定义命令都会挂在 Space 后面,形成一套你自己的命令树。
03 插件管理器:lazy.nvim
lazy.nvim 是目前的主流选择,核心卖点是惰性加载(lazy-loading):插件默认不在启动时加载,而是在某个事件、命令、按键或文件类型触发时才加载。这正是「只加载想要的功能」的底层机制。
-- 自动 clone lazy.nvim 到 data 目录(首次启动)
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath })
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = {
-- 关键:自动导入 lua/plugins/ 下的每一个文件作为插件规格
{ import = "plugins" },
},
install = { colorscheme = { "habamax" } },
checker = { enabled = true, notify = false }, -- 后台检查更新
})
有了 { import = "plugins" },lua/plugins/ 里每个返回插件表的 .lua 文件都会被自动收录。新增语言 = 新建一个文件;移除 = 删掉它。这就是模块化的好处。
常用命令::Lazy 打开管理面板,:Lazy sync 安装/更新/清理,:Lazy profile 看每个插件的启动耗时。
Neovim 0.12 自带 vim.pack.add{...},足以管理插件且零依赖。但它目前还缺 lazy.nvim 那套成熟的惰性加载与 UI,所以本教程仍以 lazy.nvim 为主。知道有这个选项即可。
04 核心插件(与语言无关)
这些插件无论你写什么语言都用得上,先把它们配好。每个都是 lua/plugins/ 下独立的一个文件。
Treesitter — 精确语法树
它用真正的语法解析器替代正则高亮,带来更准的高亮、缩进,以及基于语法的文本对象(如「选中整个函数」)。
return {
"nvim-treesitter/nvim-treesitter",
branch = "master", -- 经典稳定分支(main 分支正在重写中)
build = ":TSUpdate",
event = { "BufReadPost", "BufNewFile" }, -- 打开文件时才加载
opts = {
ensure_installed = { "lua", "vim", "vimdoc", "bash", "markdown" },
highlight = { enable = true },
indent = { enable = true },
},
config = function(_, opts)
require("nvim-treesitter.configs").setup(opts)
end,
}
Telescope — 模糊查找一切
找文件、全局搜内容、找缓冲区、搜帮助……都靠它。后端用前面装的 rg 和 fd。
return {
"nvim-telescope/telescope.nvim",
dependencies = {
"nvim-lua/plenary.nvim",
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
cmd = "Telescope", -- 按命令惰性加载
keys = { -- 也可按键触发;按下才加载
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "查找文件" },
{ "<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "全局搜索" },
{ "<leader>fb", "<cmd>Telescope buffers<cr>", desc = "缓冲区" },
{ "<leader>fh", "<cmd>Telescope help_tags<cr>", desc = "帮助文档" },
},
config = function()
require("telescope").setup({})
pcall(require("telescope").load_extension, "fzf")
end,
}
其余必备件
把这些丢进一个 ui.lua 里。这里只列清单和用途,配置都很短,按各自 README 抄即可:
| 插件 | 作用 |
|---|---|
folke/which-key.nvim | 按下 leader 后弹出可用快捷键提示,新手期的救命稻草 |
nvim-lualine/lualine.nvim | 状态栏 |
lewis6991/gitsigns.nvim | 行内 git 变更标记、blame |
stevearc/oil.nvim | 把目录当成可编辑的 buffer 来管理文件(比传统侧边栏更顺手) |
echasnovski/mini.nvim | 一个模块合集:mini.pairs 自动补括号、mini.surround 包裹/修改成对符号等 |
catppuccin/nvim 等 | 配色主题,挑一个 vim.cmd.colorscheme(...) |
05 LSP 系统:跳转、补全、诊断、重命名
这是从「文本编辑器」升级到「IDE 级」的关键一层。先理清三个常被混淆的角色:
客户端(Neovim 自带)
Neovim 内置的 vim.lsp 模块,负责和语言服务器对话、处理跳转/诊断/重命名/悬浮等。这部分不需要插件。
服务器(第三方程序)
如 lua-language-server、pyright、vtsls。它们是独立可执行文件,Neovim 不会自带,要单独装。
安装器 + 配置仓库
Mason 帮你下载服务器二进制;nvim-lspconfig 提供各服务器的默认配置(命令、文件类型、根目录标记),供原生 vim.lsp.enable() 读取。
0.11 起的原生流程极简:用 vim.lsp.config(name, {...}) 写/改配置,用 vim.lsp.enable(name) 启用。nvim-lspconfig 现在的职责主要就是提供这些预置配置,你只需覆盖想改的部分。
return {
"neovim/nvim-lspconfig",
dependencies = {
{ "mason-org/mason.nvim", opts = {} }, -- 安装器
"mason-org/mason-lspconfig.nvim", -- 桥接 mason 与 lspconfig
"saghen/blink.cmp", -- 补全(提供 capabilities)
},
event = { "BufReadPre", "BufNewFile" },
config = function()
-- ① 给所有 server 注入补全能力(来自 blink.cmp)
vim.lsp.config("*", {
capabilities = require("blink.cmp").get_lsp_capabilities(),
})
-- ② 让 Mason 确保这些 server 已安装,装好后自动 enable
require("mason-lspconfig").setup({
ensure_installed = { "lua_ls" }, -- 通用的放这;语言专属的放各自文件
automatic_enable = true,
})
-- ③ 针对单个 server 覆盖默认配置
vim.lsp.config("lua_ls", {
settings = { Lua = { diagnostics = { globals = { "vim" } } } },
})
-- ④ 快捷键:只在 LSP 真正挂到某个 buffer 时才绑定
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local m = function(k, fn, d)
vim.keymap.set("n", k, fn, { buffer = args.buf, desc = "LSP: " .. d })
end
m("gd", vim.lsp.buf.definition, "跳转到定义")
m("gD", vim.lsp.buf.declaration, "跳转到声明")
m("<leader>ca", vim.lsp.buf.code_action, "代码操作")
m("<leader>rn", vim.lsp.buf.rename, "重命名")
m("<leader>d", vim.diagnostic.open_float, "查看诊断")
end,
})
-- ⑤ 诊断显示样式(可选)
vim.diagnostic.config({ virtual_text = true, severity_sort = true })
end,
}
无需配置即可用:K 悬浮文档、grn 重命名、gra 代码操作、grr 查找引用、gri 跳实现、<C-s>(插入模式)签名帮助。上面只是再补几个你顺手的别名。
调试 LSP 的命令::checkhealth vim.lsp 看哪些配置已启用、是否挂载成功;:Mason 看/装服务器;:lsp restart(0.12)重启服务器;:LspLog 看日志。
06 补全引擎:blink.cmp
LSP 产出候选项,补全引擎负责把它们渲染成菜单并处理按键。blink.cmp 是当前的现代选择:Rust 写的模糊匹配、每次按键 0.5–4ms、开箱即用。老牌的 nvim-cmp 也很好,但要拼装一堆 source 插件,blink 更省心。
return {
"saghen/blink.cmp",
dependencies = { "rafamadriz/friendly-snippets" },
version = "1.*", -- 用稳定的 v1;v2 仍在大改
event = "InsertEnter", -- 进入插入模式才加载
opts = {
keymap = { preset = "default" }, -- <C-y> 确认, <C-n/p> 选择, <C-e> 关闭
appearance = { nerd_font_variant = "mono" },
completion = {
documentation = { auto_show = true, auto_show_delay_ms = 250 },
},
sources = {
-- 来源顺序即优先级:LSP 最先
default = { "lsp", "path", "snippets", "buffer" },
},
fuzzy = { implementation = "prefer_rust_with_warning" },
},
}
注意 lsp.lua 里那行 require("blink.cmp").get_lsp_capabilities()——它把「我支持哪些补全特性」告诉每个语言服务器,两者就此打通。如果换 nvim-cmp,对应换成 cmp_nvim_lsp 的 capabilities 即可。
07 格式化与 Lint
LSP 有时也能格式化,但用专门工具更可控。conform.nvim 负责格式化(保存时自动跑),nvim-lint 负责跑独立 linter。两者都按文件类型映射工具,扩展语言时只需往表里加一行。
return {
"stevearc/conform.nvim",
event = "BufWritePre",
cmd = "ConformInfo",
opts = {
formatters_by_ft = {
lua = { "stylua" },
-- 各语言的格式化器,在下面两个实例里继续填
},
-- 保存时格式化;某文件类型没配工具就回退到 LSP 自带的格式化
format_on_save = { timeout_ms = 1000, lsp_format = "fallback" },
},
}
stylua、prettierd、ruff 这些工具同样可以在 :Mason 里搜索安装,或用 mason-tool-installer 自动装。conform/lint 会在 PATH 里找它们。
08 实例一:React 前端环境
目标链路:TS/TSX 类型与跳转 + ESLint 诊断 + Tailwind 类名补全 + Emmet 展开 + Prettier 保存即格式化。把语言专属的东西集中到一个文件,这样整套前端能力就是「一个文件的存在与否」。
① 安装服务器与工具(Mason)
在 :Mason 里安装,或写进 ensure_installed:
| 角色 | 工具 |
|---|---|
| TS/JS 语言服务器 | vtsls(或 ts_ls) |
| ESLint | eslint(LSP 形态,可保存自动修复) |
| Tailwind | tailwindcss |
| Emmet | emmet_language_server |
| HTML / CSS | html, cssls |
| 格式化 | prettierd(快,常驻进程) |
② 语言专属配置文件
return {
"neovim/nvim-lspconfig", -- 复用同一插件,lazy 会合并配置
opts = function()
-- 让 Mason 确保这些已装
vim.list_extend(require("mason-lspconfig").get_installed_servers and {} or {}, {})
end,
config = function()
-- ESLint:保存时自动 EslintFixAll
vim.lsp.config("eslint", {
on_attach = function(_, bufnr)
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr, command = "EslintFixAll",
})
end,
})
-- 启用这一组前端服务器
vim.lsp.enable({
"vtsls", "eslint", "tailwindcss",
"emmet_language_server", "html", "cssls",
})
end,
}
lazy.nvim 会把多个 spec 的 opts 合并,但 config 函数只会执行最后一个。所以更稳妥的做法:把 vim.lsp.enable 的服务器列表统一放进主 lsp.lua,或在各语言文件里用 init/独立的非冲突插件来挂钩子。下面 Python 实例演示更干净的「独立文件」写法。
③ 补上 Treesitter 解析器与格式化器
在 treesitter.lua 的 ensure_installed 加上:tsx, typescript, javascript, html, css, json。在 formatting.lua 的 formatters_by_ft 加上:
local prettier = { "prettierd", "prettier", stop_after_first = true }
formatters_by_ft = {
lua = { "stylua" },
javascript = prettier, javascriptreact = prettier,
typescript = prettier, typescriptreact = prettier,
css = prettier, html = prettier, json = prettier,
}
打开一个 Vite + React 项目里的 .tsx,vtsls 会基于 package.json/tsconfig.json 自动定位项目根并启动。gd 跳组件定义、K 看类型、Tailwind 类名自动补全、保存自动 Prettier+ESLint。
09 实例二:Python 环境
现代 Python 组合是类型检查 + 极速 lint/format 分工:basedpyright(或 pyright)负责类型与跳转,ruff 负责 lint、格式化、整理 import。ruff 自带 LSP(ruff server),一个工具顶过去一堆。
① Mason 安装
basedpyright 和 ruff。(旧的 ruff-lsp 已废弃,直接用 ruff 即可。)
② 用「独立文件 + 独立插件」干净挂钩子
这次不去和主 lsp.lua 抢 config。我们用一个轻量插件 nvim-lspconfig 之外的钩子方式——直接在文件里写 vim.lsp.config 覆盖、再 enable,并通过一个一次性的 init 完成。最稳的写法其实是把所有 vim.lsp.enable 收进主文件,但若想保持「一个语言一个文件」,可用如下自成一体的写法:
return {
"neovim/nvim-lspconfig",
init = function()
-- init 在插件加载前运行,可叠加而不互相覆盖
-- basedpyright:类型检查交给它
vim.lsp.config("basedpyright", {
settings = {
basedpyright = {
analysis = { typeCheckingMode = "standard" },
},
},
})
-- ruff:关掉它的 hover,让 basedpyright 提供文档,避免重复
vim.lsp.config("ruff", {
on_attach = function(client)
client.server_capabilities.hoverProvider = false
end,
})
vim.lsp.enable({ "basedpyright", "ruff" })
end,
}
lazy 中每个插件的 init 都会执行(不会被覆盖),且早于插件加载;而同插件多个 config 只保留最后一个。用 init 调 vim.lsp.config/enable 既安全又能保持「一个语言一个文件」。
③ Treesitter 与格式化
给 ensure_installed 加 python, toml。格式化交给 ruff:
formatters_by_ft = {
-- ……前面的
python = { "ruff_organize_imports", "ruff_format" },
}
打开 .py 文件(项目里有 pyproject.toml 或 .git 即可定位根目录),basedpyright 给类型提示与跳转,ruff 标出 lint 问题、保存时整理 import 并格式化。要调试可再加 mfussenegger/nvim-dap + debugpy,按需即可。
10 扩展任意自己的语言(通用配方)
看完两个实例,你应该已经发现:加一门语言永远是同样的四步。把这个配方背下来,以后加 Go、Rust、Zig 都是机械操作。
装语言服务器
:Mason 搜索安装(或写进 ensure_installed)。不知道叫什么?去 nvim-lspconfig 的 lsp/ 目录或 Mason 列表里查。
启用(必要时覆盖配置)
新建 lua/plugins/lang-xxx.lua,在 init 里 vim.lsp.enable("server");要改设置就先 vim.lsp.config("server", {...})。
加 Treesitter 解析器
把语言名加进 ensure_installed,获得高亮、缩进与文本对象。
挂格式化器 / linter
在 conform 的 formatters_by_ft(必要时 nvim-lint)里加一行映射。
用你正在学的 Rust 走一遍——Rust 比较特殊,社区推荐用 rustaceanvim 这个封装插件,它内部驱动 rust-analyzer 并加上 inlay hints、cargo 集成等,不要再手动 vim.lsp.enable("rust_analyzer"):
return {
"mrcjkb/rustaceanvim",
version = "^6",
lazy = false, -- 它是 ftplugin,自己管理加载
-- 装好 rustup + rust-analyzer 后开箱即用
-- 自定义放 vim.g.rustaceanvim = { ... }
}
再加 Treesitter 的 rust 解析器、conform 里 rust = { "rustfmt" }——三步搞定。这说明配方里的「启用方式」可以替换成插件,但语法树、格式化两步永远一致。
11 如何只加载想要的功能
「只加载想要的」有两个层次:结构上只装你需要的插件,以及运行时只在需要时才加载。后者靠 lazy.nvim 的触发条件实现。
四种惰性加载触发器
| 字段 | 含义 | 例子 |
|---|---|---|
event | 某事件发生时加载 | event = "InsertEnter"(补全) |
ft | 打开某类型文件时加载 | ft = { "rust" } |
cmd | 执行某命令时加载 | cmd = "Telescope" |
keys | 按下某快捷键时加载 | keys = { "<leader>ff" } |
原则:UI/补全/LSP 用 event,工具类用 cmd/keys,语言专属用 ft。例如某个 Markdown 预览插件配 ft = "markdown",你不写 Markdown 时它根本不会进内存。
结构上的裁剪
- 删文件即删功能。不写 Python 了?删掉
lang-python.lua,下次:Lazy sync自动清理。 - 临时禁用而不删除:在某个 spec 里加
enabled = false。 - 条件加载:
enabled = function() return vim.fn.has("mac") == 1 end——只在某台机器装。你 macOS 和 Arch 两台机器共用同一份配置时很有用。 - 分析启动开销:
:Lazy profile看谁拖慢启动,把它改成惰性加载。健康的纯配置应在几十毫秒内启动。
语言服务器本身就是「按需」的:只有当你打开对应文件类型、且向上能找到根目录标记(.git、package.json、pyproject.toml 等)时才启动。所以即便你装了十种语言,进一个纯前端项目时也只有前端那几个服务器在跑。
12 如何添加自己的配置
三种粒度,从全局到单文件类型到单项目。
全局:autocmds 与 keymaps
自动命令是 Neovim 的事件钩子。两个高频例子:
local aug = vim.api.nvim_create_augroup("user", { clear = true })
-- 复制时高亮被复制的内容
vim.api.nvim_create_autocmd("TextYankPost", {
group = aug,
callback = function() vim.hl.on_yank() end, -- 0.11+ 用 vim.hl
})
-- 保存时去掉行尾空白
vim.api.nvim_create_autocmd("BufWritePre", {
group = aug,
callback = function()
local v = vim.fn.winsaveview()
vim.cmd([[keeppatterns %s/\s\+$//e]])
vim.fn.winrestview(v)
end,
})
按文件类型:after/ftplugin/
想「只在 Python 文件里」改某些设置,放 after/ftplugin/python.lua。打开 .py 时它会自动执行,且作用域是当前 buffer(用 vim.opt_local):
vim.opt_local.colorcolumn = "88" -- ruff 默认行宽参考线
vim.opt_local.shiftwidth = 4
vim.opt_local.tabstop = 4
-- 只在 python buffer 生效的按键
vim.keymap.set("n", "<leader>rr", "<cmd>!python %<cr>",
{ buffer = true, desc = "运行当前文件" })
按项目:exrc / 项目本地配置
在 options.lua 里 vim.o.exrc = true,Neovim 会加载项目根目录下的 .nvim.lua(首次会提示你信任该文件)。适合放「这个项目专用」的设置,比如某个 monorepo 的特殊路径——你的 UoA-TreasureDex 那种 pnpm monorepo 就很合适。
它会执行项目目录里的代码。Neovim 0.9+ 加了信任机制(首次询问、记住选择),但仍只对你信任的仓库开启。
13 速查与学习路径
练肌肉必做的事
:Tutor完整过一遍(30 分钟,回报极高)- 强迫自己用 w/b/e 而非方向键移动
- 练 operator+motion:ciw di" ya} cap
- 用 . 重复、f/t 行内跳、% 配对跳
- 遇到不会的就
:help 关键词
排错出问题先看这些
:checkhealth— 总体体检:checkhealth vim.lsp— LSP 是否挂载:Mason— 服务器/工具装了没:Lazy— 插件是否加载、报错:ConformInfo— 格式化器找到没:LspLog— 服务器日志
推荐的推进节奏
- 第一周:只用第 2 节的裸配置 + 第 4 节的 Telescope/which-key/treesitter,专注练 motion。别急着上 LSP。
- 第二周:接入 LSP + 补全(第 5、6 节),先只配一门你最常用的语言。
- 之后:按第 10 节的配方逐步加语言,按第 11 节把启动调快,按第 12 节沉淀你自己的习惯。
官方的 kickstart.nvim 是「单文件、带注释、教学向」的参考配置,思路和本教程一致,适合对照阅读。想直接要成品 IDE 体验则看 LazyVim——但建议你先按本教程手搭一遍,再决定。