VsCode 插件语言插件开发

最近对如何开发一个VsCode的插件比较感兴趣,于是特意去读了一些插件的开发文档,主要看的是一个中文翻译的文档,不过这个文档并不全,基本属于重点功能的罗列,但是用来入门是挺好的,不过如果想要深入,还是要去看英文的文档

大部分内容都比较简单,主要就是配置,就可以生效,我这次主要想总结的是如何开发一个全新的语言插件,也就是说,如果你想定义一门新类似JSX的语言,如果提供VsCode的支持,比如语法高亮,代码片段,标签自闭和,错误提示,自动补全等等。

插件目录结构

1
2
3
4
5
6
7
8
9
├── .vscode
│ ├── launch.json // 插件加载和调试的配置
│ └── tasks.json // 配置TypeScript编译任务
├── .gitignore // 忽略构建输出和node_modules文件
├── README.md // 一个友好的插件文档
├── src
│ └── extension.ts // 插件源代码
├── package.json // 插件配置清单
├── tsconfig.json // TypeScript配置

插件清单

每个VS Code插件都必须包含一个package.json,它就是插件的配置清单package.json混合了Node.js字段,如:scriptsdependencies,还加入了一些VS Code独有的字段,如:publisheractivationEventscontributes等。关于这些VS Code字段说明都在插件清单参考中可以找到。我们在本节介绍一些非常重要的字段:

  • namepublisher: VS Code 使用<publisher>.<name>作为一个插件的ID。你可以这么理解,Hello World 例子的 ID 就是vscode-samples.helloworld-sample。VS Code 使用 ID 来区分各个不同的插件。

  • main: 插件的主入口。

  • activationEventscontributes: 激活事件 and 发布内容配置

  • engines.vscode: 描述了这个插件依赖的最低VS Code API版本。

  • postinstall 脚本: 如果你的engines.vscode声明的是1.25版的VS Code API,那它就会按照这个声明去安装目标版本。一旦vscode.d.ts文件存在于node_modules/vscode/vscode.d.ts,IntelliSense就会开始运作,你就可以对所有VS Code API进行定义跳转或者语法检查了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"name": "helloworld-sample",
