
我的 Neovim 配置起码用了八年了。
从一份 init.vim,陆续零零碎碎加了语法高亮、LSP 客户端等等。插件管理从 vim-plug 到和 packer 混着用,随着 neovim 版本升级又引入了一套 lua 配置。层层叠叠、里出外进、不可捉摸。
直到又一次更换了主力办公机,也有了 AI 的助力,才下决心重整一番河山。
Vim 们
在讲生态换血之前,可能有必要简单交代一下 Vim 的来龙去脉。
Vi 编辑器 1976 年问世后,Bram Moolenaar 于 1991 年推出改良版 Vim。2016 年发布 Vim 8,补齐了异步任务、内置包管理器、终端等现代化基础能力,以 Vimscript 为主要脚本语言,Lua 仅作辅助。2022 年迭代出 Vim 9,推出编译提速的 Vim9 Script 优化脚本性能,整体架构仍沿用多年底层代码,长期由 Bram 单人主导维护。
2014 年,开发者因不满 Vim 臃肿老旧的代码架构与缓慢迭代节奏,发起硬分叉创建 Neovim。项目主打模块化全异步内核,将 Lua 设为一等配置语言。2021 年 0.5 版本内置 LSP 与 Tree-sitter 迎来生态爆发,由社区团队共同管理,迭代速度远快于原版 Vim。
后续 Drew DeVault 推出 Vim Classic,锁定 Vim 8.2 特定版本不再跟进 Vim9,仅人工移植安全修复,坚守传统无 AI 参与开发的极简复古路线。而 GVim、MacVim 作为同源图形前端,始终同步对应 Vim 主干版本。
说白了,同一颗种子长出了好几棵树:Vim 9 往性能走、Neovim 往可编程走、Vim Classic 往极简走、GVim/MacVim 往桌面走。
生态换血
选定了 Vim 发行版本可没完,配置和插件的生态是第二关。
最早 Vim 插件是 Perl 和 Vimscript 的天下,大家用 Pathogen 或 Vundle 把插件像脚手架一样塞进 ~/.vim。后来 vim-plug 出来,清爽了不少。再往后 Neovim 喊出 "Lua First",于是 packer.nvim 长了出来------可它的作者 wbthomason 在 2023 年宣布归档,社区一夜之间转向 lazy.nvim。
我的旧配置正好踩在两代管理器交替的中间。它同时跑着 vim-plug 和 packer ------ 前者管 coc.nvim、nerdtree、fzf 这一票"老朋友",后者管后来加的 mason、rust-tools 和补全。两个管理器互不知情,各自维护自己的 pack/ 目录,启动时谁都不知道哪些插件被另一边接管了。
LSP 这条线更精彩。早年 coc.nvim 几乎一统江湖,把 VSCode 的 LSP 协议层打包成 Node.js 守护进程塞进 Vim。它有自己的扩展生态,装个 TypeScript 支持要 :CocInstall coc-tsserver,装 ESLint 要 :CocInstall coc-eslint,Vue 还得装 coc-volar,加上 coc-emmet、coc-css、coc-html、coc-json、coc-prettier ...... 配置散在 coc-settings.json 里另一套 JSON。代价是你的 Vim 实际跑着一个 Node 进程,启动慢、吃内存,还得维护两套生态。Neovim 0.5 把 LSP 客户端做进核心、nvim-lspconfig 接管配置之后,coc 的位置就尴尬了。
格式化和诊断方面,null-ls 是一代神器,把 prettier、eslint 这种非 LSP 工具伪装成 LSP server ------ 但 2023 年作者 jose-elias-alvarez 退坑,仓库归档。社区分裂出 conform.nvim 管格式化,nvim-lint 管诊断,各做各的事。
这些不是巧合,是一条很清晰的趋势:核心在不断把功能从插件层吸进去。
Neovim 0.11 引入 vim.lsp.config 和 vim.lsp.enable,nvim-lspconfig 那层包装就不必要了------你可以直接告诉核心"这个 server 的 filetypes 是什么",然后 vim.lsp.enable({ "ts_ls", "tailwindcss" }) 一行启动。treesitter 0.12 起内置高亮启动,nvim-treesitter.configs 这位老朋友直接消失,改成 vim.treesitter.start() 配合 FileType 自动命令。旧配置一升级就崩,是因为它调用的家具被搬走了。
升级到 NeoVim 12 后
把旧配置推倒,按三个原则重建。
第一,一件事只交给一个工具。旧配置里 vim-plug 和 packer 两个管理器同时跑,coc.nvim 和 nvim-lspconfig 两套 LSP 客户端互不知情,互相打架。新配置只留 lazy.nvim,LSP 走 Neovim 原生,格式化交给 conform。
第二,能用核心就用核心。每个版本 Neovim 都在长大,跟着核心走比抱着第三方包装层不放更不容易被时代甩下。
第三,按职责切分文件,而不是按时间堆叠。旧配置是按时间生长的------什么时候装了什么,就在 init.vim 里加一段。整个文件 200 多行,把 coc 配置、键位、session 自动保存、终端打开函数、文件树、FZF、Prettier、airline 全堆在一起。新结构按职责切:
bash
~/.config/nvim/
├── init.lua # 入口
└── lua/
├── config/ # vim 选项、键位、自动命令、lazy 引导
└── plugins/ # 一个文件一个主题:lsp、cmp、treesitter、telescope...
每个文件 return 一个 lazy.nvim 的插件 spec,管理器自己汇总。新加插件不用想着往哪加,新人接手也能一眼看出地图。
迁完之后插件清单也瘦了一圈。NERDTree 换成 nvim-tree,fzf.vim 换成 telescope,vim-smoothie 换成 neoscroll,vim-airline 换成 lualine,tcomment_vim 换成 Comment.nvim,vim-surround 换成 nvim-surround------每一个都按"键位语义兼容"挑的,后面会讲为什么。
通用原则
当然,读到这里的你,旧配置肯定长得跟我不一样,坑也是另一批。但有几件事值得知道。
一是你以为的"配置错误",经常是"插件抢键位"。我花了快一小时排查 Ctrl-b 为什么打不开文件树:映射明明注册了,按下去毫无反应。最后发现 neoscroll 这个平滑滚动插件偷偷接管了 <C-b>(它默认覆盖 page-up),加载顺序晚于我的全局键位,把映射盖掉了。在 Neovim 里,后加载的会盖前面的。所有想接管标准键位的插件都得看一眼默认行为------neoscroll 的解法是把 mappings 里的 <C-b> 删掉,只留 <C-u>、<C-d>、<C-f>、zt、zz、zb。
二是迁移最大的成本不是改代码,是改肌肉记忆。Ctrl-b 开文件树,你要问我可能答不出来,但手指往键盘上一放就知道。所以这次"键位完全保留"是硬要求,插件可以全换,体验不能动。社区里很多新插件其实是按"键位语义兼容旧版"设计的:Comment.nvim 兼容 tcomment 的 gc{motion} 和 gcc,nvim-surround 兼容 vim-surround 的 ys/cs/ds 三件套(ysiw" 加引号、cs"' 把双引号换成单引号、ds" 删掉外面的引号)。LSP 那一层也是,我把 coc 时代用的 gd(跳定义)、gr(rename)、K(hover)、gy(跳类型定义)、[a ]a(上下条诊断)、ga(code action)、<Leader>a(行内诊断浮窗)、<C-x><C-x>(insert 模式签名帮助)原样接到 vim.lsp.buf.* 上。选插件先看键位能不能对上,迁移成本会从"重新学习"降到"换底层引擎"。
诊断展示也保留了原来的习惯:关掉 virtual_text、留 signs、CursorHold 时弹浮窗。这种"按住光标半秒钟就看到错"的反馈方式,比一行内塞一长串红字读起来舒服得多------但很多默认配置喜欢上来就开 virtual_text,所以这条得自己写。
格式化也是同款思路。旧的 coc-prettier 在保存时格式化,新配置用 conform.nvim 在 BufWritePre 上挂一个 format_on_save,内部按 filetype 分发到 prettier。需要排除某些项目特有的文件类型时,在 format_on_save 回调里 return nil 就跳过,比 coc-settings.json 里维护一个 formatOnSaveFiletypesIgnore 数组干净得多。
顺手把 Claude 接进来
旧配置里其实早就接过 AI ------ 一个本地 ollama 插件,跑 qwen 系列模型。当时模型小、上下文短,主要用来补补函数名、写写正则,真要让它读一段代码再改两个文件就力不从心了。后来本地模型还在,但我用得越来越少,最后那行 require("ollama-config") 在文件里待了快一年没人调用过。这次清理顺手删了。
最近转去用 Anthropic 的 Claude Code,一开始的工作流是 Neovim 一边、独立终端一边,来回 Alt-Tab,复制路径粘贴代码片段。挺割裂。
后来发现 claude-code.nvim,把这个 CLI 直接塞进 Neovim 的内置终端。一个键弹窗、再按关掉、再按还是同一会话,Claude 改了文件 buffer 自动 reload,这才是该有的样子。配置不复杂:
lua
return {
{
"greggh/claude-code.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
cmd = { "ClaudeCode", "ClaudeCodeContinue" },
keys = {
{ "<C-,>", "<cmd>ClaudeCode<cr>", mode = { "n", "t" }, desc = "Toggle Claude Code" },
{ "<leader>cC", "<cmd>ClaudeCode --continue<cr>", desc = "Claude Code continue" },
},
config = function()
require("claude-code").setup({
window = { split_ratio = 0.35, position = "botright", enter_insert = true, hide_numbers = true },
refresh = { enable = true, updatetime = 100, timer_interval = 1000 },
keymaps = {
toggle = { normal = "<C-,>", terminal = "<C-,>" },
window_navigation = true,
scrolling = true,
},
})
vim.api.nvim_create_user_command("Claude", function(opts)
vim.cmd("ClaudeCode " .. (opts.args or ""))
end, { nargs = "?" })
vim.cmd([[cabbrev claude Claude]])
end,
},
}
cmd 和 keys 同时声明,因为 lazy.nvim 是按需加载------只写 keys 而键位的回调是空的,按下去什么都不会发生,插件也不会被触发加载。第一次配完我盯着 <C-,> 没反应、:ClaudeCode 报 E492: Not an editor command 排查了好一会儿,后来才想明白是触发条件没设对。两条路径都给上,要按键有按键、要命令有命令。
命令行里 :ClaudeCode 调出子窗口,用着和 CLI/IDE 体验一样顺。
迁完那天重新打开 Neovim,启动从吭哧半天到击败毫秒就绪,屏幕干干净净没有一行警告。Neovim 在以肉眼可见的速度把更多能力吸进核心。今天 LSP,明天 treesitter,AI来了也都会有办法。
那些归档的、断更的、被时代抛下的插件,曾经帮过我,完成了它们的使命;VSCode 也终于可以关掉了。