创建VS Code 插件

VS Code提供了强大的扩展功能,我们可以通过开发插件实现自己的业务模型编辑器。这里我们快速介绍一下插件的创建、开发和发布过程。

最好的方法是跟着官网进行学习:Extension API | Visual Studio Code Extension API
可以少走一些弯路。

使用模板创建插件仍然是 yo code,注意选择npm作为包管理工具,因为在打包时vsce缺省使用npm,如果使用其它包管理工具比如pnpm,可能会有一些麻烦。

创建插件开发模板

首先需要确认系统中安装了node.js,并且可以使用npm安装程序包。然后,安装插件的开发模板生成器:

1
npm install -g yo generator-code

安装完成后,使用模板创建第一个扩展项目,我们为这个项目创建一个子目录,然后进入命令行,在这个子目录下执行:

1
yo code

模板生成程序运行:

生成完成后,在命令行运行:

1
code .

这个项目在vs code 中打开了:

插件运行和调试

我们打开extension.js文件,可以看到插件启动的代码,我们对代码进行一点修改:

将里面的提示修改为我们需要的信息。然后按F5运行。这时,一个新的Vs Code界面启动了,在这个新界面中按Ctrl+Shift+P,打开命令窗口,输入hello world,在界面下方出现我们编辑的信息:

说明这个插件已经可以运行了。

插件打包

上一讲我们使用模板开发了一个最简单的插件,现在我们看如何将这个插件打包,在其它机器上安装使用。Vs Code的插件可以同时创建vsix文件发布,也可以发布到应用商店,通过插件管理器进行安装。我们这里只介绍第一种方式。

首先需要安装插件打包工具vsce:

1
npm i vsce -g

然后,我们还需要在package.json中增加publisher的信息:

1
"publisher": "zhenlei",

如果不增加这个信息,会出现错误。
然后还要修改打包工具创建的Readme.md文件,如果不修改也会出现错误。
现在我们可以打包了,在命令行中,进入项目文件夹,运行:

1
vsce package

这时会提问,缺少respository,这是一个警告,我们可以忽略,继续执行,安装包就创建完成了。

扩展插件的安装和卸载

可以在vs code的扩展管理器中安装打包好的扩展插件,选择从VSIX安装:

也可以在扩展管理器中禁用或卸载安装好的插件:

创建一个实用插件

现在我们创建一个实用的插件,这个插件使用XLST模板将XML文件转换为另一种格式。转换功能使用开源的组件xslt-processor完成,插件本身功能很简单:打开xlst文件,转换当前的xml,将结果显示在新的窗口。

首先使用模板创建项目:

1
yo code

输入这个项目的名字zlxslt,这个项目我们使用yarn作为包管理器。项目创建完成后,使用

1
code .

在VS Code中打开项目。
现在需要引入xslt-processor,在终端中输入:

1
yarn add xslt-processor

这个命令会在项目中安装xslt-processor并更新项目中的package.json和yarn.lock。
在src目录中增加文件schema.d.ts,增加声明语句:

1
declare module 'xslt-processor';

修改package.json,去掉缺省创建的命令,增加新的命令:

1
2
3
"activationEvents": [
"onCommand:zlxslt.runMyXSLT"
],

修改extension.ts:

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
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import * as fs from 'fs';
import { xmlParse, xsltProcess } from 'xslt-processor';
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "zlxslt" is now active!');

const mydisposable: vscode.Disposable = vscode.commands.registerCommand('zlxslt.runMyXSLT', async (): Promise<any> => {
const xsltFile = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
filters: {
'XSLT' : ['xsl','xslt']
}
}
);
if(vscode.window.activeTextEditor !== undefined && xsltFile !== undefined) {
const xml: string = vscode.window.activeTextEditor.document.getText();
const xslt: string = fs.readFileSync(xsltFile[0].fsPath).toString();
try {
const rXml = xmlParse(xml);
const rXslt = xmlParse(xslt);
const result = xsltProcess(rXml, rXslt);
const textDoc = await vscode.workspace.openTextDocument(
{
content: result,
language: 'xml'
}
);

vscode.window.showTextDocument(textDoc, vscode.ViewColumn.Beside);


}
catch(e) {
vscode.window.showErrorMessage(e);
}
}
else {
vscode.window.showErrorMessage('An error occurred while accessing the XML and/or XSLT source files. Please be sure the active window is XML, and you have selected an appropriate XSLT file.');
}
});

context.subscriptions.push(mydisposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}

启动调试,会打开新的窗口,打开一个xml文件,然后按Ctrl+Shift+p打开命令窗口,选择“Run My XSLT”,这时会弹出文件选择窗口,选择xslt文件,转换后的xml会显示在旁边的窗口。

