VSCode技术揭秘(一)

前言

Visual Studio Code(如下简称VSCode)是一个轻量且强大的跨平台开源代码编辑器(IDE),VSCode 采用了 Electron,使用的代码编辑器名为 Monaco、Monaco 也是 Visual Studio Team Service(Visual Studio Online)使用的代码编辑器,在语言上,VSCode 使用了自家的 TypeScript 语言开发。javascript

VSCode提供了强大的插件拓展机制,并提供 插件市场 供开发者发布、下载插件。VSCode提供了丰富的扩展能力模型,例如基础的语法高亮/API提示、引用跳转(转到定义)、文件搜索、主题定制,高级的debug协议等等。但不容许插件直接访问底层UI DOM(即很难定制VSCode外观),由于VSCode开发团队随着优化VSCode而频繁更改UI Dom,因此将UI定制能力限制起来。css

可是当你想要开发一款专用IDE时,不想从零开始撸,而是站在巨人的肩膀上作二次开发的话,那么VSCode将是你不二的选择,像 Weex Studio白鹭Egret Wing快应用IDE等IDE,都是基于VSCode扩展加强。html

Weex Studio

Egret Wing

快应用

本系列文章将带你了解VSCode源码的总体架构和定制方法,一步一步从源码入手,定制一款专用开发工具。前端

技术介绍

学习VSCode源码的同窗基本上都是作前端工做的,那么node.js和javascript都是基本功了,这里不用过度强调了。可是在阅读VSCode源码以前,仍是须要对VSCode使用相关技术框架有所了解。java

Electron介绍

众所周知,VSCode是一款桌面编辑器应用,可是前端单纯用js是作不了桌面应用的,因此采用Electron来构建。Electron是基于 Chromium 和 Node.js,使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用,它兼容 Mac、Windows 和 Linux,能够构建出三个平台的应用程序。node


从实现上来看,Electron = Node.js + Chromium + Native APIpython

也就是说Electron拥有Node运行环境,赋予了用户与系统底层进行交互的能力,依靠Chromium提供基于Web技术(HTML、CSS、JS)的界面交互支持,另外还具备一些平台特性,好比桌面通知等。linux

从API设计上来看,Electron App通常都有1个Main Process和多个Renderer Process:git

  • main process:主进程环境下能够访问Node及Native API
  • renderer process:渲染器进程环境下能够访问Browser API和Node API及一部分Native API。

主进程和渲染进程

在Electron应用中,经过执行package.json中的main字段所指向的文件,能够开启electron的主进程(main process)。在主进程中使用BrowserWindow 实例建立web页面,并且一个electron应用有且只能有一个主进程。
主进程通常用于:github

  • 多窗体管理(建立/切换)
  • 应用生命周期管理
  • 做为进程通讯基站(IPC Server)
  • 工具条菜单栏注册

因为electron使用Chromium来展现web页面,Chromium多进程架构也会被用到。每一张web页面都运行在它本身的进程里,该进程称为渲染进程(renderer process)。渲染进程通常负责界面交互相关的,具体的业务功能。

在web页面里,调用系统底层的API是不被容许的,这是由于在web页面上处理底层GUI资源是很是危险的,很容易致使资源泄漏。若是你想要在web页面上执行GUI操做,相应web页面的渲染进程必须与主进程进行通讯,向主进程发起请求去执行那些操做.在electron中,有几种主进程与渲染进程通讯的方法,好比用ipcRenderer和ipcMain模块来发送信息,还有RPC风格的远程通讯模块。关于Electron进程间通信,这里不作过多的介绍,能够看Electron官网和网上资料来学习主进程和渲染进程间通信。

更多的了解能够参考Electron应用架构

编辑器Monaco Editor

微软以前有个项目叫作Monaco Workbench,后来这个项目变成了VSCode,而Monaco Editor(下文简称monaco)就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码(monaco-editor-core)都是共用的,因此monaco和VSCode在编辑代码,交互以及UI上几乎是一摸同样的,有点不一样的是,二者的平台不同,monaco基于浏览器,而VSCode基于electron,因此功能上VSCode更加健全,而且性能比较强大。

TypeScript

TypeScript是JavaScript类型的超集,它能够编译成纯JavaScript。TypeScript能够在任何浏览器、任何计算机和任何操做系统中运行,而且是开源的。TypeScript具备如下特色:

  • 类型批注和编译时的类型检查
  • 强类型语言
  • 面向对象
  • 类class
  • 接口
  • lambda函数
  • 泛型

VSCode源码的编写主要用TypeScript,因此学习VSCode源码的时候仍是先对TypeScript的基本使用有所了解。

以上内容是学习VSCode源码所要了解的基本内容,能够先学习Electron作个简单的桌面应用,而后学习一下TypeScript的基本语法,就能够开始VSCode源码的学习。

VSCode架构

VSCode中包含主进程,渲染进程,同时由于VSCode提供了插件的扩展能力,又出于安全稳定性的考虑,图中又多了一个Extension Host,其实这个Extension Host也是一个独立的进程,用于运行咱们的插件代码。而且同渲染进程同样,彼此都是独立互不影响的。Extension Host Process暴露了一些VSCode的API供插件开发者去使用。

