Skip to content

Vscode #23

@chenjigeng

Description

@chenjigeng

VScode 技术架构

Electron技术架构

Electron: Chromium + Nodejs + 自定义 API

  • 1 个主进程:一个 Electron App 只会启动一个主进程,它会运行 package.json 的 main 字段指定的脚本
  • N 个渲染进程:主进程代码可以调用 Chromium API 创建任意多个 web 页面,而 Chromium 本身是多进程架构,每个 web 页面都运行在属于它自己的渲染进程中

进程间通讯:

  • Render 进程之间的通讯本质上和多个 Web 页面之间通讯没有差别,可以使用各种浏览器能力如 localStorage
  • Render 进程与 Main 进程之间也可以通过 API 互相通讯 (ipcRenderer/ipcMain)

VScode

主要分为

  • 主进程:VSCode 的入口进程,负责一些类似窗口管理、进程间通信、自动更新等全局任务
  • 渲染进程:负责一个 Web 页面的渲染
  • 插件宿主进程:每个插件的代码都会运行在一个独属于自己的 NodeJS 环境的宿主进程中,插件不允许访问 UI。
  • Debug 进程:Debugger 相比普通插件做了特殊化
  • Search 进程:搜索是一类计算密集型的任务,单开进程保证软件整体体验与性能
  • LSP和DAP像两座桥梁,连接起语言和调试服务,它们都运行在插件进程中。

LSP

LSP(language service protocol) 主要是提供了一种通用的语言协议标准,只要按照这个标准来实现特定编程语言的提示,就可以实现 “实现一次,运行在多个编辑器下面”。

语言服务协议,编程语言需要为编辑器实现一些常用的功能,比如hover效果,代码提示(intelligence),代码诊断(diagnostics)等功能,每个编辑器都有一套自己的规则。 从图中我们可以看出,左边为编程语言,右边为编辑器。没有LSP之前,编程语言和编辑器之间是多对多的关系,这种复杂性为 n^2 。但是引入LSP之后,就变成了一对多的关系,主流编辑器都采用同一个协议规则,而编程语言只需要面向语言服务协议编写功能即可。

这是一张HTML语言服务协议和PHP语言服务协议的图,PHP和HTML实现了这种服务,而客户端通过JSON RPC这种远程调用,在VSCode插件进程内初始化这些语言服务。(语言服务运行在插件进程内)。语言客户端是作为一个扩展进程,而服务端则单独起了个服务,并且服务端可以用任何语言来实现。

DAP

DAP: debug adapter protocol。这个跟 LSP 一样,也是为了防止语言与IDE 有 n^2 的复杂度而提出的统一规范。所有的语言只需要按照 DAP 来实现,就可以做到实现一次,在所有 IDE 下跑。

依赖注入

InstantiationService

所有的实例都需要通过 InstantiationService.createInstance 来创建,并且所有的服务都会存放在 this._services里。只有在 services 里的服务,才能被依赖注入。

Service 类型

Service 主要分为已经构造好的实例 和 SyncDescriptor。

SyncDescriptor

它其实就是封装了实例构造参数的一个数据对象,包括以下属性:

  • ctor 将要被构造的类
  • staticArguments 被传入这个类的参数,和上文中的 args 意义相同
  • supportsDelayedInstantiation 是否支持延迟实例化

使用起来就像这样:

services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));

表示的其实就是 “不立刻实例化这个类,而当需要被注入的时候再进行实例化”

实现方式

每个服务都会有一个 services map,key 是对应的 decorator, value 则是一个实例或者 SyncDescriptor([constructor])。

每个服务都可以通过一个装饰器来声明自己想要用这个服务,之后在实例化的时候,则可以通过 services 这个 map 依赖注入进去。比如下图,通过装饰器来声明自己所依赖的服务。

装饰器的功能,只是将当前依赖的服务(比如 ILogService 对应的 logService) 加入到 StateService 的依赖项里面。

这样,当实例化构造器的时候(如下图),会先根据该实例的依赖属性构造一个依赖图,之后在从依赖项为0的节点开始实例化,实例化成功后再从依赖图里移去。从而保证被依赖的服务在依赖的服务之前实例化。

最终都是通过 createInstance 来创建最终的实例,在这里,会先拿到该服务的依赖项,并将其放入到构造器的参数里面,从而达到依赖注入的效果。

最后,在通过 _setServiceInstance 来将最新的实例与服务 id 绑定起来,起到更新的作用。

扩展加载机制

扩展的配置信息

扩展的配置信息挺多的,不过与加载相关的,我们可以先只关注 activationEvents。它主要就是用来表示这个扩展感兴趣的事件,只有这些事件触发的时候,扩展才会被激活。

