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 executedexport 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 activatedconsole.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.jsonconst 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 uservscode.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 editorconst editor = vscode.window.activeTextEditor;if (editor) {const document = editor.document;const selection = editor.selection;// Get the word within the selectionconst 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:prepublishvsocde: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); }); }
自定义的编辑器```javascriptexport 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 extensionconst 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通道发送出去