VSCode的进程结构

VSCode采用多进程架构,启动后主要由下面几个进程:

  • 后台进程
  • 编辑器窗口 - 由后台进程启动,也是多进程架构

    • HTML编写的UI

      • ActivityBar
      • SideBar
      • Panel
      • Editor
      • StatusBar
    • Nodejs异步IO

      • FileService
      • ConfigurationService
    • 插件宿主进程

      • 插件实例

        • 插件子进程 - 如TS语言服务
      • 插件实例
      • 插件实例
    • Debug进程
    • Search进程

后台进程

后台进程是 VSCode 的入口,主要负责管理编辑器生命周期,进程间通讯,自动更新,菜单管理等。

咱们启动 VSCode 的时候,后台进程会首先启动,读取各类配置信息和历史记录,而后将这些信息和主窗口 UI 的 HTML 主文件路径整合成一个 URL,启动一个浏览器窗口来显示编辑器的 UI。后台进程会一直关注 UI 进程的状态,当全部 UI 进程被关闭的时候,整个编辑器退出。

此外后台进程还会开启一个本地的 Socket,当有新的 VSCode 进程启动的时候,会尝试链接这个 Socket,并将启动的参数信息传递给它,由已经存在的 VSCode 来执行相关的动做,这样可以保证 VSCode 的惟一性,避免出现多开文件夹带来的问题。

编辑器窗口

编辑器窗口进程负责整个 UI 的展现。也就是咱们所见的部分。UI 所有用 HTML 编写没有太多须要介绍的部分。

Nodejs异步IO

项目文件的读取和保存由主进程的 NodeJS API 完成,由于所有是异步操做,即使有比较大的文件,也不会对 UI 形成阻塞。IO 跟 UI 在一个进程,并采用异步操做,在保证 IO 性能的基础上也保证了 UI 的响应速度。

插件进程

每个 UI 窗口会启动一个 NodeJS 子进程做为插件的宿主进程。全部的插件会共同运行在这个进程中。这样设计最主要的目的就是避免复杂的插件系统阻塞 UI 的响应。可是将插件放在一个单独进程也有很明显的缺点,由于是一个单独的进程,而不是 UI 进程,因此没有办法直接访问 DOM 树,想要实时高效的改变 UI 变得很难,在 VSCode 的扩展体系中几乎没有对 UI 进行扩展的 API。

Debug进程

Debugger 插件跟普通的插件有一点区别,它不运行在插件进程中,而是在每次 debug 的时候由UI单独新开一个进程。

搜索进程

搜索是一个十分耗时的任务,VSCode 也使用的单独的进程来实现这个功能,保证主窗口的效率。将耗时的任务分到多个进程中,有效的保证了主进程的响应速度。

VSCode源码运行

环境安装

  • Git
  • Node.JS,version >= 10.16.0, < 11.0.0
  • Yarn
  • Python,版本2.7以上,不支持3.0及其以上版本(mac电脑自带python不须要下载)

以上环境安装相信你们都轻车熟路了,因为我电脑使用的Mac,因此相关的示例都在Mac系统中运行,windows上的大同小异,具体能够参考官网Wiki文档

源码下载

VSCode的源码每次更新都会优化UI部分,但总体架构是没有差异的,可能网上的关于VSCode的源码教程用的老版本的VSCode,在这里我采用目前最新的版本 - v1.39.2版原本讲解。

源码下载:VSCode Releases

下载后解压用VSCode编辑器打开,在命令行中输入 yarn 命令来安装依赖,中间会很耗时。


中间会安装不少依赖包,若是发现网络不通、下载失败等状况,首先须要检查上述开发环境版本是否正确,必要时须要科学上网。

源码运行

依赖安装完成后,进入到项目中,执行 yarn watch 执行构建工做:


直到你看到 Finished compilation with 0 errors after 108726 ms 输出,说明构建成功了!


这时候不要关闭当前命令行,构建命令没有退出,它会监视vscode源码文件的变化,若是有变化,它会立刻执行增量的构建,实时反映源码变化的结果。

新起一个命令行,执行 ./scripts/code.sh ,windows下执行 scriptscode.bat,此时会下载Electron。


下载完成后,便可运行。运行界面以下:

VSCode源码结构

总体文件目录结构以下所示:

├── build         # gulp编译构建脚本
├── extensions    # 内置插件
├── gulpfile.js   # gulp task
├── out           # 编译输出目录
├── resources     # 平台相关静态资源,图标等
├── scripts       # 工具脚本,开发/测试
├── src           # 源码目录
├── test          # 测试套件
└── product.json  # App meta信息

src下文件目录结构,以下图:

├── bootstrap-amd.js    # 子进程实际入口
├── bootstrap-fork.js   #
├── bootstrap-window.js #
├── bootstrap.js        # 子进程环境初始化
├── buildfile.js        # 构建config
├── cli.js              # CLI入口
├── main.js             # 主进程入口
├── paths.js            # AppDataPath与DefaultUserDataPath
├── typings
│   └── xxx.d.ts        # ts类型声明
└── vs
    ├── base            # 定义基础的工具方法和基础的 DOM UI 控件
    │   ├── browser     # 基础UI组件,DOM操做、交互事件、DnD等
    │   ├── common      # diff描述,markdown解析器,worker协议,各类工具函数
    │   ├── node        # Node工具函数
    │   ├── parts       # IPC协议(Electron、Node),quickopen、tree组件
    │   ├── test        # base单测用例
    │   └── worker      # Worker factory 和 main Worker(运行IDE Core:Monaco)
    ├── code            # VSCode Electron 应用的入口,包括 Electron 的主进程脚本入口
    │   ├── electron-browser # 须要 Electron 渲染器处理API的源代码(可使用 common, browser, node)
    │   ├── electron-main    # 须要Electron主进程API的源代码(可使用 common, node)
    │   ├── node        # 须要Electron主进程API的源代码(可使用 common, node)
    │   ├── test
    │   └── code.main.ts
    ├── editor          # Monaco Editor 代码编辑器:其中包含单独打包发布的 Monaco Editor 和只能在 VSCode 的使用的部分
    │   ├── browser     # 代码编辑器核心
    │   ├── common      # 代码编辑器核心
    │   ├── contrib     # vscode 与独立 IDE共享的代码
    │   ├── standalone  # 独立 IDE 独有的代码
    │   ├── test
    │   ├── editor.all.ts
    │   ├── editor.api.ts
    │   ├── editor.main.ts
    │   └── editor.worker.ts
    ├── platform        # 依赖注入的实现和 VSCode 使用的基础服务 Services
    ├── workbench       # VSCode 桌面应用程序工做台的实现
    ├── buildunit.json
    ├── css.build.js    # 用于插件构建的CSS loader
    ├── css.js          # CSS loader
    ├── loader.js       # AMD loader(用于异步加载AMD模块,相似于require.js)
    ├── nls.build.js    # 用于插件构建的 NLS loader
    └── nls.js          # NLS(National Language Support)多语言loader

首先 VSCode 总体由其核心core和内置的扩展Extensions组成,core是实现了基本的代码编辑器和 VSCode 桌面应用程序,即 VSCode workbench,同时提供扩展 API,容许内置的扩展和第三方开发的扩展程序来扩展 VSCode Core 的能力。

其次,因为 VSCode 依赖 Electron,而 Electron 存在着主进程和渲染进程,它们能使用的 API 有所不到,因此 VSCode Core 中每一个目录的组织也按照它们能使用的 API 来组织安排。在 Core 下的每一个子目录下,按照代码所运行的目标环境分为如下几类:

  • common: 只使用 JavaScript API 的源代码,可能运行在任何环境
  • browser: 须要使用浏览器提供的 API 的源代码,如 DOM 操做等
  • node: 须要使用Node.js提供的 API 的源代码
  • electron-browser: 须要使用 Electron 渲染进程 API 的源代码
  • electron-main: 须要使用 Electron 主进程 API 的源代码

按照上述规则,即src/vs/workbench/browser中的源代码只能使用基本的 JavaScript API 和浏览器提供的 API,而src/vs/workbench/electron-browser中的源代码则可使用 JavaScript API,浏览器提供的 API、Node.js提供的 API、和 Electron 渲染进程中的 API。

在 VSCode 代码仓库中,出了上述的src/vs的Core以外,还有一大块即 VSCode 内置的扩展,它们源代码位于extensions内。

VSCode 做为代码编辑器,与各类代码编辑的功能如语法高亮、补全提示、验证等都有扩展实现的。因此在 VSCode 的内置扩展内,一大部分都是各类编程语言的支持扩展,如:extensionshtml、extensionsjavascript、extensionscpp等等,大部分语言扩展中都会出现如.tmTheme、.tmLanguage等 TextMate 的语法定义。还有一类内置的扩展是 VSCode 主体扩展,如 VSCode 默认主体extensions/theme-defaults等。

VSCode启动流程

因为VSCode是基于Electron开发的,Electron的启动入口在package.json中,其中的 main 字段所表示的脚本为应用的启动脚本,它将会在主进程中执行。


./out/main.js显然这就是主进程的入口程序,可是main.js是在out文件夹下,很明显是编译输出出来的,而后找到src下tsconfig.json文件中有如下配置:

"outDir": "../out",

因此很明显是将src下代码编译后输出到out文件夹中。因此真实入口在src下main.js中,接下来只需从main.js文件分析便可。

在main.js中,咱们能够看到下面一行引入

const app = require('electron').app;

electron.app负责管理Electron 应用程序的生命周期,运行在主进程中,而后找到 ready 监听事件

// Load our code once ready
app.once('ready', function () {
    if (args['trace']) {
        // @ts-ignore
        const contentTracing = require('electron').contentTracing;

        const traceOptions = {
            categoryFilter: args['trace-category-filter'] || '*',
            traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
        };

        contentTracing.startRecording(traceOptions, () => onReady());
    } else {
        onReady();
    }
});

这个ready监听表示,Electron 会在初始化后并准备,部分 API 在 ready 事件触发后才能使用。建立窗口也须要在ready后建立。最后这个函数中调用 onReady() 函数。