{

    activationEvents: [

        "onLanguage:json",

        "onLanguage:markdown",

        "onLanguage:typescript"

    ]

}

扩展是如何加载的

客户端(Renderer)建立扩展进程

客户端会通过 LocalProcessExtensionHost 来加载vs/workbench/services/extensions/node/extensionHostProcess 文件,则这个 文件就是用来创建一个扩展进程的。

class A {

    start() {

        const env = objects.mixin(objects.deepClone(process.env), {

            AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',

            PIPE_LOGGING: 'true',

            VERBOSE_LOGGING: true,

            VSCODE_IPC_HOOK_EXTHOST: pipeName,

            VSCODE_HANDLES_UNCAUGHT_ERRORS: true,

            VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose),

            VSCODE_LOG_LEVEL: this._environmentService.verbose ? 'trace' : this._environmentService.log

        });

        const opts = {

            env,

            // We only detach the extension host on windows. Linux and Mac orphan by default

            // and detach under Linux and Mac create another process group.

            // We detach because we have noticed that when the renderer exits, its child processes

            // (i.e. extension host) are taken down in a brutal fashion by the OS

            detached: !!platform.isWindows,

            execArgv: undefined as string[] | undefined,

            silent: true

        };

        if (portNumber !== 0) {

            opts.execArgv = [

                '--nolazy',

                (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber

            ];

        } else {

            opts.execArgv = ['--inspect-port=0'];

        }

        // Run Extension Host as fork of current process

        this._extensionHostProcess = fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, ['--type=extensionHost'], opts);

    }

}

拿到客户端的扩展信息

在创建扩展进程的时候,会执行 startExtensionHostProcess 函数,而在这个函数中,首先会先跟(render)渲染器,也就是 vscode 客户端进行连接, 通过连接,可以拿到客户端的详细内容,其中就包括了一些初始化参数(包括客户端安装的扩展等)。

export async function startExtensionHostProcess(): Promise<void> {  

    const protocol = await createExtHostProtocol();

    const renderer = await connectToRenderer(protocol);

    const { initData } = renderer;

    ...

}

初始化ExtensionHostMain

const extensionHostMain = new ExtensionHostMain(

        renderer.protocol,

        initData,

        hostUtils,

        uriTransformer

);

这个函数的实现也很简单,在这里我忽略了很多其他代码 。其实就是将拿到的扩展函数进行格式转化,之后则直接初始化一个 ExtHostExtentionService 实例。而这个实例,则会根据扩展配置信息来实例化配置。

class ExtensionHostMain {

    constructor(

        protocol: IMessagePassingProtocol,

        initData: IInitData,

        hostUtils: IHostUtils,

        uriTransformer: IURITransformer | null

    ) {

        // ensure URIs are transformed and revived

        initData = ExtensionHostMain._transform(initData, rpcProtocol);

       

        this._extensionService = instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService));

        this._extensionService.initialize();

    }

}

创建一个 ExtensionsActivator,负责根据各种事件来决定 extention 的激活和关闭

ExtHostExtentionService 内部主要就是利用客户端的扩展信息创建了一个 ExtensionsActivator 实例,这个实例就是用来管理具体的扩展是激活还是关闭的。

它主要就是接受客户端传来的各种事件,然后根据扩展配置的 activationEvents 来做匹配,如果匹配上的话,那么这个扩展就会被激活。

export abstract class AbstractExtHostExtensionService extends Disposable implements ExtHostExtensionServiceShape {

constructor(

        @IInstantiationService instaService: IInstantiationService,

        @IHostUtils hostUtils: IHostUtils,

        @IExtHostRpcService extHostContext: IExtHostRpcService,

        @IExtHostWorkspace extHostWorkspace: IExtHostWorkspace,

        @IExtHostConfiguration extHostConfiguration: IExtHostConfiguration,

        @ILogService logService: ILogService,

        @IExtHostInitDataService initData: IExtHostInitDataService,

        @IExtensionStoragePaths storagePath: IExtensionStoragePaths,

        @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService,

        @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService

    ) {

        super();

        this._initData = initData;

        this._registry = new ExtensionDescriptionRegistry(this._initData.extensions);

        this._activator = new ExtensionsActivator(

            this._registry,

            this._initData.resolvedExtensions,

            this._initData.hostExtensions,

            {

                onExtensionActivationError: (extensionId: ExtensionIdentifier, error: ExtensionActivationError): void => {

                    this._mainThreadExtensionsProxy.$onExtensionActivationError(extensionId, error);

                },

                actualActivateExtension: async (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<ActivatedExtension> => {

                    if (hostExtensions.has(ExtensionIdentifier.toKey(extensionId))) {

                        await this._mainThreadExtensionsProxy.$activateExtension(extensionId, reason);

                        return new HostExtension();

                    }

                    const extensionDescription = this._registry.getExtensionDescription(extensionId)!;

                    return this._activateExtension(extensionDescription, reason);

                }

            },

            this._logService

        );

    }

}

当扩展被激活的时候,则先加载扩展之后激活

当 Activator 接收到一个扩展被激活后,则触发 ExtHostExtentionService 的 _doActivateExtension。在这里,则会先加载扩展的入口文件(在加载的时候会做一些骚操作,可以看下文),之后在将这个扩展服务激活。

class AbstractExtHostExtensionService {

