使用 typescript 开发 vscode 插件

这里不讨论常规的插件开发,仅讨论基于语言服务器的插件,对于常规插件的开发,请参考 Extension API。本文基于 VS Code Language Extensions

我们以一个 vue 插件为例来展示插件的开发。

直接基于 lsp-sample 项目进行开发:

git clone --depth=1 https://github.com/microsoft/vscode-extension-samples.git vue-lsp-extension
cd vue-lsp-extension
rm -rf .git

修改 package.json 中的 name, description, author 等信息。

npm i

这样,一个 vscode 的语言服务器插件的基本框架就搭好了。我们关注 client/src/extension.tsserver/src/server.ts ,它们分别是语言服务器的客户端和服务端。客户端使用了 vscode-languageclient 库,服务端使用了 vscode-languageserver 库。

活动事件

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

对于此项目,我们使用 onLanguage:vue:

    "activationEvents": [
-        "onLanguage:plaintext"
+        "onLanguage:vue"
    ],

客户端文档筛选器

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

修改 extension.ts:

    const clientOptions: LanguageClientOptions = {
        // Register the server for plain text documents
-        documentSelector: [{ scheme: 'file', language: 'plaintext' }],
+        documentSelector: [{ scheme: 'file', language: 'vue' }],
        synchronize: {
            // Notify the server about file changes to '.clientrc files contained in the workspace
            fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
        }
    };

注册语言

如果开发的插件不是语言插件,跳过此步骤。

如果 vscode 之前没有安装过与 vue 相关的语言服务器插件,那么名为 vuelanguage vscode 并不认识,因此,启动调试会发现插件并没有启动。

所以,如果您是为新的语言添加扩展,那么需要声明语言:

    "contributes": {
+        "languages": [
+            {
+                "id": "vue",
+                "aliases": [
+                    "Vue",
+                    "vue"
+                ],
+                "extensions": [
+                    ".vue"
+                ]
+            }
+        ],
    }

注意,这里的语言声明仅仅注册了语言,未包含其他配置,对于 vue 的完整配置,请参考 vuejs/language-tools

F5 或者点击 Launch Client 启动调试,vscode 会打开一个新窗口。我们打开一个 vue 项目,进入某个 vue 文件,此时,插件启动,找到 Output / 输出 面板,从下拉框中选择 Language Server Example,此面板会打印日志。

我们关闭此窗口,进入 extension.ts 文件,修改扩展 id 和名称:

    client = new LanguageClient(
-        'languageServerExample',
-        'Language Server Example',
+        'vue-lsp-extension',
+        'Vue LSP Extension',
        serverOptions,
        clientOptions
    );

重新启动调试,会发现插件名称变成了 Vue LSP Extension

vue-lsp-extension

打印日志

服务端打印日志使用 connection.console,例如:

connection.onInitialized(() => {
    if (hasConfigurationCapability) {
        // Register for all configuration changes.
        connection.client.register(DidChangeConfigurationNotification.type, undefined);
    }
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
            connection.console.log('Workspace folder change event received.');
        });
    }
+    connection.console.log("test log");
});

重启插件,打开日志面板:

console-log

我们可以看到成功输出日志。

语言功能

server.ts 中已经存在 Completion 的示例,我们参考 Hover 再增加一个 Hover 的功能。

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

    const result: InitializeResult = {
        capabilities: {
            textDocumentSync: TextDocumentSyncKind.Incremental,
            // Tell the client that this server supports code completion.
            completionProvider: {
                resolveProvider: true
+                hoverProvider: true
            }
        }
    };

然后,处理 textDocument/hover 请求,在 connection.onCompletionResolve 之后,增加:

connection.onHover((item) => {
    return {
        contents: "hover result"
    };
});

然后重启即可。

插件配置

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

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

拉取模式

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

connection.onInitialized(() => {
    if (hasConfigurationCapability) {
        // Register for all configuration changes.
-        connection.client.register(DidChangeConfigurationNotification.type, undefined);
    }
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
            connection.console.log('Workspace folder change event received.');
        });
    }
+    connection.sendRequest("workspace/configuration", { items: [{ section: "languageServerExample.maxNumberOfProblems" }]}).then(result => {
+        connection.console.log(JSON.stringify(result));
+    });
});

推送模式

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

  1. 注册配置变更请求 connection.client.register(DidChangeConfigurationNotification.type, undefined),如下:
connection.onInitialized(() => {
    if (hasConfigurationCapability) {
        // Register for all configuration changes.
        connection.client.register(DidChangeConfigurationNotification.type, undefined);
    }
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
            connection.console.log('Workspace folder change event received.');
        });
    }
});
  1. 初始化或配置更改会被 connection.onDidChangeConfiguration 的回调函数处理,在此处接收并缓存定义的配置。