function onReady() {
    perf.mark('main:appReady');

    Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => {
        if (locale && !nlsConfiguration) {
            nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
        }

        if (!nlsConfiguration) {
            nlsConfiguration = Promise.resolve(undefined);
        }

        // First, we need to test a user defined locale. If it fails we try the app locale.
        // If that fails we fall back to English.
        nlsConfiguration.then(nlsConfig => {

            const startup = nlsConfig => {
                nlsConfig._languagePackSupport = true;
                process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
                process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';

                // Load main in AMD
                perf.mark('willLoadMainBundle');
                require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
                    perf.mark('didLoadMainBundle');
                });
            };

            // We received a valid nlsConfig from a user defined locale
            if (nlsConfig) {
                startup(nlsConfig);
            }

            // Try to use the app locale. Please note that the app locale is only
            // valid after we have received the app ready event. This is why the
            // code is here.
            else {
                let appLocale = app.getLocale();
                if (!appLocale) {
                    startup({ locale: 'en', availableLanguages: {} });
                } else {

                    // See above the comment about the loader and case sensitiviness
                    appLocale = appLocale.toLowerCase();

                    lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => {
                        if (!nlsConfig) {
                            nlsConfig = { locale: appLocale, availableLanguages: {} };
                        }

                        startup(nlsConfig);
                    });
                }
            }
        });
    }, console.error);
}

整个函数读取了用户语言设置,而后最终调用了 startup()

const startup = nlsConfig => {
    nlsConfig._languagePackSupport = true;
    process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
    process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';

    // Load main in AMD
    perf.mark('willLoadMainBundle');
    require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
        perf.mark('didLoadMainBundle');
    });
};

startup中主要是引入了 boostrap-amd ,这个bootstrap-amd引入了/vs/loader,并建立了一个loader。

const loader = require('./vs/loader');

loader是微软自家的AMD模块加载开源项目:https://github.com/Microsoft/...

而后经过loader加载 vs/code/electron-main/main 模块,这是 VSCode 真正的入口,而后在 vs/code/electron-main/main.ts 中能够看到定义了一个 CodeMain 类,而后初始化这个CodeMain类,并调用了 main 函数。

// src/vs/code/electron-main/main
class CodeMain {
    main(): void {
        ...
        // Launch
        this.startup(args);
    }

    private async startup(args: ParsedArgs): Promise<void> {

        // We need to buffer the spdlog logs until we are sure
        // we are the only instance running, otherwise we'll have concurrent
        // log file access on Windows (https://github.com/Microsoft/vscode/issues/41218)
        const bufferLogService = new BufferLogService();

        const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService);
        try {

            // Init services
            await instantiationService.invokeFunction(async accessor => {
                const environmentService = accessor.get(IEnvironmentService);
                const configurationService = accessor.get(IConfigurationService);
                const stateService = accessor.get(IStateService);

                try {
                    await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
                } catch (error) {

                    // Show a dialog for errors that can be resolved by the user
                    this.handleStartupDataDirError(environmentService, error);

                    throw error;
                }
            });

            // Startup
            await instantiationService.invokeFunction(async accessor => {
                const environmentService = accessor.get(IEnvironmentService);
                const logService = accessor.get(ILogService);
                const lifecycleMainService = accessor.get(ILifecycleMainService);
                const configurationService = accessor.get(IConfigurationService);

                const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleMainService, instantiationService, true);

                bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
                once(lifecycleMainService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose());

                return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
            });
        } catch (error) {
            instantiationService.invokeFunction(this.quit, error);
        }
    }

    private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] {
        const services = new ServiceCollection();

        const environmentService = new EnvironmentService(args, process.execPath);
        const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
        services.set(IEnvironmentService, environmentService);

        const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
        process.once('exit', () => logService.dispose());
        services.set(ILogService, logService);

        services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
        services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));
        services.set(IStateService, new SyncDescriptor(StateService));
        services.set(IRequestService, new SyncDescriptor(RequestMainService));
        services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
        services.set(ISignService, new SyncDescriptor(SignService));

        return [new InstantiationService(services, true), instanceEnvironment];
    }
    ...
}

// Main Startup
const code = new CodeMain();
code.main();

能够看到 main() 函数最终调用了 startup() 函数。

startup() 函数中,先调用了 this.createServices() 函数来建立依赖的Services。
Services(服务) 是 VSCode 中一系列能够被注入的公共模块,这些 Services 分别负责不一样的功能,在这里建立了几个基本服务。除了这些基本服务,VSCode 内还包含了大量的服务,如 IModeService、ICodeEditorService、IPanelService 等,经过 VSCode 实现的「依赖注入」模式,能够在须要用到这些服务的地方以 Decorator 的方式作为构造函数参数声明依赖,会被自动注入到类中。关于服务的依赖注入,后面的章节会重点讲解。