    private _doActivateExtension(extensionDescription: IExtensionDescription, reason: ExtensionActivationReason): Promise<ActivatedExtension> {

       const entryPoint = this._getEntryPoint(extensionDescription);

       const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup);

        return Promise.all([

            this._loadCommonJSModule<IExtensionModule>(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder),

            this._loadExtensionContext(extensionDescription)

        ]).then(values => {

            return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder);

        });

    }



}

总结

这样一下,整个流程就跑通了。

  1. 在客户端刚启动的时候,会加载 一个extensionHostProcess文件,并通过这个 文件来创建一个扩展进程。
  2. 扩展进程会通过与客户端进程连接通信,拿到客户端的扩展信息。
  3. 之后,会根据扩展信息来初始化 ExtentionHostMain,则在这个实例内部,是通过一个 Activator 来 控制激活扩展
  4. Activator 通过监听各种 activeEvents 来决定激活哪些扩展,如果 扩展要激活的话,则通过 ExtentionHostMain 来 激活。
  5. 当一个扩展被激活的时候,会先加载这个扩展的入口文件,并执行 active 函数,至此,一个扩展的加载就完成了。

为什么扩展可以直接使用 vscode

import * as vscode from 'vscode';

在扩展里面,使用这个代码引入的,其实只有 vscode 的类型提示,并不会引入真正的代码 ,那么 vscode 是怎么做到 扩展代码在实际执行的时候不会有问题呢。(vscode 具体的实现在 api/common/extHost.api.impl.ts 里)

原理:如下代码(node/extHostExtensionService.ts),vscode 将 required 整个重写了,对于 要加载的模块,先判断自己的 _factories(如下图)有没有,如果没有的话,再走原生的加载逻辑。有的话,则直接使用预先设定好的模块代码。

原理:通过 ExtHostExtensionService 的 _loadCommonJSMw bodule 函数来加载对应的扩展代码,而 _loadCommonJSModule 这里的话,其实本质上就是通过动态执行代码( new Function ) 的方式,而通过这种方式,就可以将自己的 module/exports/require 注入进去,所以扩展里拿到的 require 其实本质上是外层传进去的。而 vscode 则通过对这个 require 做劫持,来将自己的代码注入进去。

