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

  1. import * as vscode from 'vscode';
  2. // this method is called when your extension is activated
  3. // your extension is activated the very first time the command is executed
  4. export function activate(context: vscode.ExtensionContext) {
  5. // Use the console to output diagnostic information (console.log) and errors (console.error)
  6. // This line of code will only be executed once when your extension is activated
  7. console.log('Congratulations, your extension "helloworld-sample" is now active!');
  8. // The command has been defined in the package.json file
  9. // Now provide the implementation of the command with registerCommand
  10. // The commandId parameter must match the command field in package.json
  11. const disposable = vscode.commands.registerCommand('extension.helloWorld', () => {
  12. // The code you place here will be executed every time your command is executed
  13. // Display a message box to the user
  14. vscode.window.showInformationMessage('Hello World!');
  15. });
  16. context.subscriptions.push(disposable);
  17. }
  1. 'use strict';
  2. import * as vscode from 'vscode';
  3. export function activate(context: vscode.ExtensionContext) {
  4. const disposable = vscode.commands.registerCommand('extension.reverseWord', function () {
  5. // Get the active text editor
  6. const editor = vscode.window.activeTextEditor;
  7. if (editor) {
  8. const document = editor.document;
  9. const selection = editor.selection;
  10. // Get the word within the selection
  11. const word = document.getText(selection);
  12. const reversed = word.split('').reverse().join('');
  13. //
  14. editor.edit(editBuilder => {
  15. editBuilder.replace(selection, reversed);
  16. });
  17. }
  18. });
  19. context.subscriptions.push(disposable);
  20. }


Manifest文件中关键字段

  • main 入口文件
  • engines 兼容的vscode版本
  • categories 类型
  • keywords
  • contributes 众多配置参数都在这里
  • activationEvents 插件触发的时机
  • extensionDependencies 依赖的其它插件,在安装此插件时会先安装被依赖的插件
  • extensionPack 打包到一起的插件
  • extensionKind
  • icon
  • scripts 有一些vscode的特殊命令, vscode:prepublish vsocde:uninstall
  1. {
  2. "activationEvents": [
  3. "onCommand:extension.helloWorld"
  4. ],
  5. "main": "./out/extension.js",
  6. "extensionDependencies": ['vscode.csharp'],
  7. "contributes": {
  8. "commands": [
  9. {
  10. "command": "extension.helloWorld",
  11. "title": "Hello World"
  12. }
  13. ]
  14. },
  15. }

提供的API

  • context: ExtensionContext 当前插件的一些上下文,
  • vscode api 提供和vscode内部各种api

context

  • subscriptions
  • storageUri,storagePath,logUri,logPath,extensionUri,extensionPath,extensionMode等

关于vscode api

提供有事件

  1. vscode.workspace.onDidChangeConfiguration((_) => {
  2. this._onDidChangeCodeLenses.fire();
  3. });

提供有API,属性,方法

提供有注册扩展的方法

  1. vscode.window.registerCustomEditorProvider

核心的一些对象

  • editor 编辑
  • workspace
  • window
  • extensions
  • commands

调用其它插件

  1. import { extensions } from 'vscode';
  2. let mathExt = extensions.getExtension('genius.math');
  3. let importedApi = mathExt.exports;
  4. 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 基本的位置
    1. new Range(start:Position, end: Postion): Range;
    2. new Position(line: number, character: number): Position;
    reverse word插件的部分代码 ```javascript 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: TextEditorEdit) => { editBuilder.replace(selection, reversed); }); }

  1. 自定义的编辑器
  2. ```javascript
  3. export class PawDrawEditorProvider implements vscode.CustomEditorProvider<PawDrawDocument> {
  4. resolveCustomEditor
  5. }

codelens插件

http://github.com/microsoft/vscode-extension-samples/blob/master/codelens-sample

使用Provider模式

  1. export class CodelensProvider implements vscode.CodeLensProvider {
  2. private codeLenses: vscode.CodeLens[] = [];
  3. public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
  4. return this.codeLens();
  5. }
  6. public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken) {
  7. return codeLens;
  8. }
  9. }

language插件

一些特性探究

探究VSCode API的实现

  1. import {window, command} from 'vscode'