private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] {
        const services = new ServiceCollection();

        const environmentService = new EnvironmentService(args, process.execPath);
        const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
        // environmentService 一些基本配置,包括运行目录、用户数据目录、工做区缓存目录等
        services.set(IEnvironmentService, environmentService);

        const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
        process.once('exit', () => logService.dispose());
        // logService 日志服务
        services.set(ILogService, logService);

        // ConfigurationService 配置项
        services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
        // LifecycleService 生命周期相关的一些方法
        services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));
        // StateService 持久化数据
        services.set(IStateService, new SyncDescriptor(StateService));
        // RequestService 请求服务
        services.set(IRequestService, new SyncDescriptor(RequestMainService));
        services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
        services.set(ISignService, new SyncDescriptor(SignService));

        return [new InstantiationService(services, true), instanceEnvironment];
    }

代码中能够看到 createServices() 最终实例化了一个 InstantiationService 实例并return回去,而后在 startup() 中调用 InstantiationService的createInstance方法并传参数CodeApplication,表示初始化CodeApplication实例,而后调用实例的 startup() 方法。

return instantiationService.createInstance(CodeApplication, mainIpcServer,instanceEnvironment).startup();

接下来咱们去看CodeApplication中的startup方法。

//src/vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
    ...
    async startup(): Promise<void> {
        this.logService.debug('Starting VS Code');
        this.logService.debug(`from: ${this.environmentService.appRoot}`);
        this.logService.debug('args:', this.environmentService.args);

        // Make sure we associate the program with the app user model id
        // This will help Windows to associate the running program with
        // any shortcut that is pinned to the taskbar and prevent showing
        // two icons in the taskbar for the same app.
        const win32AppUserModelId = product.win32AppUserModelId;
        if (isWindows && win32AppUserModelId) {
            app.setAppUserModelId(win32AppUserModelId);
        }

        // Fix native tabs on macOS 10.13
        // macOS enables a compatibility patch for any bundle ID beginning with
        // "com.microsoft.", which breaks native tabs for VS Code when using this
        // identifier (from the official build).
        // Explicitly opt out of the patch here before creating any windows.
        // See: https://github.com/Microsoft/vscode/issues/35361#issuecomment-399794085
        try {
            if (isMacintosh && this.configurationService.getValue<boolean>('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) {
                systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any);
            }
        } catch (error) {
            this.logService.error(error);
        }

        // Create Electron IPC Server
        const electronIpcServer = new ElectronIPCServer();

        // Resolve unique machine ID
        this.logService.trace('Resolving machine identifier...');
        const { machineId, trueMachineId } = await this.resolveMachineId();
        this.logService.trace(`Resolved machine identifier: ${machineId} (trueMachineId: ${trueMachineId})`);

        // Spawn shared process after the first window has opened and 3s have passed
        const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
        const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
        this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
            this._register(new RunOnceScheduler(async () => {
                const userEnv = await getShellEnvironment(this.logService, this.environmentService);

                sharedProcess.spawn(userEnv);
            }, 3000)).schedule();
        });

        // Services
        const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);

        // Create driver
        if (this.environmentService.driverHandle) {
            const server = await serveDriver(electronIpcServer, this.environmentService.driverHandle!, this.environmentService, appInstantiationService);

            this.logService.info('Driver started at:', this.environmentService.driverHandle);
            this._register(server);
        }

        // Setup Auth Handler
        this._register(new ProxyAuthHandler());

        // Open Windows
        const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));

        // Post Open Windows Tasks
        this.afterWindowOpen();

        // Tracing: Stop tracing after windows are ready if enabled
        if (this.environmentService.args.trace) {
            this.stopTracingEventually(windows);
        }
    }

    ...

    private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {

        // Register more Main IPC services
        const launchMainService = accessor.get(ILaunchMainService);
        const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true });
        this.mainIpcServer.registerChannel('launch', launchChannel);

        // Register more Electron IPC services
        const updateService = accessor.get(IUpdateService);
        const updateChannel = new UpdateChannel(updateService);
        electronIpcServer.registerChannel('update', updateChannel);

        const issueService = accessor.get(IIssueService);
        const issueChannel = createChannelReceiver(issueService);
        electronIpcServer.registerChannel('issue', issueChannel);

        const electronService = accessor.get(IElectronService);
        const electronChannel = createChannelReceiver(electronService);
        electronIpcServer.registerChannel('electron', electronChannel);
        sharedProcessClient.then(client => client.registerChannel('electron', electronChannel));

        const sharedProcessMainService = accessor.get(ISharedProcessMainService);
        const sharedProcessChannel = createChannelReceiver(sharedProcessMainService);
        electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel);

        const workspacesService = accessor.get(IWorkspacesService);
        const workspacesChannel = createChannelReceiver(workspacesService);
        electronIpcServer.registerChannel('workspaces', workspacesChannel);

        const menubarService = accessor.get(IMenubarService);
        const menubarChannel = createChannelReceiver(menubarService);
        electronIpcServer.registerChannel('menubar', menubarChannel);

        const urlService = accessor.get(IURLService);
        const urlChannel = createChannelReceiver(urlService);
        electronIpcServer.registerChannel('url', urlChannel);

        const storageMainService = accessor.get(IStorageMainService);
        const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
        electronIpcServer.registerChannel('storage', storageChannel);

        const loggerChannel = new LoggerChannel(accessor.get(ILogService));
        electronIpcServer.registerChannel('logger', loggerChannel);
        sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));

        // ExtensionHost Debug broadcast service
        electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());

        // Signal phase: ready (services set)
        this.lifecycleMainService.phase = LifecycleMainPhase.Ready;

        // Propagate to clients
        const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
        this.dialogMainService = accessor.get(IDialogMainService);

        // Create a URL handler to open file URIs in the active window
        const environmentService = accessor.get(IEnvironmentService);
        urlService.registerHandler({
            async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {

                // Catch file URLs
                if (uri.authority === Schemas.file && !!uri.path) {
                    const cli = assign(Object.create(null), environmentService.args);
                    const urisToOpen = [{ fileUri: URI.file(uri.fsPath) }];

                    windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true });

                    return true;
                }

                return false;
            }
        });

        // Create a URL handler which forwards to the last active window
        const activeWindowManager = new ActiveWindowManager(electronService);
        const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
        const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter);
        const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter);
        const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel);

        // On Mac, Code can be running without any open windows, so we must create a window to handle urls,
        // if there is none
        if (isMacintosh) {
            urlService.registerHandler({
                async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
                    if (windowsMainService.getWindowCount() === 0) {
                        const cli = { ...environmentService.args };
                        const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true, gotoLineMode: true });

                        await window.ready();

                        return urlService.open(uri);
                    }

                    return false;
                }
            });
        }

        // Register the multiple URL handler
        urlService.registerHandler(multiplexURLHandler);

        // Watch Electron URLs and forward them to the UrlService
        const args = this.environmentService.args;
        const urls = args['open-url'] ? args._urls : [];
        const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService);
        this._register(urlListener);

        // Open our first window
        const macOpenFiles: string[] = (<any>global).macOpenFiles;
        const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
        const hasCliArgs = args._.length;
        const hasFolderURIs = !!args['folder-uri'];
        const hasFileURIs = !!args['file-uri'];
        const noRecentEntry = args['skip-add-to-recently-opened'] === true;
        const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;

        // new window if "-n" was used without paths
        if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
            return windowsMainService.open({
                context,
                cli: args,
                forceNewWindow: true,
                forceEmpty: true,
                noRecentEntry,
                waitMarkerFileURI,
                initialStartup: true
            });
        }

        // mac: open-file event received on startup
        if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
            return windowsMainService.open({
                context: OpenContext.DOCK,
                cli: args,
                urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)),
                noRecentEntry,
                waitMarkerFileURI,
                gotoLineMode: false,
                initialStartup: true
            });
        }

        // default: read paths from cli
        return windowsMainService.open({
            context,
            cli: args,
            forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
            diffMode: args.diff,
            noRecentEntry,
            waitMarkerFileURI,
            gotoLineMode: args.goto,
            initialStartup: true
        });
    }
    ...
}

