使用 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.ts
和 server/src/server.ts
,它们分别是语言服务器的客户端和服务端。客户端使用了 vscode-languageclient
库,服务端使用了 vscode-languageserver
库。
活动事件
插件需要先在根目录的 package.json
文件的扩展字段 activationEvents
中声明激活的条件,只有满足激活条件,插件才会开始工作。下面是所有可选的条件:
- onLanguage
- onCommand
- onDebug
- onDebugInitialConfigurations
- onDebugResolve
- workspaceContains
- onFileSystem
- onView
- onUri
- onWebviewPanel
- onCustomEditor
- onAuthenticationRequest
- onStartupFinished
- *
对于此项目,我们使用 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
相关的语言服务器插件,那么名为 vue
的 language
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
。
打印日志
服务端打印日志使用 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");
});
重启插件,打开日志面板:
我们可以看到成功输出日志。
语言功能
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));
+ });
});
推送模式
推送模式是服务器使用注册模式注册空配置更改,客户端通过事件发出配置更改信号。
- 注册配置变更请求
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.');
});
}
});
- 初始化或配置更改会被
connection.onDidChangeConfiguration
的回调函数处理,在此处接收并缓存定义的配置。