AB

碎碎念:最后还是离开了 NeoVim

2025-11-20



从 JB 到 NeoVim

作为一个经典前端开发,我平时用到的语言主要是 JS/TS、Rust、Python 和 CPP,偶尔会写一写 Fish Script、Flutter 或者一些配置文件,这些需求在大多数时候都是通过 JB 编辑器来完成,编辑单文件配置或者脚本时,可能会用 VSC 辅助一下。23 年初,可能因为工作太无聊的原因,我开始尝试上手 Vim,感受到一种比较新奇的体验。Vim 的快捷键设计使他很适合做英文文本的编辑工作,借助语法解析器可以做到非常快速、精准的跳转,于是经过一段时间熟悉之后,我给手头的 IDE 都添加了 Vim Simulator。

23 年 Cursor 发布了,补全和提示能力上大幅超越了当时的 Github Copilot,这是我放弃 JetBrains 的契机。顺便吐槽一下,虽然后续我司也跟进做了自己的类似产品,也提供了 Jetbrains 插件支持,但这个产品只能说沿袭了我司一贯的内部工具风格 ── 难用,非常难用。

不过说实话,促使我放弃 JB 的核心原因并不是 LLM。羸弱的 IdeaVim 终究还是只能在编辑区使用,我无法使用键盘控制整个 IDE。随着 Vim 熟练度的上升,编辑代码时的键鼠切换变得越来越卡手。编辑当前文件的文本时,可以用键盘控制光标快速移动,而 Commit 代码却需要从键盘换到鼠标,然后“慢吞吞”地把光标移动到“版本控制”图标,定位到 Commit Message 输入框,输入完成之后点击 Commit 和 Push。编辑和 IDE 操作之间的割裂越来越明显,键鼠切换同时需要让大脑切换到不同的操作记忆,这让我感受到了无法忍受的混乱。

在切换到我目前稳定的 LazyVim 配置之前,我尝试了 VSC、Zed、Fleet、Helix 这些编辑器、 SpaceVim、LunarVim、AstroNvim 这些主流的集成化 Vim 配置,也尝试了从 Kickstart.nvim 开始自建 NVim 配置。LazyVim 是我最后的选择,原因很简单,舒适的默认快捷键配置和按键提示,丰富的预设配置,并且依然在积极维护(AstroNvim 和 LunarVim 都已经不再维护了)。

NeoVim 的痛点:依赖地狱

或者也可以叫配置地狱。众所周知,Vim 或者 NeoVim 的一个超大优势就是可以通过 VimScript 和 Lua 进行配置和插件开发。在社区里,大多数插件都 out-of-box,零配置或者编写少量配置(一般是快捷键)就能用上。所有配置当中,最麻烦的部分是 LSP 和 Formatter。很多时候 LSP 是黑箱,开源的 LSP 全靠 Repo 中提供的说明来使用,少有提供详尽文档的 LSP。

虽然缺少文档,配置困难,但 Nvim 官方已经提供了相对比较成熟的 LSP 配置起点,并且社区中也有不少帮助安装和配置 LSP 的工具,比如 Mason。在大多数情况下是可以顺畅工作的,比如 Rust、Python、Lua 项目和比较纯粹的 React/TS 项目。

由于 JS/TS 生态百花齐放(非常混乱),各种前端项目常常会使用独特的语法和文件格式,例如 Vue、Svelte、Astro、Scss、Less 等等,同时还依赖着各种纷繁复杂的周边工具,例如已经堪称基石的 TypeScript、ESLint、TailwindCSS。语法多样就意味着每种语言需要依赖不同的 LSP,依赖复杂也导致同一类型的代码文件可能依赖两个冲突的 LSP,以下有两个简单的例子。

例子一:Vue 的各种 LSP

Vue@2 和 Vue@3 是 Vue 的两个主流版本,关系像是过去的 Python27 和 Python3,它们拥有相似但不同的语法。Vue@2 所提供的 LSP 是 Vetur(或称 vls),Vue@3 所提供的 LSP 是 @vue/language-server(或称 vue_ls)。由于 vue_ls 需要对 tsserver(TypeScript 的 LSP,后改名 ts_ls)做一些配置上的变更,所以社区有好心人提供了更加完善的包装,也就是 vtsls。