在CodeApplication.startup 中首先会启动 SharedProcess 共享进程,同时也建立了一些窗口相关的服务,包括 WindowsManager、WindowsService、MenubarService 等,负责窗口、多窗口管理及菜单等功能。而后调用 openFirstWindow 方法来开启窗口。

在openFirstWindow中,先建立一系列 Electron 的 IPC 频道,用于主进程和渲染进程间通讯,其中 window 和 logLevel 频道还会被注册到 sharedProcessClient ,sharedProcessClient 是主进程与共享进程(SharedProcess)进行通讯的 client,以后根据 environmentService 提供的相关参数(file_uri、folder_uri)调用了 windowsMainService.open 方法。

windowsMainService是WindowsManager实例化的服务,而WindowsManager是多窗体管理类(src/vs/code/electron-main/windows.ts)。接下来咱们看windowsMainService.open 方法,能够看到其调用了doOpen方法。

open(openConfig: IOpenConfiguration): ICodeWindow[] {
        ...

        // Open based on config
        const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, 
        ...
    }

doOpen方法最终调用了openInBrowserWindow方法。

private doOpen(
        openConfig: IOpenConfiguration,
        workspacesToOpen: IWorkspacePathToOpen[],
        foldersToOpen: IFolderPathToOpen[],
        emptyToRestore: IEmptyWindowBackupInfo[],
        emptyToOpen: number,
        fileInputs: IFileInputs | undefined,
        foldersToAdd: IFolderPathToOpen[]
    ) {
        const usedWindows: ICodeWindow[] = [];

        ...

        // Handle empty to open (only if no other window opened)
        if (usedWindows.length === 0 || fileInputs) {
            if (fileInputs && !emptyToOpen) {
                emptyToOpen++;
            }

            const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined);

            for (let i = 0; i < emptyToOpen; i++) {
                usedWindows.push(this.openInBrowserWindow({
                    userEnv: openConfig.userEnv,
                    cli: openConfig.cli,
                    initialStartup: openConfig.initialStartup,
                    remoteAuthority,
                    forceNewWindow: openFolderInNewWindow,
                    forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
                    fileInputs
                }));

                // Reset these because we handled them
                fileInputs = undefined;
                openFolderInNewWindow = true; // any other window to open must open in new window then
            }
        }

        return arrays.distinct(usedWindows);
    }

在openInBrowserWindow中,建立一个CodeWindow实例并返回,而且还调用了doOpenInBrowserWindow这个方法,这个方法看下文介绍。