protected async _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {

    module = module.with({ path: ensureSuffix(module.path, '.js') });

    const response = await fetch(module.toString(true));

    if (response.status !== 200) {

        throw new Error(response.statusText);

    }

    // fetch JS sources as text and create a new function around it

    const source = await response.text();

    // Here we append #vscode-extension to serve as a marker, such that source maps

    // can be adjusted for the extra wrapping function.

    const sourceURL = `${module.toString(true)}#vscode-extension`;

    const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`);

    // define commonjs globals: `module`, `exports`, and `require`

    const _exports = {};

    const _module = { exports: _exports };

    // 核心在这里

    const _require = (request: string) => {

        const result = this._fakeModules!.getModule(request, module);

        if (result === undefined) {

            throw new Error(`Cannot load module '${request}'`);

        }

        return result;

    };

    try {

        activationTimesBuilder.codeLoadingStart();

        initFn(_module, _exports, _require);

        return <T>(_module.exports !== _exports ? _module.exports : _exports);

    } finally {

        activationTimesBuilder.codeLoadingStop();

    }

}

这段代码的核心就在于 require 这里,通过劫持它,走自己的 fakeModules.getModule 逻辑。

VScode 如何实现注册命令和执行命令

命令注册器 (command.ts 里的 CommandsRegistry 和 keybindingsRegistry 里的 KeybindingsRegistry)

命令主要分为两种,一种是单纯的命令,还有一种是快捷键命令。

  1. 对于单纯的命令,VScode 将所有的命令都统一放到一个全局的 CommandsRegistry 来管理。通过 对外暴露 CommandsRegistry.registerCommand 来注册命令。
  2. 对于快捷键命令的话,由于需要根据用户键盘的输入来做判断依据,因此走的是 KeybindingsRegistry.registerCommandAndKeybindingRule 逻辑,registerCommandAndKeybindingRule 只是添加了一个键盘输入的判断指令逻辑,内部依然也调用CommandsRegistry.registerCommand 来注册命令。

一个 Command 的类型如下,主要 有一个 id(唯一标识),一个触发命令后要执行的函数 handler,以及命令的一些描述信息。

export interface ICommandHandler {

    (accessor: ServicesAccessor, ...args: any[]): void;

}

export interface ICommand {

    id: string;

    handler: ICommandHandler;

    description?: ICommandHandlerDescription | null;

}

export interface ICommandHandlerDescription {

    readonly description: string;

    readonly args: ReadonlyArray<{

        readonly name: string;

        readonly description?: string;

        readonly constraint?: TypeConstraint;

        readonly schema?: IJSONSchema;

    }>;

    readonly returns?: string;

}

命令执行器(commandService 里的 commandService)

这个主要是用来执行 command 的。对外暴露了 executeCommand 方法,接受一个 commandId 和 一个 args( 命令执行参数)作为输入。在这里根据 commandId 来通过找到对应的命令,并执行命令注册时的 handler。

命令接收器

既然有个命令注册和执行的接口了,接下来就需要去监听命令指令并执行。vscode 触发命令的方式有点多,比如通过顶部菜单触发、通过快捷键触发等。

快捷键触发(keybindingService 里的 WorkbenchKeybindingService)

通过全局监听 keydown 事件,然后将用户的按键在 KeybindingsRegistry 里注册的按键指令里判断是否有对应的指令要执行。如果有的话,则执行该命令的 handler。

顶部菜单触发(quickInput.ts)

顶部菜单栏是通过quickInput.getUI(src/vs/base/parts/quickinput/browser/quickInput.ts) 来渲染的,在 getUI 的时候,会注册一个 KEY_DOWN 事件,当键盘输入 Enter 的时候,则触发选中的指令对应的 handler。

还有很多其他地方会触发,不过其实都是通过命令执行器(commandService.executeCommand)来执行。

语言服务是怎么工作的

注册入口文件是src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts,统一在这里注册语言特性 (类似于 CommonService)。这里以 doHover 为例。

mainThreadLanguageFeatures 通过对外暴露各种 api 接口去注册服务。比如 doHover 的话,对外的接口是 $registerHoverProvider,而这个接口只在 extHostLanguageFeatures.ts 里的registerHoverProvider 用到。而这个 extHostLanguageFeatures.ts 其实就是vscode 的实现(extHost.api.impl.ts 里,也只是简单的传参而已)

// extHost.api.impl.ts 

function registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider): vscode.Disposable {

    return extHostLanguageFeatures.registerHoverProvider(extension, checkSelector(selector), provider, extension.identifier);

}

主要接受一个 selector 和 一个 provider。我们先来看下两者的结构

Selector 结构

Selector 的话,要么接受一个 string(文件类型),要么接受一个 DocumentFilter 类型,主要就是用来描述我这个 provider 应用于哪些文件。

export type DocumentSelector = DocumentFilter | string | ReadonlyArray<DocumentFilter | string>;



export interface DocumentFilter {

    /**

     * A language id, like `typescript`.

     */

    readonly language?: string;

    /**

     * A Uri [scheme](#Uri.scheme), like `file` or `untitled`.

     */

    readonly scheme?: string;

    /**

     * A [glob pattern](#GlobPattern) that is matched on the absolute path of the document. Use a [relative pattern](#RelativePattern)

     * to filter documents to a [workspace folder](#WorkspaceFolder).

     */

    readonly pattern?: GlobPattern;

}

Provider 结构

一个 Provider 主要有一个 providerXXX 方法,这个方法就用于接受一些参数,并且按照 LSP 规定的协议来返回固定的结构。比如 doHover 的话,则是返回要展示的 Hover 的 内容。

export interface HoverProvider {

        /**

         * Provide a hover for the given position and document. Multiple hovers at the same

         * position will be merged by the editor. A hover can have a range which defaults

         * to the word range at the position when omitted.

         *

         * @param document The document in which the command was invoked.

         * @param position The position at which the command was invoked.

         * @param token A cancellation token.

         * @return A hover or a thenable that resolves to such. The lack of a result can be

         * signaled by returning `undefined` or `null`.

         */

        provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<Hover>;

    }

工作流程

理清楚这个结构之后,我想就比较清晰了

注册阶段

对于一个 extention 来说,就是通过调用 vscode 暴露的 api(比如 registHoverProvider) 来注册特定语言的语言特性。每个特性都会有一个 selector(文件匹配规则) 和 provider (匹配后的处理函数)来对应。

鼠标的事件如何交互

通过上面的如何注册和执行命令,我们知道了我们可以通过劫持键盘或者手动选择命令的方式来执行指令,可是对于一些鼠标的事件,比如 hover,点击这种,vscode 是如何来交互的呢?这里以 doHover 为例

编辑其相关的指令和操作,都是通过 src/vs/editor/contrib 里的来定义的,一种操作是一个单独的文件夹。

Vscode 客户端启动流程

  1. 每个客户端都会加载一份 workbench.js 代码
  2. workbench.js 里面,会加载 vs/workbench/electron-browser/desktop.main 代码并执行 main 函数

  1. Main 函数,则是初始化了一个 DesktopMain 实例并执行 open 函数来创建客户端主框架

  1. DesktopMain 的 open 方法主要就是创建一个 WorkBench 实例, 并执行一些初始化和事件监听操作。之后执行 workbench.startup 来渲染界面

  1. 通过 renderWorkbench 来将页面每一块渲染到 body 里面(侧边栏,顶部栏,菜单等)

Disposable 和 Event

Event

VSCode 的 Event 采取的也是 eventEmit 的方式,通过创建一个 Emitter 对象,之后就可以注册、发布事件。

export class EventEmitter<T> {



    /**

     * The event listeners can subscribe to.

     */

    event: Event<T>;



    /**

     * Notify all subscribers of the [event](#EventEmitter.event). Failure

     * of one or more listener will not fail this function call.

     *

     * @param data The event object.

     */

    fire(data: T): void;



    /**

     * Dispose this object and free resources.

     */

    dispose(): void;

}



export interface Event<T> {



        /**

         * A function that represents an event to which you subscribe by calling it with

         * a listener function as argument.

         *

         * @param listener The listener function will be called when the event happens.

         * @param thisArgs The `this`-argument which will be used when calling the event listener.

         * @param disposables An array to which a [disposable](#Disposable) will be added.

         * @return A disposable which unsubscribes the event listener.

         */

        (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable;

}

�这里我觉得毕竟奇怪的是,要订阅这个事件,居然得通过访问 emitter.event(cb) 方式来订阅,真是匪夷所思。

使用方式

const countEmitter = new Emitter<number>();

const countEmitterEvent = countEmitter.event;

countEmitterEvent((c) => {

    console.log('event cb', c)

});



countEmitter.fire()

Disposable

Disposable 主要是用来做副作用清除的,比如假设你订阅了很多个事件,你可以通过 Disposable 来做管理,在合适的时机来将所有的副作用清空。

export abstract class Disposable implements IDisposable {



    static readonly None = Object.freeze<IDisposable>({ dispose() { } });



    private readonly _store = new DisposableStore();



    constructor() {

        trackDisposable(this);

    }



    public dispose(): void {

        markTracked(this);



        this._store.dispose();

    }



    protected _register<T extends IDisposable>(t: T): T {

        if ((t as unknown as Disposable) === this) {

            throw new Error('Cannot register a disposable on itself!');

        }

        return this._store.add(t);

    }

}

Dispoable 的代码也很简单,主要就是通过 _register 来将副作用的实例(继承自 IDisposable)放入到 this._store 数组,然后在 clear 的时候,调用所有副作用的 dispose 方法来清除副作用。

public dispose(): void {

    if (this._isDisposed) {

        return;

    }



    markTracked(this);

    this._isDisposed = true;

    this.clear();

}



/**

 * Dispose of all registered disposables but do not mark this object as disposed.

 */

public clear(): void {

    try {

        dispose(this._toDispose.values());

    } finally {

        this._toDispose.clear();

    }

}



export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | undefined): any {

    if (Iterable.is(arg)) {

        let errors: any[] = [];



        for (const d of arg) {

            if (d) {

                markTracked(d);

                try {

                    d.dispose();

                } catch (e) {

                    errors.push(e);

                }

            }

        }



        if (errors.length === 1) {

            throw errors[0];

        } else if (errors.length > 1) {

            throw new MultiDisposeError(errors);

        }



        return Array.isArray(arg) ? [] : arg;

    } else if (arg) {

        markTracked(arg);

        arg.dispose();

        return arg;

    }

}

那么我们来看下 IDisposable 长啥样

export interface IDisposable {

    dispose(): void;

}

没错,很简单。只要你具有一个 dispose 函数,你就是一个 IDisposable 实例。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions