写这篇文章是因为最近一段时间的工作涉及到 Cloud Studio 插件这一块的内容,旧的插件系统在面向用户开放后暴露了安全性、扩展性等诸多问题。调研了几个不同架构下 IDE 的插件系统实现( Theia, VS Code 等),也大致阅读了一遍 VS Code 插件系统相关的源码,在这里做一个简单的分享,个人水平有限,如有错误之处还请观众老爷们指点一下。


从加载一个插件开始

以我们熟悉的 vscode-eslint 为例,查看源码会发现入口是 extension.ts 文件里的 activate 函数,它的函数签名像这样:

activate(context: ExtensionContext): void

需要了解的一点是, package.json 里的 activationEvents 字段定义了插件的激活事件,考虑到性能问题,我们并不需要一启动 VS Code 就立即激活所有的插件。activation-events 定义了一组事件,当 activationEvents 字段指定的事件被触发时才会激活相应的插件。包含了特定语言的文件被打开,或者特定的【命令】被触发,以及某些视图被切换甚至是一些自定义命令被触发等等事件。

例如在 vscode-java 中,activationEvents 字段的值为

"activationEvents": [
    "onLanguage:java",
    "onCommand:java.show.references",
    "onCommand:java.show.implementations",
    "onCommand:java.open.output",
    "onCommand:java.open.serverLog",
    "onCommand:java.execute.workspaceCommand",
    "onCommand:java.projectConfiguration.update",
    "workspaceContains:pom.xml",
    "workspaceContains:build.gradle"
]

其中包含 languageId 为 java 的文件被打开,以及由该插件自定义的几个 JDT 语言服务命令被触发,和【工作空间】包含 pom.xml/buld.gradle 这些事件。在以上事件被触发时插件将会被激活。

这段逻辑被定义在 src/vs/workbench/api/node/extHostExtensionService.ts 中

// 由 ExtensionHostProcessManager 调用并传入相应事件作为参数
public $activateByEvent(activationEvent: string): Thenable<void> {
  return (
    this._barrier.wait()
      .then(_ => this._activateByEvent(activationEvent, false))
  );
}

/* 省略部分代码 */

// 实例化 activator
this._activator = new ExtensionsActivator(this._registry, {
  
  /* 省略部分代码 */

  actualActivateExtension: (extensionDescription: IExtensionDescription, reason: ExtensionActivationReason): Promise<ActivatedExtension> => {
    return this._activateExtension(extensionDescription, reason);
  }
});

// 调用 ExtensionsActivator 的实例 activator 的方法激活插件
private _activateByEvent(activationEvent: string, startup: boolean): Thenable<void> {
  const reason = new ExtensionActivatedByEvent(startup, activationEvent);
  return this._activator.activateByEvent(activationEvent, reason);
}

其中 ExtensionsActivator 定义在 src/vs/workbench/api/node/extHostExtensionActivator.ts 中

export class ExtensionsActivator {
  constructor(
    registry: ExtensionDescriptionRegistry,
    // 既上文中实例化 activator 传的第二个参数
    host: IExtensionsActivatorHost,
  ) {
    this._registry = registry;
    this._host = host;
  }
}

当调用 activator.activateByEvent 方法时(既某个事件被触发),activator 会获取所有符合该事件的插件并逐一执行 extHostExtensionService._activateExtension 方法(也就是 activator.actualActivateExtension) ,中间省去获取上下文,记录日志等一通操作后调用了 extHostExtensionService._callActivateOptional 静态方法

/* 省略部分代码 */
// extension.ts 里的 activate 函数
if (typeof extensionModule.activate === 'function') {
  try {
    activationTimesBuilder.activateCallStart();
    logService.trace(`ExtensionService#_callActivateOptional ${extensionId}`);
    // 调用并传入相关参数
    const activateResult: Thenable<IExtensionAPI> = extensionModule.activate.apply(global, [context]);
    activationTimesBuilder.activateCallStop();

    activationTimesBuilder.activateResolveStart();
    return Promise.resolve(activateResult).then((value) => {
      activationTimesBuilder.activateResolveStop();
      return value;
    });
  } catch (err) {
    return Promise.reject(err);
  }
}

至此,插件被成功激活。

插件如何运行

再来看插件的代码,插件中需要引入一个叫 vscode 的模块

import * as vscode from 'vscode';