"displayName": "helloworld-sample",
"description": "HelloWorld example for VS Code",
"version": "0.0.1",
"publisher": "vscode-samples",
"repository": "https://github.com/Microsoft/vscode-extension-samples/helloworld-sample",
"engines": {
"vscode": "^1.25.0"
},
"categories": ["Other"],
"activationEvents": ["onCommand:extension.helloWorld"],//激活事件,也就是什么事件会激活这个插件,这里个例子里就是当发生extension.helloWorld这个命令时会激活这个插件
"main": "./out/extension.js",//插件的入口文件
"contributes": {//插件的发布配置,非常重要,注册事件,文件图标,标签自闭合等等绝大部分插件的配置都在这里
"commands": [//注册命令
{
"command": "extension.helloWorld",//这就是上面那个activationEvent里面的那个命令,也就是我们comman+shift+P时候中选择运行的命令
"title": "Hello World"//命令列表里显示的内容
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"devDependencies": {
"@types/node": "^8.10.25",
"tslint": "^5.11.0",
"typescript": "^2.6.1",
"vscode": "^1.1.22"
}
}

入口文件(package.json中的main)

插件入口文件会导出两个函数,activatedeactivate,你注册的激活事件被触发之时执行activatedeactivate则提供了插件关闭前执行清理工作的机会。

vscode模块包含了一个位于node ./node_modules/vscode/bin/install的脚本,这个脚本会拉取package.jsonengines.vscode字段定义的VS Code API。这个脚本执行过后,你就得到了智能代码提示,定义跳转等TS特性了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 'vscode'模块包含了VS Code extensibility API
// 按下述方式导入这个模块
import * as vscode from 'vscode';
// 一旦你的插件激活,vscode会立刻调用下述方法
export function activate(context: vscode.ExtensionContext) {

// 用console输出诊断信息(console.log)和错误(console.error)
// 下面的代码只会在你的插件激活时执行一次
console.log('Congratulations, your extension "my-first-extension" is now active!');

// 入口命令已经在package.json文件中定义好了,现在调用registerCommand方法
// registerCommand中的参数必须与package.json中的command保持一致
let disposable = vscode.commands.registerCommand('extension.sayHello', () => {
// 把你的代码写在这里,每次命令执行时都会调用这里的代码
// ...
// 给用户显示一个消息提示
vscode.window.showInformationMessage('Hello World!');
});

context.subscriptions.push(disposable);
}

发布内容配置

插件可以做什么

常用功能

  • 注册命令、配置、快捷键绑定、菜单等。

  • 保存工作区或全局数据。

  • 显示通知信息。

  • 使用快速选择获得用户输入。

  • 打开系统的文件选择工具,以便用户选择文件或文件夹。

  • 使用进度API提示耗时较长的操作。

命令

  • 使用pacakge.json中的contributes.commands注册命令,只有在这里注册的命令才可以在命令面板中搜索到。
  • 默认情况下,所有命令面板中出现的命令都可以在package.jsoncommands部分中配置。不过,有些命令是场景相关的,比如在特定的语言的编辑器中,或者只有用户设置了某些选项时才展示。Visual Studio Code Key Bindings:可以查看如何写这个when命令
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"contributes": {
"menus": {
"commandPalette": [
{
"command": "myExtension.sayHello",
"when": "editorLangId == markdown"
}
]
}
}
}
//现在myExtension.sayHello命令只会出现在用户的Markdown文件中了。
  • 使用vscode.command.registerCommand为命令注册回调函数
  • 使用vscode.command.excuteCommand执行命令

vscode也有很多内置的命令(VS Code内置命令清单

我们看个例子🌰:editor.action.addCommentLine命令可以将当前选中行变成注释(你可以偷偷把这个功能地集成到你自己的插件中哦):

1
2
3
4
import * as vscode from 'vscode';
function commentLine() {
vscode.commands.executeCommand('editor.action.addCommentLine');
}

配置

插件需要在contributes.configuration发布内容配置点发布内容配置点package.json的一部分,用于配置插件启动命令、用户可更改的插件配置,可以理解为插件的主要配置文件。中填写有关的配置,你可以workspace.getConfigurationAPI中阅读有关内容。

键位绑定

插件可以添加自定义键位映射,在contributes.keybindings键位绑定中了解更多有关内容。

菜单

插件可以自定义上下文菜单项,菜单会根据用户右击VS Code UI的不同位置而各不相同。查看更多contributes.menus发布内容配置。

数据存储

VS Code中有三种数据储存方式:

  • ExtensionContext.workspaceState:键值对组成的工作区数据。当同一个工作区再次打开时会重新取出数据。

  • ExtensionContext.globalState:键值对组成的全局数据。当插件激活时会再次取出这些数据。

  • ExtensionContext.storagePath:指向你的插件可以读写的本地文件夹的路径。如果你要储存比较大的数据,这是一个非常好的选择。

  • ExtensionContext.globalStoragePath:指向你的插件可以读写的本地存储的路径。如果你要存储所有工作区内的大文件,这是一个非常好的选择。

通知

几乎所有的插件都需要在某些时候为用户提示信息。VS Code提供了3个API来展示不同重要程度的信息:

  • window.showInformationMessage

  • window.showWarningMessage

  • window.showErrorMessage

主题

产品图标主题和色彩主题可以参考文档:VS Code插件创作中文开发文档 - 色彩主题

文件图标主题

VS Code的UI在文件名称左边显示图标,插件配置的图标系列可以让用户自由选择他们喜爱的图标。

但是Vscode目前不支持某个语言插件只定义自己语言文件的图标,如果用户切换到你的插件提供的主题,却不能支持所有文件类型,就会出现问题

首先,创建一个VS Code插件,然后把iconTheme配置点(contribution point)添加进去

1
2
3
4
5
6
7
8
9
"contributes": {
"iconThemes": [
{
"id": "turtles",
"label": "Turtles",
"path": "./fileicons/turtles-icon-theme.json"
}
]
}

复制到剪贴板复制错误已复制

  • id作为这个图标主题的标识,目前只做内部使用,未来可能会用在设置里面,所以最好设置一个可读性强的唯一值。

  • label会显示在主题选择下拉框中。

  • path指示了图标集所在的位置。如果你的图标系列名称遵循*icon-theme.json命名规范,那么VS Code就能提供完整的支持。

1
2
3
4
5
6
7
//turtles-icon-theme.json{
"iconDefinitions": {
"_folder_dark": {
"iconPath": "./images/Folder_16x_inverse.svg"
}
}
}

这里,图标定义包含了一个标识符_folder_dark。除此之外还支持以下属性:

  • iconPath:当使用svg/png文件时:指向图片的路径。

  • fontCharacter:当使用glyph字体时:字体中使用的字符。

  • fontColor:当使用glyph字体时:设置glyph的颜色。

  • fontSize:当使用字体时:设置字体大小。默认情况下会使用字体本身定义的字体大小。这个值应为父级字号的相对值(如 150%)。

  • fontId:当使用字体时: 字体的ID。如果没有指定,则会采用font specification部分的第一个字体。

在iconDefinitions中定义之后,就可以引用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"iconDefinitions": {
"_folder_dark": {
"iconPath": "./images/Folder_16x_inverse.svg"
}
},
"file": "_file_dark",
"folder": "_folder_dark",
"folderExpanded": "_folder_open_dark",
"folderNames": {
".vscode": "_vscode_folder",
},
"fileExtensions": {
"ini": "_ini_file",
},
"fileNames": {
"win.ini": "_win_ini_file",
},
"languageIds": {
"ini": "_ini_file"
},
"light": {
"folderExpanded": "_folder_open_light",
"folder": "_folder_light",
"file": "_file_light",
"fileExtensions": {
"ini": "_ini_file_light",
}
},
"highContrast": {
}
}
  • file是一个默认文件图标,为那些没有匹配到任何插件、文件名、语言类型的文件所准备的。目前所有文件图标属性都会被继承(只适用于:glyphs字体、字体大小(fontSize))。

  • folder收起的文件夹图标,如果folderExpanded没有设置,那么展开的文件夹也会使用这个图标。使用folderNames关联特殊名称的文件夹。文件夹图标是可选的,如果不设置,那文件夹就不会显示任何图标。

  • folderExpanded展开的文件夹图标。这个图标是可选的,如果不设置就会使用folder定义好的图标。

  • folderNames特殊名称文件夹图标。这个键是用于文件夹名称的,不支持包含路径的名称,不支持匹配模式和通配符。大小写不敏感。

  • folderNamesExpanded展开的特殊名称文件夹图标。

  • rootFolder 收起的工作区根文件夹图标,如果rootFolderExpanded没有设置,那么展开的工作区根文件夹也会使用这个图标。如果不设置,则会使用folder定义的文件夹图标。

  • rootFolderExpanded 展开的工作区根文件夹图标。如果没有设置,则会使用rootFolder定义的文件夹图标。

  • languageIds语言类型图标。这个键将匹配在*语言配置点(contribution point)*配置的语言id。注意语言配置的’第一行’是不考虑在内的。

  • fileExtensions文件插件图标。根据文件插件的名称匹配。插件名称是文件名点号后面(不包含点号)。拥有多重点号的文件名称,如lib.d.ts会匹配多个模式——d.tsts。大小写敏感。

  • fileNames文件图标。这个键需要文件的全称进行匹配,不支持包含路径的名称,不支持模式和通配符。大小写敏感。fileNames是最高优先匹配。

匹配优先级:fileNames > fileExtensions > languageIds

lighthighContrast部分的属性表和上面相同,只是会在对应的主题下覆盖原有图标配置。

语言特性

声明式语言特性

声明式语言特性添加了基础的编程语言编辑支持,如括号匹配、自动缩进和语法高亮。这些功能都可以通过声明配置而不用写任何代码就可以获得,更高级的语言特性如IntelliSense或调试,请看编程式添加语言特性

  • 将常用的JS代码片段打包到插件中

  • 为VS Code添加新的语言支持

  • 为一门语言添加或替换语法

  • 通过注入的方式,扩展一门语法

  • 将现有的 TextMate 语法迁移到VS Code中

代码片段

VS Code插件创作中文开发文档 - 代码片段

语言配置

通过contributes.languages发布内容配置,你可以配置以下声明式语言特性

  • 启用/关闭注释

  • 定义括号

  • 自动闭合符号

  • 自动环绕符号

  • 代码折叠

  • 单词匹配

  • 缩进规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"name": "language-configuration-sample",
"displayName": "Language Configuration Sample",
"description": "Language Configuration Sample",
"version": "0.0.1",
"publisher": "vscode-samples",
"engines": {
"vscode": "^1.28.0"
},
"categories": [
"Programming Languages"
],
"contributes": {
"languages": [ // 定义新的语言
{
"id": "vnm", // 语言的id
"extensions": [
".js"
],
"aliases": [
"js",
"JavaScript"
],
"configuration": "./language-configuration.json" // 该语言的配置
}
]
}
}
{
"comments": { // 定义注释方式
"lineComment": "//",
"blockComment": ["/*", "*/"]
},
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]], //定义括号,当鼠标移动到某个括号上时,会自动高亮对应的括号
"autoClosingPairs": [//自动闭合括号
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"autoCloseBefore": ";:.,=}])>` \n\t",//默认情况下只有在右侧有空白时才会闭合标签,可以通过这个配置覆盖默认行为
"surroundingPairs": [ // 自动环绕括号
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"]
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
"indentationRules": {
"increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$",
"decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$"
}
}

编程式语言特性(监听用户动作)

VS Code插件创作中文开发文档 - 程序性语言特性

下面这表对应的是我们可以实现的编程性语言特性,每个特性都可以单独通过左边一列的VScode API来实现,同时可以单独通过Language Server Protocol在语言服务器中实现。

理论上所有通过语言服务器实现的特性都可以单独用客户端实现

Unable to copy while content loads

编程式添加语言特性可以为编程语言添加更为丰富的特性,如:悬停提示、转跳定义、错误诊断、IntelliSense和CodeLens。这些语言特性暴露于vscode.languages.*API。语言插件可以直接使用这些API,或是自己写一个语言服务器,通过语言服务器库将它适配到VS Code。

虽然我们提供了一个语言特性列表,但是并不阻碍你发挥想象,自由使用这些API。比方说,在行内显示额外信息,使用CodeLens和代码悬停是非常好的方式,而错误诊断可以高亮拼写或代码风格错误。

  • 鼠标悬停于API上时, 出现用法示例

  • 使用诊断,报告代码风格错误

  • 注册新的HTML代码格式化

  • 提供丰富的IntelliSense中间件

  • 为一门语言添加代码折叠、面包屑、轮廓支持

以自动补全为例子说明用法

客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GoCompletionItemProvider implements vscode.CompletionItemProvider {
public provideCompletionItems(
document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken):
Thenable<vscode.CompletionItem[]> {
...
}
}
export function activate(ctx: vscode.ExtensionContext): void {
...
//registerCompletionItemProvider返回的是解除本身注册的函数,这里猜测添加订阅的一个目的是为了以后的unregister
ctx.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
GO_MODE, new GoCompletionItemProvider(), '.', '\"'));
...
}