打开外部网站

插件中需要打开外部网站,可以使用下面的代码:

1
vscode.env.openExternal(vscode.Uri.parse('http://localhost:8800));

上面的代码可以打开浏览器进行访问。如果是希望在VSCode中显示编辑完成的html,可以使用Webview作为子页面打开,但这种方式不支持打开外部网页,有使用iframe嵌入打开的,经过测试不可行,因为有安全限制。另外在内嵌的iframe中无法使用F12开发者调试工具。

WebView

VSCode的WebView用于显示HTML,第一步先创建一个Web View Panel,代码如下:

1
2
3
4
5
6
7
8
9
10
const panel = vscode.window.createWebviewPanel(
// 该webview的标识,任意字符串
'catCoding',
// webview面板的标题,会展示给用户
'Cat Coding',
// webview面板所在的分栏
vscode.ViewColumn.One,
// 其它webview选项
{}
);

然后在panel.webview.html中设置需要显示的html文本就可以了。这种方式可以用来预览自己编写的html,但有很多限制,这些限制主要出于安全的考虑,所以,如果希望调试高级的功能,就不适合使用web view方式。

TreeView

使用TreeView可以在左边显示需要的树形结构,比如,显示远程服务器中的文档结构。这需要进行几个步骤,这里先说第一步,需要创建一个实现vscode.TreeDataProvider接口的类,在这个类中,实现获取树节点和子节点的方法。节点的类型可以自己定义,继承vscode.TreeItem,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class TempNode extends vscode.TreeItem {
constructor(
public readonly label: string,
public children: any,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.tooltip = `${this.label}`;
this.description = this.label;
}
}

TreeDataProvider

TreeDataProvider负责显示TreeView中的节点,在代码中,可以在构造函数中,传入需要显示的数据,比如:

1
constructor(private temps: any) { }

然后实现getTreeItem,这里只要简单返回就行:

1
2
3
getTreeItem(element: TempNode): vscode.TreeItem {
return element;
}

最主要的是需要实现getChildren,这里有几种情况,首先是没有element传入,这时返回的是根节点集合,然后是有element传入,需要根据返回的节点是否有子节点,决定节点的展开和关闭图标,示例代码如下:

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
getChildren(element?: TempNode): vscode.ProviderResult<TempNode[]> {
if (element) {
let res=[];
for(var i=0;i<element.children.length;i++){
let temp=element.children[i];
if(temp.isFolder){ //有子节点
res.push(new TempNode(temp.name,temp.temps,vscode.TreeItemCollapsibleState.Collapsed));
}else{ //没有子节点
res.push(new TempNode(temp.name,temp.temps,vscode.TreeItemCollapsibleState.None));
}
}
return Promise.resolve(res);
}else{
//根节点
let res=[];
for(var i=0;i<this.temps.length;i++){
let temp=this.temps[i];
if(temp.isFolder){
res.push(new TempNode(temp.name,temp.temps,vscode.TreeItemCollapsibleState.Collapsed));
}else{
res.push(new TempNode(temp.name,temp.temps,vscode.TreeItemCollapsibleState.None));
}

}
return Promise.resolve(res);
}
}

注册TreeView

需要将开发完成的Provider进行注册才可以显示,需要在package.json中注册几个地方,在”contributes”下进行注册。首先,需要将provider与view进行关联:

“views”: {
“template-explorer”: [
{
“id”: “templateTree”,
“name”: “模板树”,
“icon”: “media/dep.svg”,
“contextualTitle”: “Template Explorer”
}
]
}
上面的代码注册了一个view,”template-explorer”,关联的provider是“templateTree”。
然后,还需要注册一个左侧的view container,用来打开treeview:
“viewsContainers”: {
“activitybar”: [
{
“id”: “template-explorer”,
“title”: “Template Explorer”,
“icon”: “media/dep.svg”
}
]
},
最后,需要在extension代码中增加注册provider的代码:

1
2
3
const templateTreeProvider = new TemplateTreeProvider(res.data.sort(by("name")));

vscode.window.registerTreeDataProvider('templateTree', templateTreeProvider);

自定义设置

当扩展中涉及到网络访问等功能时,网络地址需要可以进行设置,这时,需要使用VS Code的自定义设置。在package.json中定义conguration的contributes,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"configuration": {
"title":"TemplateUplader",
"properties": {
"templateuploader.remoteViewUrl":{
"type":"string",
"default":"http://localhost:8800/",
"description": "远程预览Url"
},
"templateuploader.remoteApiUrl":{
"type":"string",
"default":"http://localhost:6009/",
"description": "远程模板库Url"
}
}
}

其中templateuploader.remoteViewUrl和templateuploader.remoteApiUrl就是新增的设置项。在扩展代码中,可以读取这些设置项,示例代码如下:

1
2
3
let config=vscode.workspace.getConfiguration('templateuploader');
let remoteApiUrl=config.get("remoteApiUrl");
let remoteViewUrl=config.get("remoteViewUrl");

如果需要修改设置,可以在”文件-首选项-设置“中进行搜索templateuploader,然后进行修改。

添加TreeView刷新按钮

在TemplateTreeProvider中增加刷新事件的定义,代码如下:

1
2
3
private _onDidChangeTreeData: vscode.EventEmitter<TempNode | undefined | null | void> = new vscode.EventEmitter<TempNode | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<TempNode | undefined | null | void> = this._onDidChangeTreeData.event;

然后增加刷新代码:

1
2
3
4
5
async refresh(): Promise<void> {
await this.getData();
this._onDidChangeTreeData.fire();
}

需要使用一个命令执行refresh:

1
2
3
vscode.commands.registerCommand('tempuploader.refreshTemplates', () =>
templateTreeProvider.refresh()
);

还需要将这个命令进行注册:
{
“command”: “tempuploader.refreshTemplates”,
“title”: “Refresh”,
“icon”: {
“light”: “resources/light/refresh.svg”,
“dark”: “resources/dark/refresh.svg”
}
},
然后,在TreeView的顶部导航菜单部分,增加这个命令的图标:
“menus”: {
“view/title”: [
{
“command”: “tempuploader.refreshTemplates”,
“when”: “view == templateTree”,
“group”: “navigation”
}
],
当view是templateTree时,显示command,显示的位置为navigation。
这样就完成了刷新按钮的添加。

为TreeView选择项增加编辑功能

当选择TreeView中的节点时,通常需要对节点进行某种操作,比如删除、编辑等。这时,需要为选择项关联一个命令。
首先,定义一个Command来响应编辑事件:
{
“command”: “tempuploader.editTemplate”,
“title”: “Edit”,
“icon”: {
“light”: “resources/light/edit.svg”,
“dark”: “resources/dark/edit.svg”
}
}
然后,将这个命令与选择的节点相关联,在menus中定义:
“view/item/context”: [
{
“command”: “tempuploader.editTemplate”,
“when”: “view == templateTree && viewItem == tempNode”,
“group”: “inline”
}
]
这里需要注意viewItem == tempNode,说明选中的节点contextvalue是tempNode,这个定义在节点的Class中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class TempNode extends TempNodeBasic {
constructor(
public readonly label: string,
public readonly tempName:string,
public readonly isFolder:boolean,
public children: any,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(label, tempName,isFolder,children, collapsibleState);
this.tooltip = `${this.label}`;
this.description = this.label;
}

contextValue = 'tempNode';
}

然后,在extensions中注册命令:

1
2
3
vscode.commands.registerCommand('tempuploader.editTemplate', (tempNode: TempNode) => {
vscode.window.showInformationMessage("编辑模板:" + tempNode.label);
}

打开编辑器

可以在VS Code中使用扩展打开编辑器,首先创建一个文本文档,并设置内容和语言:
const textDoc = await vscode.workspace.openTextDocument(
{
content: JSON.stringify(res.data),
language: ‘json’
}
);
然后,可以显示这个文档:
vscode.window.showTextDocument(textDoc, vscode.ViewColumn.Beside);
显示时,可以定义文档的显示位置,Beside是在旁边显示,也可以是Active,就是在活动部分显示,还可以是One到Nine。

图标

VS Code有丰富的图标库,在扩展中可以使用这些图标,可以从github找到这些图标的定义并下载,网址是microsoft/vscode-icons: Icons for Visual Studio Code (github.com)
一般情况下,不需要下载图标,可以使用类似”icon”: “$(preview)”的定义方式,如果使用本地文件,需要分别定义在light和dark模式下的图标:
“icon”: {
“light”: “resources/light/file-code.svg”,
“dark”: “resources/dark/file-code.svg”
}

输入框

VSCode需要使用内置的输入框作为输入方式,具体的语法如下:
let tempName=await vscode.window.showInputBox(
{ // 这个对象中所有参数都是可选参数
password:false, // 输入内容是否是密码
ignoreFocusOut:true, // 默认false,设置为true时鼠标点击别的地方输入框不会消失
placeHolder:’请输入模板名称’, // 在输入框内的提示信息
prompt:’输入完成后,会创建相应的目录、html和json文件’, // 在输入框下方的提示信息
//validateInput:function(text){return text;} // 对输入内容进行验证并返回
});