private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {

        ...

        // Create the window
        window = this.instantiationService.createInstance(CodeWindow, {
            state,
            extensionDevelopmentPath: configuration.extensionDevelopmentPath,
            isExtensionTestHost: !!configuration.extensionTestsPath
        });
        ...

        // If the window was already loaded, make sure to unload it
        // first and only load the new configuration if that was
        // not vetoed
        if (window.isReady) {
            this.lifecycleMainService.unload(window, UnloadReason.LOAD).then(veto => {
                if (!veto) {
                    this.doOpenInBrowserWindow(window!, configuration, options);
                }
            });
        } else {
            this.doOpenInBrowserWindow(window, configuration, options);
        }

        return window;
    }

接下来咱们找到CodeWindow定义在src/vs/code/electron-main/window.ts中,在CodeWindow的构造函数中调用了createBrowserWindow方法,而后在createBrowserWindow方法中看到实例化了一个BrowserWindow,这是Electron中浏览器窗口的定义。

//src/vs/code/electron-main/window.ts
export class CodeWindow extends Disposable implements ICodeWindow {

    ...

    constructor(
        config: IWindowCreationOptions,
        @ILogService private readonly logService: ILogService,
        @IEnvironmentService private readonly environmentService: IEnvironmentService,
        @IFileService private readonly fileService: IFileService,
        @IConfigurationService private readonly configurationService: IConfigurationService,
        @IThemeMainService private readonly themeMainService: IThemeMainService,
        @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
        @IBackupMainService private readonly backupMainService: IBackupMainService,
    ) {
        super();

        this.touchBarGroups = [];
        this._lastFocusTime = -1;
        this._readyState = ReadyState.NONE;
        this.whenReadyCallbacks = [];

        // create browser window
        this.createBrowserWindow(config);

        // respect configured menu bar visibility
        this.onConfigurationUpdated();

        // macOS: touch bar support
        this.createTouchBar();

        // Request handling
        this.handleMarketplaceRequests();

        // Eventing
        this.registerListeners();
    }

    private createBrowserWindow(config: IWindowCreationOptions): void {

        ...
        // Create the browser window.
        this._win = new BrowserWindow(options);
        ...
    }
}

如今窗口有了,那么何时加载页面呢?刚刚咱们在上文提到,在openInBrowserWindow中,建立一个CodeWindow实例并返回,而且还调用了doOpenInBrowserWindow这个方法,那么咱们看一下这个方法的定义。

private doOpenInBrowserWindow(window: ICodeWindow, configuration: IWindowConfiguration, options: IOpenBrowserWindowOptions): void {
        ...
        // Load it
        window.load(configuration);
        ...
    }

这个方法有调用CodeWindow的load方法,而后看一下load方法的定义。会看到调用了this._win.loadURL,这个this._win就是CodeWindow建立的BrowserWindow窗口,这就找到了窗口加载的URL时机。

load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
    ...
    // Load URL
    perf.mark('main:loadWindow');
    this._win.loadURL(this.getUrl(configuration));
    ...
}

而后看一下getUrl方法的定义,最终返回的configUrl是调用doGetUrl获取的。

private getUrl(windowConfiguration: IWindowConfiguration): string {

        ...
        let configUrl = this.doGetUrl(config);
        ...

        return configUrl;
    }

而后看一下doGetUrl方法,能够看到返回的Url路径为vs/code/electron-browser/workbench/workbench.html。

private doGetUrl(config: object): string {
    return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}

这是整个 Workbench 的入口,HTML出现了,主进程的使命完成,渲染进程登场。

//src/vs/code/electron-browser/workbench/workbench.html
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' https://*.vscode-webview-test.com; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
    </head>
    <body class="vs-dark" aria-label="">
    </body>

    <!-- Startup via workbench.js -->
    <script src="workbench.js"></script>
</html>

workbench.html中加载了workbench.js文件,这个文件负责加载真正的 Workbench 模块并调用其 main 方法初始化主界面。

// src/vs/code/electron-browser/workbench/workbench.js
const bootstrapWindow = require('../../../../bootstrap-window');

// Setup shell environment
process['lazyEnv'] = getLazyEnv();

// Load workbench main JS, CSS and NLS all in parallel. This is an
// optimization to prevent a waterfall of loading to happen, because
// we know for a fact that workbench.desktop.main will depend on
// the related CSS and NLS counterparts.
bootstrapWindow.load([
    'vs/workbench/workbench.desktop.main',
    'vs/nls!vs/workbench/workbench.desktop.main',
    'vs/css!vs/workbench/workbench.desktop.main'
],
    function (workbench, configuration) {
        perf.mark('didLoadWorkbenchMain');

        return process['lazyEnv'].then(function () {
            perf.mark('main/startup');

            // @ts-ignore
            //加载 Workbench 并初始化主界面
            return require('vs/workbench/electron-browser/desktop.main').main(configuration);
        });
    }, {
        removeDeveloperKeybindingsAfterLoad: true,
        canModifyDOM: function (windowConfig) {
            showPartsSplash(windowConfig);
        },
        beforeLoaderConfig: function (windowConfig, loaderConfig) {
            loaderConfig.recordStats = true;
        },
        beforeRequire: function () {
            perf.mark('willLoadWorkbenchMain');
        }
    });

