vscode插件的几点特色
- 对VScode的UI改造能力弱,但可以开一个独立的webview页面
- 通过独立的进程extension host process运行
- 在某种情况下激活插件
- 插件可依赖其它插件
几个比较好奇的点
- 既然是独立进程,
vscode
这个变量如何注入的,如何调用主进行的API
大纲
- 插件可以做什么?
- VSCode API的实现
- 插件的设计 有什么值得学习的
- 插件配置文件 (可以做哪些)
vscode插件案例
microsoft/vscode-extension-samples: Sample code illustrating the VS Code extension API.
VSCode中插件核心思想
很多的核心部分都是通过插件来实现的
插件的分类
- 主题类插件
- 扩展工作区插件
- 一个自定义webview
- 支持language类插件
- 工具类插件 (最多见的)
- debugger插件
如何开发一个插件
Extension Guidelines | Visual Studio Code Extension API vscode的组成部分,暴露给插件的API
一个插件类似一个npm包开发方式
- Manifest信息 写在
package.json
中 - 入口文件,在
package.json
中的main
字段指出
插件目录结构
package.json main文件
插件的main文件示例
main文件通过返回一个函数,在插件被激活时,会调用此函数,传入context
import * as vscode from 'vscode';
// 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 "helloworld-sample" is now active!');
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
const disposable = vscode.commands.registerCommand('extension.helloWorld', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World!');
});
context.subscriptions.push(disposable);
}
'use strict';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.commands.registerCommand('extension.reverseWord', function () {
// Get the active text editor
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
const selection = editor.selection;
// Get the word within the selection
const word = document.getText(selection);
const reversed = word.split('').reverse().join('');
//
editor.edit(editBuilder => {
editBuilder.replace(selection, reversed);
});
}
});
context.subscriptions.push(disposable);
}
Manifest文件中关键字段
- main 入口文件
- engines 兼容的vscode版本
- categories 类型
- keywords
- contributes 众多配置参数都在这里
- activationEvents 插件触发的时机
- extensionDependencies 依赖的其它插件,在安装此插件时会先安装被依赖的插件
- extensionPack 打包到一起的插件
- extensionKind
- icon
- scripts 有一些vscode的特殊命令,
vscode:prepublish
vsocde:uninstall
{
"activationEvents": [
"onCommand:extension.helloWorld"
],
"main": "./out/extension.js",
"extensionDependencies": ['vscode.csharp'],
"contributes": {
"commands": [
{
"command": "extension.helloWorld",
"title": "Hello World"
}
]
},
}
提供的API
- context: ExtensionContext 当前插件的一些上下文,
- vscode api 提供和vscode内部各种api
context
- subscriptions
- storageUri,storagePath,logUri,logPath,extensionUri,extensionPath,extensionMode等
关于vscode api
提供有事件
vscode.workspace.onDidChangeConfiguration((_) => {
this._onDidChangeCodeLenses.fire();
});
提供有API,属性,方法
提供有注册扩展的方法
vscode.window.registerCustomEditorProvider
核心的一些对象
- editor 编辑
- workspace
- window
- extensions
- commands
调用其它插件
import { extensions } from 'vscode';
let mathExt = extensions.getExtension('genius.math');
let importedApi = mathExt.exports;
console.log(importedApi.mul(42, 1));
一些插件探究
Editor插件
https://github.com/microsoft/vscode-extension-samples/blob/master/document-editing-sample
https://github.com/microsoft/vscode-extension-samples/tree/master/custom-editor-sample
可以学习一下编辑器编辑的核心抽象
- registerCustomEditorProvider
核心编辑对象
- TextEditor 整个编辑器对象,下面有document,selection的核心属性,edit方法
- TextDocument 文件内容操作的抽象,getText(range),lineAt,save。为什么没有edit方法?
- TextEditorEdit 在edit时,生成一个此对象,供修改操作
为何editor.edit要从callback的方式,提供一个TextEditorEdit对象
为何TextDocument没有edit方法,而在TextEditor对象上
选中的范围
- Selection 选中的范围
- SelectionRangeProvider
位置对象
- Range 重要的区块范围
- Position 基本的位置
reverse word插件的部分代码 ```javascript const editor = vscode.window.activeTextEditor;new Range(start:Position, end: Postion): Range;
new Position(line: number, character: number): Position;
if (editor) { const document = editor.document; const selection = editor.selection;
// Get the word within the selection const word = document.getText(selection); const reversed = word.split(‘’).reverse().join(‘’); // editor.edit((editBuilder: TextEditorEdit) => { editBuilder.replace(selection, reversed); }); }
自定义的编辑器
```javascript
export class PawDrawEditorProvider implements vscode.CustomEditorProvider<PawDrawDocument> {
resolveCustomEditor
}
codelens插件
http://github.com/microsoft/vscode-extension-samples/blob/master/codelens-sample
使用Provider模式
export class CodelensProvider implements vscode.CodeLensProvider {
private codeLenses: vscode.CodeLens[] = [];
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
return this.codeLens();
}
public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken) {
return codeLens;
}
}
language插件
一些特性探究
探究VSCode API的实现
import {window, command} from 'vscode'
还要从nodejs模块中 require
背后如何工作说起。参见Node.js 中的 require 是如何工作的? - 掘金
Module.prototype.require = function(path) {
return Module._load(path, this);
};
每个模块里的调用 require
方法背后调用的是 Module._load
这个代码。因此我们在 require(vscode)
时,我们把它替换为我们需要的变量即可。
Vscode中 vscode
变量的注入,JEST中 mock module
的实现都是基于修改 Module._load
实现的
- 会为每一个插件生成一个独立的API实例,why ?
extHost.api.impl.ts
const commands: typeof vscode.commands = {
registerCommand(id: string, command: <T>(...args: any[]) => T | Thenable<T>, thisArgs?: any): vscode.Disposable {
return extHostCommands.registerCommand(true, id, command, thisArgs);
},
}
// 重写Module._load方法
function defineAPI(...): void{
node_module._load = function load(request: string, parent: any, isMain: any) {
if (request !== 'vscode') {
return original.apply(this, arguments);
}
// get extension id from filename and api for extension
const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
if (ext) {
let apiImpl = extApiImpl.get(ext.id);
if (!apiImpl) {
apiImpl = factory(ext, extensionRegistry);
extApiImpl.set(ext.id, apiImpl);
}
return apiImpl;
}
// fall back to a default implementation
...
}
}
extHostCommands
export class ExtHostCommands implements ExtHostCommandsShape {
constructor(
mainContext: IMainContext,
heapService: ExtHostHeapService,
logService: ILogService
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadCommands);
this._logService = logService;
this._converter = new CommandsConverter(this, heapService);
this._argumentProcessors = [{ processArgument(a) { return revive(a, 0); } }];
}
registerCommand(global: boolean, id: string, callback: <T>(...args: any[]) => T | Thenable<T>, thisArg?: any, description?: ICommandHandlerDescription): extHostTypes.Disposable {
this._logService.trace('ExtHostCommands#registerCommand', id);
if (!id.trim().length) {
throw new Error('invalid id');
}
if (this._commands.has(id)) {
throw new Error(`command '${id}' already exists`);
}
this._commands.set(id, { callback, thisArg, description });
if (global) {
// 这部分是关键
this._proxy.$registerCommand(id);
}
return new extHostTypes.Disposable(() => {
if (this._commands.delete(id)) {
if (global) {
this._proxy.$unregisterCommand(id);
}
}
});
}
}
rpcProcotol
export class RPCProtocol extends Disposable implements IRPCProtocol {
public getProxy<T>(identifier: ProxyIdentifier<T>): T {
const rpcId = identifier.nid;
if (!this._proxies[rpcId]) {
this._proxies[rpcId] = this._createProxy(rpcId);
}
return this._proxies[rpcId];
}
private _createProxy<T>(rpcId: number): T {
let handler = {
get: (target: any, name: string) => {
if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) {
target[name] = (...myArgs: any[]) => {
return this._remoteCall(rpcId, name, myArgs);
};
}
return target[name];
}
};
return new Proxy(Object.create(null), handler);
}
}
这里面还用到了ES6中的Proxy
commands.registerCommand -> extHostCommands.registerCommand -> this._proxy.$registerCommand -> this._proxy._remoteCall -> this._protocol.send (最后通过ipc通道发送消息)
即$registerCommand是不存在的,会调用_createProxy中handler中的get方法,会拿到messageName通过ipc通道发送出去