Contribute to Rust Analyzer
前言
我写 Rust 用的 IDE 是 VSCode 搭配 Rust Analyzer 插件,二者搭配起来使用体验还是相当不错的。Rust Analyzer 虽然只是 VSCode 生态下的一个插件,但实际上可以看成 Rust 语言的一个 LSP(Language Server Protocol) 完整实现,提供了诸如补全提示、代码诊断、引用查找、跳转到定义等 IDE 常用功能。Rust Analyzer 插件可以分为前端和后端,前端是代码仓库中 editors/code 路径下的 TypeScript 项目,其余部分属于后端服务实现代码,我在这次贡献中涉及的是后端服务部分,下文中如无说明默认讨论的是 Rust Analyzer 后端服务。
LSP 只是一个协议,并没有规定实现的形式。Rust Analyzer 由 Rust 语言开发,开发团队为此做了许多工作,比如用 Rust 实现了 Parser、lsp_server 等工具。此外,Rust Analyzer 还有详细的开发文档,为 new contributor 提供了许多指引说明,,是我见过的开源项目中开发文档最详细的项目之一,甚至还有一个 Rust Analyzer 核心开发者主讲的系列视频(虽然我看不太懂听不太懂 🙃)介绍 Rust Analyzer 的工作流程。如果没有这些资料,从上手理解这么一个庞大的项目到能提 PR 解决某个小问题对我来说应该要花费更多的时间精力,再次感慨详细的开发文档对于 new contributor 加入社区的重要性。
入坑
每天都在使用 IDE,我对于 IDE 是如何实现这些功能是比较好奇的,某天心血来潮去 Rust Analyzer 的项目仓库逛逛,浏览了一遍 issue,发现 Feature request: if x.is_some() -> if let Some(_tmp) = x 这个我能看懂的 issue,于是打算从这个 issue 入手,通过 learning by doing 的方式去学习一下如何开发 Rust Analyzer。这个 issue 描述的是希望在 Rust Analyzer 中增加一个 feature,使得 IDE 能够智能感知并为 Option
以及 Result
类型的某些方法提供自动改写功能,如以下代码所示。其中 ┃ 表示光标的位置,意思是当光标停留在某个特定的 context 位置的时候 IDE 能提供一个自动改写功能。
1 |
|
过程
我第一版实现的效果是下面这样,当光标停留在 Option
或者 Result
对象的 is_some 或者 is_ok 方法的时候,通过点击小灯泡💡展示当下可选的功能,用户单击选择某个选项即可应用某项功能(在 Rust Analyzer 中这些点击💡后展示的功能称为 assist
)。
一开始我完全不知道从哪里入手,粗略过了一遍文档之后也没有什么头绪。于是我转变了思路,在已经合并的 PR 里面找是否有完成了类似的事情的 PR,终于找到了 feat: Add unqualify_method_call assist 这个 PR,它同样是提供了当光标在特定 context 位置时的一项可选的 assist
。assist
是 Rust Analyzer 的后端服务通过分析当前光标所处的 context 位置所提供的一些“智能”功能,关于小灯泡💡assist
Rust Analyzer 的官方博客中有一篇文章做了介绍,我也是在看了这篇博客之后才悟到原来这些 assist
是由服务端提供的而不是预先定义在客户端中的。
从这个参考 PR 中的 Files changed 中我知道了要增加一项 assist
需要在 crates/ide-assists/src/handlers
下增加一个对应的处理方法,然后注册到 /crates/ide-assists/src/lib.rs
中的 pub(crate) fn all() -> &'static [Handler]
方法,调用链如下。当光标停留的时候,Rust Analyzer 会检查所有在 all()
中注册的 assist
是否可用于当前的 context,如果可以则会将这项 assist
返回给客户端,对应的就是我们点击💡时看到的那些选项。
实现
Rust Analyzer 实现了一系列数据结构用于映射用户在 VSCode 中写的代码,参考其他的一些 assist
,我在实现 replace_is_method_with_if_let_method
这个 assist
的思路如下:
- 首先检查光标当前所处的 context 位置是否是一个 if 表达式,因为我要实现的
assist
是改写if x.is_some() {}
,所以要确保光标所处的位置是一个 if 表达式,这样才有必要进行后续的操作; - 读取 if 表达式中的 condition 部分,检查该 condition 是否是一个方法调用(
x.is_some()
),如果不是那这就不是我要的操作目标,直接 return 即可; - 如果是一个方法调用的话,这个方法调用的名字是否是
is_some
或者is_ok
,若不是则直接 return; - 确保光标位置的 context 是我要操作的目标代码后,构造出替代后的代码语句实现代码替换。
得益于 Rust Analyzer 所做的一些抽象,只用 30 多行代码我就完成了 replace_is_method_with_if_let_method 这项 assist
。思路其实很简单,而且 Rust Analyzer 也有许多 api 用于代码 context 检查以及信息提取(比如读取某个方法调用的发起对象名以及方法名等),完成的代码如下:
1 |
|
我认为这次的难点不在于实现思路,而是在于理解项目,知道要在哪里做出什么样的修改,理解 Rust Analyzer 的哪些数据结构对应了用户侧写的代码。关于这一点可以参考 Rust Analyzer 的 Syntax tree 结构,在 VSCode 中写下 rust 代码,按下 ctrl + shift + p 之后在搜索栏中输入 Syntax tree,按下回车之后就能在编辑器右侧得到代码的语法树结构。比如 let x: i32 = 1;
这行代码对应的语法树结果是下面这样的。其中 PATH_TYPE
、PATH_SEGMENT
、NAME_REF
这些关键字对应了 crates/syntax/src/ast/generated/nodes.rs
中对应的 struct
。
1 |
|
细节
在 replace_is_method_with_if_let_method 的初版实现中,完成代码改写后光标并不会自动选中 _tmp
变量。我希望它可以,这样用户在选择应用这项 assist
之后不需要任何鼠标和键盘的操作就可以直接编辑这个变量名。同样的,一开始我不知道如何实现,翻看了其他 assist
的实现似乎也没找到这样的功能,于是问了 ChatGPT,得到了一个能 work 的方法,利用 VSCode 中的 ${0:_tmp}
表达式实现自动范围选择。不过我这里的实现是用粗糙的手动构造方式,或者 Rust Analyzer 里有封装好某个对应的 api 而我还没找到…
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!