vue_ls 为 Vue@2 提供的支持在 3.1 版本被移除了(#5455),所以想要让 NeoVim 同时支持 Vue2 和 Vue3 项目变得比较麻烦。我的配置方法是通过不同的环境变量加载不同的 Vue LSP。

例子二:Deno 和 TypeScript

第二个例子是 Deno。一般情况下,我们会使用 Node 或者 Bun 来运行本地的 TypeScript 项目,通过 tsconfig.json 来区分当前代码的运行环境是 browser、打包器或是 nodejs。而自带一切的 Deno 并不遵从这些古早繁琐的约定,Deno 运行时在全局环境提供了一个 Deno 对象,同时带来了全新的 ESM Import 规则(从 HTTP 链接远程导入代码库),这些能力都是原有的 tsserver 不具备的。因此,TypeScript 编写的 Deno 项目需要一些额外的工具来辅助处理这些独有的语法和逻辑,也就是 denols。

可以想到,我们要为同一类代码文件加载不同的 LSP,需要根据当前的 Workspace 做一些逻辑判断,常规的解决方案就是根目录有 package.json 时加载 tsserver,有 deno.jsonc? 时加载 denols。可惜我的使用场景是将 Deno 作为 NodeJS 的补充脚本,所以根目录会同时存在这两者的项目配置文件。我的解决方案是添加一个手动开关,当我编辑 Deno 文件时,打开这个开关,为所有 TypeScript 代码文件引入 denols。

LSP 配置复杂是配置地狱的一个侧面,依赖地狱的体现则是在升级的那一刻。Lazy.nvim 升级很容易,只要点一下 U 就可以。这很像是 pacman -Syu,打开潘多拉魔盒之后发生的任何事都不在你的掌控之中。虽然大多数升级之后不会有什么问题,但总有几次,升级之后出现了一些稀奇古怪的 Bug,Debug 起来又异常困难。

早知道,还是 Helix / Zed

使用 NeoVim 的一年中,我也尝试了很多种不同的方式,比如 Neovide、Zed 和 VSC 的 nvim 插件,最终还是稳定在了命令行这种经典用法。在 Claude Code 流行之后,TUI 相比 GUI 的优势就更大了,因为我基本上是 0 付费的大模型编程用户,无法直接使用 Claude、Copilot、Gemini、Codex 之类的 Key 来使用相应插件,所以 Zed 和 Cursor 对我来说都不是非常友好。

Helix 基本上是开箱即用的,它自带 TreeSitter,预设大量功能,同时可以自行处理大部分的 LSP 配置,并且暂时不支持插件系统。Helix 在我的开发设备上安装了许久,由于它的设计和 Vim 还是有不少差异,并且缺少一个侧边栏文件树(类似 NeoTree),所以一直没有尝试切换过去。

打通这最后一公里的时机,是我偶然发现了 Yazi 对 Helix 的支持(#12934),体验了一下,完全可用,只是存在少许延迟。基本上可以解决我对文件树的需求了。通过 Zellij 让 Helix 和 Claude Code 同屏,额外单开一个 Zellij 来单独运行开发服务器,日常开发就够用了。

Helix 的问题主要还是缺少插件社区导致的功能缺失,目前 Debugger 功能还在早期测试,Unit Test 集成基本不存在,Git 能力也相当孱弱,需要自己写一些脚本来获取 Hunk 和 Blame。

对于 Helix 难以处理的项目,我选择 Zed 作为备选方案,Zed 提供了 helix_mode,基本上可以无缝衔接。你问我为什么不用宇宙第一 VSCode?因为 VSC 的 Helix 插件有 Bug……

NeoVim 还是很不错的

虽然我切换到了 Helix,但或许有一天我还是会回到 NeoVim(with helix.nvim),虽然配置环境的代价未免有点大,备份、迁移起来也不算方便,但它确实还是最好用的现代 Vim 编辑器。