构建 Rust LSP 为何如此艰难
Why building a Rust LSP is hard

我并非 matklad,但正在开发 Rust Glancer 这个实验性 Rust LSP。在这个过程中,我深刻体会到 LSP 与编译器的本质区别:编译器追求二元确定的完成状态,而 LSP 必须在信息不全的情况下,尽可能快地为用户提供有用的反馈。从处理 LSP 协议中“无文件系统”的假设,到构建虚拟文件系统(VFS)以应对竞态条件;从按需解析当前文件,到为查找引用(References)而遍历整个工作区,每一个看似简单的功能背后都隐藏着巨大的工程挑战。本文将分享我在构建 LSP 过程中遇到的那些“看似简单实则艰难”的架构难题,以及如何在部分信息中产出有效答案的思考。
构建 LSP 的核心在于,必须从部分信息中产出有用的答案。
HN 评论区
71- Panzerschrek
这篇文章描述的很多内容不仅适用于 Rust,几乎适用于任何语言。比如,请求应该异步处理,以及需要进行 UTF-16 的转换,这些都很明显。但实际上并没有那么难。
我也为自己的语言编写过语言服务器。最难的是找到一种方法,能在文档处于编辑状态且语法不正确时,仍然提供有用的自动补全。这是最棘手的地方:如何在处理这种语法错误的同时,又不丢失必要的上下文。
- mitxela
在 Windows 上,这原本应该是直接的函数调用(COM),在 Eclipse 中则是 Java 模块间的直接函数调用,却要用 JSON TCP 连接来实现,这总让人觉得有点恶心。
- klodolph
我认为 Rust 的某些特性让这件事稍微难了一点。在让语言更简洁和增加有用的冗余之间,存在着某种张力,而 Rust 通常倾向于“简洁”这一侧,其中一些冗余特性会让工具链的使用稍微痛苦一些。比如导入语句。
impl std::fmt::Display for Blah {
}
如果你的语言要求你限定导入(如上所示),那么你的 LSP 就能愉快地、可靠地执行某些操作(比如重命名),即使项目的某些部分无法解析。但如果你使用了通配符导入 std::fmt,同时又通配符导入了其他东西,那你就完了。Display 可能来自任何地方(也许来自一个当前存在解析错误的模块)。我非常欣赏那些完全禁止通配符导入(或其等效功能),或者典型代码中不使用它的语言。
与此同时,如果你添加一个新文件,还得跳一小段舞:
mod mycoolmod;
然后你创建 mycoolmod.rs。或者反过来操作。这点小小的冗余(文件存在且已声明),似乎只是给 LSP 增加了一点摩擦,因为 mycoolmod 在父模块中声明之前无法获得正常的 LSP 支持(你必须先创建两者,然后会暂时出现一个诊断提示,说你的模块未被使用一会儿)。这只是一个小问题,是 Rust 工具链中又一点与类型系统无关的摩擦。
- chrisjj
> 为什么构建 Rust LSP 如此艰难
显然,没搞懂 LSP 是什么意思更难。
我本来还感兴趣,直到发现这个项目仅仅是一个 LSP 服务器。
- octoberfranklin
欢迎来到地狱:LSP 假设它是唯一的事实来源,但你仍然需要自己访问文件系统,并且要以同步的方式进行。
LSP 是技术设计彻底糟糕的典范。
别再让微软设计协议和 API 了。他们在这方面简直烂透了。