LSP实现

在接收响应的initialize方法中,你的语言服务器需要声明它是否能提供补全,以及它是否支持动态计算补全项的completionItem\resolve方法。

1
2
3
4
5
6
7
8
9
10
{
...
"capabilities" : {
"completionProvider" : {
"resolveProvider": "true",
"triggerCharacters": [ '.' ]
}
...
}
}

返回了这个配置,客户端才会在输入完成后发送textDocument/completion事件给语言服务器,语言服务器通过onCompletion事件监听这个事件,监听后返回匹配的列表。

当我们具体选择了某一个补全项之后,会发送completionItem/resolve事件,该事件被onCompletionResolve事件监听,可以补全具体的信息,或者进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//server.ts
connection.onCompletion(
({ textDocument, position, context }: CompletionParams): CompletionItem[] => {
const doc = documentService.getDocument(textDocument.uri)!;

//返回类型为CompletionItem[]
const completionItems = doComplete(doc, position);

return completionItems || [];
}
);

// 这个函数为补全列表的选中项提供了更多信息
connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
if (item.label === '$store') {
item.insertText = 'store'
}
return item;
});

监听文档的变化

上面的编程式语言特性主要做的当我们进行了某些动作之后,客户端本身监听到或者客户端将监听到的动作与服务器之间利用事件进行通信。

还有一种方式,是文本本身发生变化时通知客户端或者告诉语言服务器文本的内容

客户端本身监听到的内容改变,可以在vscode.workspace.*中找到对应的api,如vscode.workspace.onDidChangeTextDocument

LSP则稍微复杂一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//DocumentService.ts
import { Connection, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';

/**
* Service responsible for managing documents being syned through LSP
*/
export class DocumentService {
private documents: TextDocuments<TextDocument>;

constructor(conn: Connection) {
this.documents = new TextDocuments(TextDocument);
this.documents.listen(conn);
}

getDocument(uri: string) {
return this.documents.get(uri);
}

getAllDocuments() {
return this.documents.all();
}

get onDidChangeContent() {
return this.documents.onDidChangeContent;
}
get onDidClose() {
return this.documents.onDidClose;
}
}
//server.ts
documentService.onDidChangeContent((change) => {
const textDocument = change.document;
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: validateTextDocument(textDocument) });
});

这种方式每次都是发送全量文本,对性能会有影响,所以vscode又提供了增量文本更新同步