使用 rust 开发 vscode 插件

由于 node 是单线程架构,对于性能要求较高的插件来说,难以胜任。所以,建议使用 rust 来开发高性能插件。

为了充分利用 rust 的性能,建议采用异步的 tokio 库,使用 tower-lsp 的语言服务器实现来开发。

为了方便搭建项目,我制作了一个模版,使用以下命令:

git clone https://github.com/ren-wei/rust-lsp-extension-template.git <your-project-name>

打开此项目后在 vscode 中搜索并替换 rust-lsp-extension-template.

搜索 rust_lsp_extension_template_server 然后修复它。

然后,执行以下命令:

cd <your-project-name>
rm -rf .git
git init
npm i

活动事件

插件需要先在根目录的 package.json 文件的扩展字段 activationEvents 中声明激活的条件,只有满足激活条件,插件才会开始工作。下面是所有可选的条件:

已配置的活动事件是 "onLanguage:typescript",表示在 typescript 文件被打开时,激活此扩展。

客户端文档筛选器

文档筛选器 documentSelector 通过不同的属性(如语言、其资源的方案或应用于路径的 glob-pattern)来表示文档,只有符合筛选的文件的打开,修改和关闭,客户端才会发送对应的 textDocument/* 通知。

已配置的筛选器是 [{ scheme: "file", language: "typescript" }],表示扩展只关注 typescript 文件的打开、修改和关闭等活动。

打印日志

服务器预设了日志库 tracing 来帮助打印日志,只需要在任何您需要打印日志的函数或方法中使用 debug!("content"), info!("content"), warn!("content")error!("content") 来打印不同等级的日志。具体使用请参考 使用 tracing 记录日志

语言功能

我们参考 Hover 增加一个 Hover 的功能。

首先在初始化请求的响应中,声明支持 hover 能力:

    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
        Ok(InitializeResult {
            capabilities: ServerCapabilities {
                text_document_sync: Some(TextDocumentSyncCapability::Kind(
                    TextDocumentSyncKind::FULL,
                )),
+                hover_provider: Some(HoverProviderCapability::Simple(true)),
                ..Default::default()
            },
            server_info: Some(ServerInfo {
                name: "rust-lsp-extension-template-server".to_string(),
                version: Some("1.0.0".to_string()),
            }),
        })
    }

然后,处理 hover 请求:

    async fn hover(&self, _params: HoverParams) -> Result<Option<Hover>> {
-        error!("method not found");
-        Err(Error::method_not_found())
+        Ok(Some(Hover {
+            contents: HoverContents::Scalar(MarkedString::String("hover result".to_string())),
+            range: None,
+        }))
    }

然后重启即可。

插件配置

package.json 中的 contributes.configuration 是用来定义插件的配置的。用户将能够使用设置编辑器或直接编辑 JSON 设置文件,将这些配置选项设置为用户设置工作区设置。对于定义配置项,参考 contributes.configuration

此项目已存在设置的示例,要定义和使用一个配置,需要以下步骤: 使用插件配置有两种模式: 拉取模式推送模式,语言服务协议推荐使用前者。

拉取模式

请求从服务器发送到客户端,从客户端获取配置设置。

    #[instrument]
    async fn initialized(&self, _params: InitializedParams) {
        info!("start");
+        let config = self
+            ._client
+            .configuration(vec![ConfigurationItem {
+                scope_uri: None,
+                section: Some("languageServerExample.maxNumberOfProblems".to_string()),
+            }])
+            .await;
+        debug!("{:?}", config);
        info!("done");
    }

推送模式

推送模式是服务器使用注册模式注册空配置更改,客户端通过事件发出配置更改信号。

  1. 注册配置变更请求:
    #[instrument]
    async fn initialized(&self, _params: InitializedParams) {
        info!("start");
+        let _ = self
+            ._client
+            .register_capability(vec![Registration {
+                id: "register-did-change-configuration-id".to_string(),
+                method: "workspace/didChangeConfiguration".to_string(),
+                register_options: None,
+            }])
+            .await;
        info!("done");
    }
  1. impl LanguageServer for LspServer 中新增方法处理配置变更请求:
#![allow(unused)]
fn main() {
    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
        let _ = params;
    }
}

提示: 此处为了避免更改过多代码,直接使用了 _client,实际使用时,应该将其重命名为 client