Qirui's Blog

我的第一次 Vibe Coding:打造 lf.nvim

一直想解决的问题

我使用 Lingua Franca——一种面向并发与分布式系统、支持多种编程语言的协调语言。它的官方工具集成在 VSCode 里,而我日常用的是 Neovim。这个落差一直让我有些不爽。

2025 年 11 月 22 日,我坐下来,决定动手解决它。不是先埋头啃几周文档,而是试试 vibe coding:只有我和 Claude,一边聊我想要什么,一边看着代码成形,再根据实际效果随时调整方向。

当时我完全没想到,短短几天,它就会变成一个超过 4,500 行代码的插件。

lf.nvim 功能演示

第一天:一口气做出雏形

第一条提交出现在下午 4:48:“Initial commit: LF file manager integration for Neovim.”

到了晚上 7:39——还不到三个小时——仓库里已经有了 8 次提交。插件从零起步,已经能做到:

  • 首次使用时自动构建图表功能所需的依赖
  • 在交互式图表中点击元素,跳转到对应源码
  • 支持任意深度嵌套的 reactor 层级
  • 通过程序化 LSP 请求实现即时、无闪烁的跳转
  • 妥善处理外部或导入的 reactor

最后这一部分很有意思。最初的图表到源码跳转虽然能用,但光标会明显闪烁:先跳到实例声明,再跳到类型名,最后触发“跳转到定义”。总共跳三次。我说,这种感觉有点卡顿。几分钟后,Claude 就重写了跳转逻辑,在后台直接发出一次 LSP 请求——瞬间到位,用户看不到任何中间位置。这类细节打磨,平时往往会被推到“以后再说”。

逐渐成形的架构

最有挑战性的部分是交互式图表查看器。VSCode 扩展使用 KLighD 渲染 reactor 图表,通信采用基于 Sprotty 的协议。要在终端编辑器里实现同样的体验,就得搭建一个三层系统:

Browser (React/KLighD) <-> WebSocket <-> Node.js Sidecar <-> RPC <-> Neovim <-> LSP

浏览器负责渲染图表,Node.js 辅助进程负责转发消息,Neovim 注入文件上下文,并与 LF Language Server 通信。在浏览器中点击一个 reactor,Neovim 中的光标就会跳到它的定义处。

一开始,我们把协议用错了:把 diagram/accept 当作 command 发送,而它其实应该是 notification。后来,我们阅读 Sprotty 源码,弄清了边界计算的握手流程:requestBounds → computedBounds → setModel。这类协议细节,独自开发时可能要反复试错好几个小时。用 vibe coding 的方式,我们先试一种方案,我反馈实际发生了什么,Claude 再调整。

第二天:图表随动更新与 SSH 支持

第二天,插件有了随编辑器状态自动更新图表的能力:在 Neovim 中切换文件,浏览器里的图表就会自动刷新。我们最终采用了一个简洁的 URL 设计:浏览器只需访问不带参数的 http://localhost:8765/,当前文件的 URI 由 Neovim 动态注入。

我有时会通过 SSH 工作,所以我们加了一个 no_browser 模式:不自动打开浏览器,只打印 URL,方便做端口转发。实际上,后来我们还把它设成了默认模式。这个决定不大,却体现了 vibe coding 处理边缘场景的自然:我提了一下自己的 SSH 工作方式,这个功能就做出来了。

第五天:Tree-sitter

到 11 月 27 日,插件已经有了自定义的 tree-sitter 语法。不只是基础高亮,还完整支持了嵌入式语言代码块。在 LF 文件中写下 target C,其中的 C 代码块就能获得正确的 C 语法高亮。Python、TypeScript、Rust 和 C++ 也一样。

tree-sitter 相关代码还放进了独立仓库 tree-sitter-lf,其中包含高亮、局部变量、文本对象、折叠和缩进所需的 query 文件。:LFTSInstall 命令会处理整个安装过程:检测 parser 目录,必要时进行编译,然后安装 query 文件。

第二周:更细粒度的跳转

12 月 1 日,我们又往前走了一步。图表导航现在不仅能跳到 reactor 实例,还能跳到具体的 port 和 reaction。为此,我们需要解析 KLighD 的元素 ID 格式,例如表示 port 的 $$P0,以及表示 reaction 的 _reaction_1,再根据索引匹配源码位置。这里有几个细节:KLighD 的 port 索引从 0 开始,reaction 索引却从 1 开始;而且 LSP 不会把 reaction 暴露为 symbol,所以我们必须直接搜索源码文本。

几个月后:CI/CD

2026 年 2 月 8 日,最后一次开发会话把剩余的手动步骤也自动化了。现在,CI 工作流会根据上游发布版本构建 LSP jar。每周运行的同步任务会检查 VSCode 扩展的 TextMate 语法是否新增了关键字,有变化就自动创建 PR。:LFLspInstall 命令则会下载预构建的 jar,用户不再需要克隆整个 Lingua Franca 仓库。

一些数字

  • 大约 11 周内,完成了 34 次提交
  • 4,573 行 Lua 代码,分布在 16 个模块中
  • 三层架构:浏览器、辅助进程和 Neovim
  • 从首次提交到图表功能可用:3 小时
  • 从首次提交到支持 tree-sitter:5 天

Vibe coding 实际上是什么感觉

它并不是“AI 替我写代码”那么简单。更像是和一个极有耐心、熟悉各种你从未用过的 API 的搭档结对编程。我知道自己想要什么,也熟悉 Lingua Franca 生态。Claude 熟悉 Lua、Neovim API、tree-sitter 内部机制、LSP 协议细节,以及 TypeScript/React 的开发模式。

整个节奏是:我描述需求,代码出现,我试用,反馈实际情况,然后立即改进。这个反馈循环以分钟计,而不是小时。

工作区里的 .claude/ 目录记录了这段过程:会话笔记、架构文档、速查资料,都是在开发过程中生成的。最终留下的不只是代码,还有一个知识库,让我即便隔了几个月,也能从上次停下的地方继续。

最让我意外的是:代码质量很好。它有清晰的模块划分,也处理了各种边缘情况。975 行的 diagram_klighd.lua 把 ID 解析、LSP 跳转和 UI 逻辑分得很清楚。这并不是因为我在一开始就精心设计好了架构,而是因为 vibe coding 让迭代足够快,好的结构可以在解决实际问题的过程中自然形成。

我还会这样做吗?

我已经又这么做过了。但第一次的体验——看着一个功能丰富的 Neovim 插件在一个下午逐渐成形,随后一路加入交互式图表、LSP 集成、tree-sitter 高亮和 CI/CD 自动化——改变了我对“一个人究竟能做出什么”的认识。

如果你也使用 Lingua Franca 和 Neovim,可以看看这个插件:github.com/remifan/lf.nvim。即使你不使用它们,也许真正值得带走的是这个想法:那个你一直希望有人做出来的工具,从“要是有就好了”到“我已经做出来了”,距离可能比你想象的更短。