还要从nodejs模块中 require 背后如何工作说起。参见Node.js 中的 require 是如何工作的? - 掘金

  1. Module.prototype.require = function(path) {
  2. return Module._load(path, this);
  3. };

每个模块里的调用 require 方法背后调用的是 Module._load 这个代码。因此我们在 require(vscode) 时,我们把它替换为我们需要的变量即可。

Vscode中 vscode 变量的注入,JEST中 mock module 的实现都是基于修改 Module._load 实现的

  • 会为每一个插件生成一个独立的API实例,why ?

extHost.api.impl.ts

  1. const commands: typeof vscode.commands = {
  2. registerCommand(id: string, command: <T>(...args: any[]) => T | Thenable<T>, thisArgs?: any): vscode.Disposable {
  3. return extHostCommands.registerCommand(true, id, command, thisArgs);
  4. },
  5. }
  6. // 重写Module._load方法
  7. function defineAPI(...): void{
  8. node_module._load = function load(request: string, parent: any, isMain: any) {
  9. if (request !== 'vscode') {
  10. return original.apply(this, arguments);
  11. }
  12. // get extension id from filename and api for extension
  13. const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
  14. if (ext) {
  15. let apiImpl = extApiImpl.get(ext.id);
  16. if (!apiImpl) {
  17. apiImpl = factory(ext, extensionRegistry);
  18. extApiImpl.set(ext.id, apiImpl);
  19. }
  20. return apiImpl;
  21. }
  22. // fall back to a default implementation
  23. ...
  24. }
  25. }

extHostCommands

  1. export class ExtHostCommands implements ExtHostCommandsShape {
  2. constructor(
  3. mainContext: IMainContext,
  4. heapService: ExtHostHeapService,
  5. logService: ILogService
  6. ) {
  7. this._proxy = mainContext.getProxy(MainContext.MainThreadCommands);
  8. this._logService = logService;
  9. this._converter = new CommandsConverter(this, heapService);
  10. this._argumentProcessors = [{ processArgument(a) { return revive(a, 0); } }];
  11. }
  12. registerCommand(global: boolean, id: string, callback: <T>(...args: any[]) => T | Thenable<T>, thisArg?: any, description?: ICommandHandlerDescription): extHostTypes.Disposable {
  13. this._logService.trace('ExtHostCommands#registerCommand', id);
  14. if (!id.trim().length) {
  15. throw new Error('invalid id');
  16. }
  17. if (this._commands.has(id)) {
  18. throw new Error(`command '${id}' already exists`);
  19. }
  20. this._commands.set(id, { callback, thisArg, description });
  21. if (global) {
  22. // 这部分是关键
  23. this._proxy.$registerCommand(id);
  24. }
  25. return new extHostTypes.Disposable(() => {
  26. if (this._commands.delete(id)) {
  27. if (global) {
  28. this._proxy.$unregisterCommand(id);
  29. }
  30. }
  31. });
  32. }
  33. }

rpcProcotol

  1. export class RPCProtocol extends Disposable implements IRPCProtocol {
  2. public getProxy<T>(identifier: ProxyIdentifier<T>): T {
  3. const rpcId = identifier.nid;
  4. if (!this._proxies[rpcId]) {
  5. this._proxies[rpcId] = this._createProxy(rpcId);
  6. }
  7. return this._proxies[rpcId];
  8. }
  9. private _createProxy<T>(rpcId: number): T {
  10. let handler = {
  11. get: (target: any, name: string) => {
  12. if (!target[name] && name.charCodeAt(0) === CharCode.DollarSign) {
  13. target[name] = (...myArgs: any[]) => {
  14. return this._remoteCall(rpcId, name, myArgs);
  15. };
  16. }
  17. return target[name];
  18. }
  19. };
  20. return new Proxy(Object.create(null), handler);
  21. }
  22. }

这里面还用到了ES6中的Proxy

commands.registerCommand -> extHostCommands.registerCommand -> this._proxy.$registerCommand -> this._proxy._remoteCall -> this._protocol.send (最后通过ipc通道发送消息)

即$registerCommand是不存在的,会调用_createProxy中handler中的get方法,会拿到messageName通过ipc通道发送出去

借鉴