咱们能够看到加载了vs/workbench/electron-browser/desktop.main模块,并调用了模块的main方法。main方法中实例化了一个DesktopMain,并调用了DesktopMain的open方法。

class DesktopMain extends Disposable {

    async open(): Promise<void> {
        ...

        // Create Workbench
        const workbench = new Workbench(document.body, services.serviceCollection, services.logService);

        // Listeners
        this.registerListeners(workbench, services.storageService);

        // Startup
        const instantiationService = workbench.startup();
        ...
    }
    ...
}

export function main(configuration: IWindowConfiguration): Promise<void> {
    const renderer = new DesktopMain(configuration);
    return renderer.open();
}

咱们看到DesktopMain的open方法中实例化了Workbench类,并调用了Workbench的startup方法。接下来咱们看一下这个Workbench类。

export class Workbench extends Layout {

    ...
    startup(): IInstantiationService {
        try {

            // Configure emitter leak warning threshold
            setGlobalLeakWarningThreshold(175);

            // ARIA
            setARIAContainer(document.body);

            // Services
            const instantiationService = this.initServices(this.serviceCollection);

            instantiationService.invokeFunction(async accessor => {
                const lifecycleService = accessor.get(ILifecycleService);
                const storageService = accessor.get(IStorageService);
                const configurationService = accessor.get(IConfigurationService);

                // Layout
                this.initLayout(accessor);

                // Registries
                this.startRegistries(accessor);

                // Context Keys
                this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));

                // Register Listeners
                this.registerListeners(lifecycleService, storageService, configurationService);

                // Render Workbench
                this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);

                // Workbench Layout
                this.createWorkbenchLayout(instantiationService);

                // Layout
                this.layout();

                // Restore
                try {
                    await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
                } catch (error) {
                    onUnexpectedError(error);
                }
            });

            return instantiationService;
        } catch (error) {
            onUnexpectedError(error);

            throw error; // rethrow because this is a critical issue we cannot handle properly here
        }
    }
    ...
}

咱们能够看到Workbench继承Layout布局类,在 workbench.startup 方法中构建主界面布局、建立全局事件监听以及实例化一些依赖的服务,所有完成后会还原以前打开的编辑器,整个 Workbench 加载完成。

因此前文中的大量代码只是为这里最终建立主界面作铺垫,Workbench 模块主要代码都在 vs/workbench 目录下,主要负责界面元素的建立和具体业务功能的实现。

至此,从启动到加载到html,再到构建主界面布局,整个流程很清晰。

应用信息定制

在VSCode源码根目录下有一个product.json文件,此文件用于配置应用的信息。

{
    "nameShort": "Code - OSS",
    "nameLong": "Code - OSS",
    "applicationName": "code-oss",
    "dataFolderName": ".vscode-oss",
    "win32MutexName": "vscodeoss",
    "licenseName": "MIT",
    "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt",
    "win32DirName": "Microsoft Code OSS",
    "win32NameVersion": "Microsoft Code OSS",
    "win32RegValueName": "CodeOSS",
    "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}",
    "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
    "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}",
    "win32x64UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}",
    "win32AppUserModelId": "Microsoft.CodeOSS",
    "win32ShellNameShort": "C&ode - OSS",
    "darwinBundleIdentifier": "com.visualstudio.code.oss",
    "linuxIconName": "com.visualstudio.code.oss",
    "licenseFileName": "LICENSE.txt",
    "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new",
    "urlProtocol": "code-oss",
    "extensionAllowedProposedApi": [
        "ms-vscode.references-view"
    ]
}

能够修改product.json的信息来更新定制VSCode的名称等信息。若是你在执行了./scripts/code.sh后修改了product.json的信息,好比修改了nameLong的配置,这时候从新运行./scripts/code.sh会报错。

错误信息是 ./scripts/code.sh: line 53: /Users/jiangshuaijie/Desktop/vscode-1.39.2/.build/electron/test.app/Contents/MacOS/Electron: No such file or directory ,能够看出是在code.sh中报错了,看一下code.sh中内容。

...
function code() {
    cd "$ROOT"
    if [[ "$OSTYPE" == "darwin"* ]]; then
        NAME=`node -p "require('./product.json').nameLong"`
        CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron"
    else
        NAME=`node -p "require('./product.json').applicationName"`
        CODE=".build/electron/$NAME"
    fi
    ...
    # Launch Code
    exec "$CODE" . "$@"
}
...

最终根据product.json中的nameLong来运行根目录下.build/electron/下生成的app,这时候的应用是以前生成过的,因此会报错。咱们只需删除掉根目录下.build文件夹,从新执行./scripts/code.sh便可。

应用图标定制

在VSCode源码根目录下resources文件夹主要用于存放VSCode平台的静态资源,例如应用图标等。

其中darwin、linux、win32对应三个不一样的平台,能够在不一样平台文件夹下替换图片资源。

更多精彩请关注 https://codeteenager.github.i... ,后面会更新更多内容。