自定义命令
命令可以出发 VSCode 的操作。通过命令,插件可以实现如下所示的功能
- 把插件的功能开放给用户使用
- 把插件的功能开放给其他插件使用
- 把操作绑定到 VSCode 的用户界面(如快捷菜单)
- 实现内部逻辑
使用 Command
VSCode 包含大量的内置命令,用于与编辑器交互、控制用户界面、执行后台操作等。
- 执行 Command
通过 vscode.commands.executeCommand API 传入 VSCode 或其他插件命令 ID,可以直接执行此 ID 对应的命令。 在下面的例子中,调用 editor.addCommandLine 命令可以对选中的文本添加评论 - 此外一些命令需要传入参数,一些命令还会返回结果;
例如,vscode.openFolder API 用于打开一个文件夹,其中需要传入文件夹路径的 URI,并返回是否成功打开; - Command URI
命令的 URI 是用于执行命令的链接,是可单击的链接,可以出现在以下 3 个地方
csharp
function commentLine() {
vscode.commands.executeCommand('editor.action.addCommentLine');
}
ini
let uri = Uri.file('/some/path/to/folder')
let sucdess = awati commands.executeCommand('vscode.openFolder', uri)
- 悬停信息的文本
- 自动补全项的详情区域
- Webview(网页视图)
命令的 URI 使用 command:格式。例如:editor.action.addCommentLine 命令的 URI 为 command:editor.action.addCommentLine
下面的例子会在悬停界面添加一个 Add comment 的链接
ini
class MyHoverProvider implements vscode.HoverProvider{
provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
const commentCommandUri = vscode.Uri.parse('command:editor.action.addCommentLine');
const contents = new vscode.MarkdownString(`[Add comment](${commentCommandUri})`);
contents.isTrusted = true;
return new vscode.Hover(contents);
}
}
vscode.languages.registerHoverProvider('javascript', new MyHoverProvider());
对于要传参的命令,需要把参数列表以 JSON 数组格式传入,并进行 URI 编码。
下面例子使用 git.stage 命令在悬停界面添加一个 Stage file 的链接
ini
class MyHoverProvider implements vscode.HoverProvider{
provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
const args = [{resourceUri: document.uri}];
const stageCommandUri = vscode.Uri.parse('command:git.stage?${encodeURIComponent(JSON.stringify(args))}');
const contents = new vscode.MarkdownString(`[Stage file](${stageCommandUri})`);
contents.isTrusted = true;
return new vscode.Hover(contents);
}
}
- 内置的 Command
通过 Ctrl + Shift + P 快捷键打开命令面板,然后输入并执行 Preferences:Open Default Keyboard Shortcuts(JSON)命令,可以打开 keybingdings.json 文件查看完整的命令列表及命令 ID。 此外,我们可以通过快捷键编辑器来获取每一个命令 ID 可以使用一下菜单来打开快捷键编辑器,不同系统下所使用的菜单分别如下所示:
- Windows/Linux: Fiel -> Preferences -> Keyboard Shortcuts
- maxOS: Code -> Preferences -> Keyboard Shortcuts
创建 Command
除了使用 VSCode 内置或其他插件的命令,插件还可以创建新的命令,供用户或其他插件使用
- 注册 Command
通过 vscode.registerComand API, 可以把命令 ID 绑定到一个函数上,如下所示:
javascript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const command = 'myExtension.sayHello';
const commandHandler = (name?: string = 'world') => {
console.log(`Hello ${name}`);
};
context.subscriptions.push(vscode.commands.registerCommand(command, commandHandler));
}
// 执行myExtension.sayHello命令时就会调用commandHandler函数
- 创建用户可使用的 Command
为了使终端用户可以在命令面板中调用插件提供的命令,在 package.json 文件的 contributes 贡献点属性中需要定义相应的命令,如下所示:
json
{
"contributes": {
"commands": [
{
"command": "myExtension.sayHello",
"title": "Say Hello"
}
]
}
}
通过以上配置,用户可以通过 Ctrl + Shift + P 快捷键打开命令面板, 然后输入并执行 myExtension.sayHello 命令了。
需要注意的是,在命令面板中调用 myExtension.sayHello 命令时,需要确保插件已经激活,可以通过 package.json 文件的 activationEvent 属性设置插件的激活事件,如下所示:
json
{
"activationEvents": ["onCommand:myExtension.sayHello"]
}
- 控制 Command 的显示
默认情况下,所有的命令都会显示在命令面板中,且通过 package.json 文件的 menus.commandPalette 贡献点,可以控制命令的显示条件;
json
// 下面的例子中只有用户打开了一个Markdown文件时,myExtension.sayHello命令才会显示在命令面板中。
{
"contributes": {
"menus": {
"commandPalette": [
{
"command": "myExtension.sayHello",
"when": "editorLangId === markdown"
}
]
}
}
}
子定义树状视图
定义 View Container
在 package.json 文件中,插件可以通过 contributes.viewsContainers 贡献点来定义新的视图容器,如下所示:
json
// 视图容器就是最左侧的图标,比如Explorer Search Folders Source Controll Debug Extenstions等
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "package-explorer",
"title": "Package Explorer",
"icon": "media/dep.svg"
}
]
}
}
定义树状视图
在 package.json 文件中,插件可以通过 contributes.views 贡献点来定义新的树状视图。树状视图可以被定义在以下视图容器中。
- explorer: 文件资源管理器视图容器
- debug: 运行与调试视图容器
- scm: 源代码管理视图容器
- test: 测试资源管理器视图容器
- 插件自定义的视图容器
json
// 该实例是把nodeDependencies视图放置到了自定义的package-explorer视图容器中
"contributes": {
"views": {
"package-explorer": [
{
"id": "nodeDependencies",
"name": "Node Dependencies",
"when": "explorer"
}
]
}
}
需要注意的是,在打开视图时需要确保插件已经激活。可以通过 package.json 文件的 activationEvent 属性设置插件的激活事件 onView:${viewId},如下所示:
json
{
"activationEvents": ["onView:nodeDependencies"]
}
树状视图中的操作
在树状视图中,可以在标题栏或快捷菜单中定义相应的操作入口,如下所示
- view/title: 在视图的标题栏中显示操作入口
- view/item/context: 在树状视图的节点中显示操作入口
json
"contributes": {
"commands": [
{
"command": "nodeDependencies.refreshEntry",
"title": "Refresh",
"icon": {
"light": "resources/light/refresh.svg",
"dark": "resources/dark/refresh.svg"
}
},
{
"command": "nodeDependencies.addEntry",
"title": "Add"
},
{
"command": "nodeDependencies.editEntry",
"title": "Edit",
"icon": {
"light": "resources/light/edit.svg",
"dark": "resources/dark/edit.svg"
}
},
{
"command": "nodeDependencies.deleteEntry",
"title": "Delete"
},
],
"menus": {
"view/title": [
{
"command": "nodeDependencies.refreshEntry",
"when": "view == nodeDependencies",
"group": "navigation"
},
{
"command": "nodeDependencies.addEntry",
"when": "view == nodeDependencies"
}
]
},
"view/item/context": [
{
"command": "nodeDependencies.editEntry",
"when": "view == nodeDependencies && viewItem == dependency",
"group": "inline"
},
{
"command": "nodeDependencies.deleteEntry",
"when": "view == nodeDependencies && viewItem == dependency"
}
]
}
TreeDataProvider API
在 package.json 文件中定义好树状视图后,需要在 Typescript/Javascript 文件中通过 TreeDataProvider API 来注册视图,如:
dart
vscode.window.registerTreeDataProvider('nodeDependencies', new DepNodeProvider());
TreeView API
如果你需要在视图中实现更复杂的用户界面操作,那么可以使用 window.creatTreeView API,如:
php
vscode.window.createTreeView('ftpExplorer', {
treeDataProvider: new FtpTreeDataProvider(),
});
自定义网页视图
VSCode 没有向插件开发 DOM 的直接控制权限,插件只能通过 VSCode 开发的 API 来定义用户界面。网页视图为插件开发者提供了一个高度可定制化的界面,我们可以把网页视图当做嵌入在 VSCode 中的 iframe. 在编辑器区域中,开发者可以通过前端技术(HTML/CSS/JavaScript)来构建一个用户界面,灵活性非常大
了解 Webview API
创建网页视图
通过 vscode.window.createWebviewPanel 函数,可以创建网页视图,并在编辑器中显示。
通过 webview.html,可以设置网页视图的 HTML 内容
xml
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', ()=>{
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel('CatCoding', 'Cat Coding', vscode.ViewColumn.One, {});
panel.webview.html = getWebviewContent();
});
);
}
function getWebviewContent(){
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" alt="" width="300">
</body>
</html>
`;
}
通过 Ctrl + Shift + P 快捷键打开命令面板,然后输入 Cat Coding: Start cat coding session 命令,即可在网页视图中显示插件中定义的 HTML 页面
更新网页视图中的 HTML 内容
通过改变 webview.html 中的内容,可以动态更新网页视图的页面。此外,通过 title 属性,可以更新网页视图的标题
ini
// 下面的例子中,每隔1000毫秒就会更新一次网页视图的页面和标题
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', ()=>{
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel('CatCoding', 'Cat Coding', vscode.ViewColumn.One, {});
let iteration = 0;
const updateWebview = ()=>{
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// 设置初始化的内容
updateWebview();
setInterval(updateWebview, 1000);
});
);
}
function getWebviewContent(cat){
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${cats[cat]}" alt="" width="300">
</body>
</html>
`;
}
生命周期
网页视图被关闭后会触发 onDidDispose 事件。 在此事件中,我们可以对网页视图的资源进行清理。此外, 我们也可以主动调用 dispose 函数来关闭网页视图。
javascript
// 下面的例子会在5000毫秒后自动关闭网页视图
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', ()=>{
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel('CatCoding', 'Cat Coding', vscode.ViewColumn.One, {});
panel.webview.html = getWebviewContent();
// 10000毫秒后自动关闭网页视图
const timeout = setTimeout(()=>panel.dispose(), 10000);
panel.onDidDispose(
()=>{
clearTimeout(timeout);
},
null,
context.subscriptions
);
});
);
}
显示
当焦点切换到其他标签页时,网页视图会变成隐藏状态,但是并不会被销毁,通过 reveal 函数可以主动显示网页视图;代码如下:
ini
export function activate(context: vscode.ExtensionContext) {
// 记录当前的网页视图对象
let currentPanel: vscode.WebviewPanel | undefined = undefined;
// 网页视图
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const columnToShownIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (currentPanel) {
currentPanel.reveal(columnToShownIn);
} else {
// 创建并显示网页视图
currentPanel = vscode.window.createWebviewPanel(
'CatCoding',
'Cat Coding',
columnToShownIn,
{}
);
}
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// 设置初始化的内容
updateWebview();
setInterval(updateWebview, 3000);
// 10000毫秒后自动关闭网页视图
const timeout = setTimeout(() => panel.dispose(), 12000);
panel.onDidDispose(
() => {
clearTimeout(timeout);
},
null,
context.subscriptions
);
})
);
}
调试网页视图
通过 Ctrl + Shift + P 快捷键打开命令面板,然后输入并执行 Developer: Open Webview Developer Tools 命令,可以打开开发者工具对网页进行调试
加载本地资源
网页视图运行在隔离的环境中。出于安全性的考虑,默认情况下,网页视图不能访问本地资源。为了加载本地的图片、css 或其他资源,需要使用 webview.asWebviewUri 函数
javascript
import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', ()=>{
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel('CatCoding', 'Cat Coding', vscode.ViewColumn.One, {});
// 获取文件在磁盘上的路径
const onDiskPath = vscode.Uri.file(path.join(context.extensionPath, 'media', 'python.png'));
// 转换成适用于网页视图的URI
const pythonImgSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(pythonImgSrc);
});
);
}
启用 JavaScript
默认情况下,网页视图不能运行 JavaScript 代码。通过 enableScript: true 选项可以启用 JavaScript
xml
import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel(
'CatCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// 在网页视图中启用JavaScript
enableScripts: false,
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<h1 id="root"></h1>
<script>
const rootContainer = document.querySelector('#root');
let count = 0;
setInterval(()=>{
rootContainer.textContent = count++;
}, 1000);
</script>
</body>
</html>
`;
}
消息传递
网页视图与插件是相互独立的运行环境。通过消息传递,两种运行环境可以进行双向通信。
从插件传递数据到网页视图
在插件中,通过 webview.postMessage()函数可以向网页视图(Webview)发送数据。
在网页视图中,通过 message 事件可以监听从插件传过来的数据。
ini
// 下面的例子中,catCoding.doRefactor命令调用了postMessage函数,网页视图通过window.addEventListener('message', event=>{ // ... });来处理传来的数据
function getWebviewContent() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<h1 id="root"></h1>
<script>
const rootContainer = document.querySelector('#root');
let count = 0;
setInterval(()=>{
rootContainer.textContent = count++;
}, 1000);
// 处理接收到的消息
window.addEventListener('message', (event)=>{
const message = event.data;
switch(message.command){
case 'refactor':
count = Math.ceil(count * 0.5);
rootContainer.textContent = count;
break;
}
})
</script>
</body>
</html>
`;
}
export function activate(context: vscode.ExtensionContext) {
// 记录当前的网页视图对象
let currentPanel: any = undefined;
// 网页视图
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
// 创建并显示网页视图
currentPanel = vscode.window.createWebviewPanel(
'CatCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true,
}
);
}
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
undefined,
context.subscriptions
);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// 把JSON数据发送到网页视图
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
从网页视图中将数据传递给插件
从网页视图中,可以通过 acquireVsCodeApi()函数获得 VSCode 的 API 对象,然后使用 vscode.postMessage()函数向插件发送数据。
在插件中,可以通过 webview.onDidReceiveMessage()函数监听从网页视图传来的数据
xml
function getWebviewContent() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<h1 id="root"></h1>
<script>
const rootContainer = document.querySelector('#root');
const vscode = acquireVsCodeApi();
let count = 0;
setInterval(()=>{
rootContainer.textContent = count++;
// 给插件发送数据
if(count > 15){
vscode.postMessage({
command: 'alert',
text: 'on line ' + count
})
}
}, 1000);
</script>
</body>
</html>
`;
}
export function activate(context: vscode.ExtensionContext) {
// 网页视图
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'CatCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true,
}
);
panel.webview.html = getWebviewContent();
// 处理从网页视图传来的消息
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
break;
}
},
undefined,
context.subscriptions
);
})
);
}
持久化
当焦点切换到其他标签页时,网页视图会变成隐藏状态。虽然网页视图没有被销毁,但重新获得焦点后页面会被重新渲染,之前的状态也不复存在。
在创建网页视图时,通过设置 retainContextWhenHidden 选项可以解决这个问题:
xml
function getWebviewContent() {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<h1 id="root"></h1>
<script>
const rootContainer = document.querySelector('#root');
let count = 0;
setInterval(()=>{
rootContainer.textContent = count++;
}, 1000);
</script>
</body>
</html>
`;
}
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// 创建并显示网页视图
const panel = vscode.window.createWebviewPanel(
'CatCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// 在网页视图中启用JavaScript
enableScripts: true,
retainContextWhenHidden: true,
}
);
panel.webview.html = getWebviewContent();
})
);
}
集成终端
VSCode 为开发者提供了丰富的集成终端 API,下面介绍部分主要 API
创建终端
ini
// 如下所示,通过createTerminal函数可以创建一个新的集成终端
const terminal = vscode.window.createTerminal('my terminal');
在集成终端中运行命令
ini
// 如下所示,通过sendText函数可以在集成终端中运行命令
const terminal = vscode.window.createTerminal('my terminal');
terminal.sendText('python /path/to/hello.py');
获取集成终端
通过以下 API 可以获得集成终端实例
- vscode.window.activeTernimal: 当前活跃的集成终端
- vscode.window.terminals: 所有打开的集成终端
监听集成终端的事件
通过以下 API 可以监听集成终端事件
- vscode.window.onDidChangeActiveTerminal: 当前活跃的集成终端被改变
- vscode.window.onDidCloseTerminal: 有集成终端被关闭
- vscode.window.onDidOpenTerminal: 有新的集成终端被打开
ini
vscode.window.onDidChangeActiveTerminal(e => {
console.log(`Active terminal changed, name=${e ? e.name : 'undefined'}`);
});
vscode.window.onDidOpenTerminal((terminal: vscode.Terminal) => {
console.log(`onDidOpenTerminal, name=${terminal.name}`);
});
vscode.window.onDidCloseTerminal((terminal: vscode.Terminal) => {
console.log(`onDidCloseTerminal, name=${terminal.name}`);
});
存储
在许多场景下都需要使用数据存储,因此 VSCode 提供了多种数据存储方案。
配置项
VSCode 的配置项存储在 setting.json 文件中
json
// 在package.json文件中,插件可以定义新的配置项
{
"contributes": {
"configuration": {
"title": "Typescript",
"properties": {
"typescript.useCodeSnippetsOnMethodSuggest": {
"type": "boolean",
"default": false,
"description": "Complete functions with their parameter sinature."
},
"typescript.tsdk": {
"type": ["string", "null"],
"default": null,
"description": "Specifies the folder path containing the tsserver and lib*.d.ts files to use."
}
}
}
}
}
访问配置项
在 Typescript/Javascript 文件中,可以通过 vscode.workspace.getConfiguration 访问配置项
ini
const config = vscode.workspace.getConfiguration('typescript');
const value = config.get('typescript.useCodeSnippetsOnMethodSuggest');
插件的数据存储
VSCode 为插件提供了数据存储的读写 API,每个插件都有独立的存储空间,主要分为以下两种:
- ExtensionContext.globalState: 全局范围的存储空间
- ExtensionContext.workspaceState: 针对当前工作区的存储空间
javascript
// 以下是插件的数据存储相关代码样例
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const value1 = context.globalState.get('key1');
context.globalState.update('key2', 'value2');
}
加密的数据存储
VSCode 并未直接提供用于存储加密数据的 API,但是 VScode 包含了 Keytar 软件包,可以让开发者通过操作系统的加密存储来添加、读取、更改、删除机密数据
要使用 Keytar,首先要在 package.json 文件中添加 TypeScript 的类型定义, "devDependencies": { "@types/keytar": "^4.0.1" }
接下来,就可以在 TypeScript/Javscript 文件中过使用 Keytar 了,代码如下:
typescript
import * as keytarType from 'keytar';
private _keytar: typeof keytarType;
this._keytar = getCoreNodeModule('keytar');
const token = await this._keytar.getPassword('vscode-docker', 'dockerhub');
function getCoreNodeModule(moduleName: string){
try{
return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`);
}catch(err){}
try{
return require(`${vscode.env.appRoot}/node_modules/${moduleName}`);
}catch(err){}
return null;
}