├── README.md ├── all.md ├── chapter-1.md ├── chapter-2.md ├── chapter-3.md ├── chapter-4.md ├── chapter-5.md ├── chapter-6.md ├── vscode-debugg.png ├── vscode-framework.png ├── vscode-source.png ├── vscode-vside-zh.png ├── vscode-vside.png └── vscode-welcome.png /README.md: -------------------------------------------------------------------------------- 1 | 2 | # VSCode源码分析 3 | 4 | ## 目录 5 | * 总览 6 | * (一)启动主进程 7 | * (二)实例化服务 8 | * (三)事件分发 9 | * (四)进程通信 10 | * (五)主要窗口 11 | * (六)开发调试 12 | 13 | ## 简介 14 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。 15 | 16 | git仓库地址: https://github.com/microsoft/vscode 17 | 18 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。 19 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用 20 | 21 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。 22 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-vside-zh.png) 23 | 24 | ## 编译安装 25 | 下载最新版本,目前我用的是1.37.1版本 26 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank) 27 | 28 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档, 29 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。 30 | 31 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行) 32 | 33 | 34 | 35 | ## 技术架构 36 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-framework.png) 37 | 38 | ### Electron 39 | 40 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API) 41 | 42 | ### Monaco Editor 43 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。 44 | 45 | ### TypeScript 46 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程 47 | 48 | ## 目录结构 49 | ``` 50 | ├── build # gulp编译构建脚本 51 | ├── extensions # 内置插件 52 | ├── product.json # App meta信息 53 | ├── resources # 平台相关静态资源 54 | ├── scripts # 工具脚本,开发/测试 55 | ├── src # 源码目录 56 | └── typings # 函数语法补全定义 57 | └── vs 58 | ├── base # 通用工具/协议和UI库 59 | │ ├── browser # 基础UI组件,DOM操作 60 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数 61 | │ ├── node # Node工具函数 62 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件 63 | │ ├── test # base单测用例 64 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco) 65 | ├── code # VSCode主运行窗口 66 | ├── editor # IDE代码编辑器 67 | | ├── browser # 代码编辑器核心 68 | | ├── common # 代码编辑器核心 69 | | ├── contrib # vscode 与独立 IDE共享的代码 70 | | └── standalone # 独立 IDE 独有的代码 71 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏) 72 | ├── workbench # 工作区UI布局,功能主界面 73 | │ ├── api # 74 | │ ├── browser # 75 | │ ├── common # 76 | │ ├── contrib # 77 | │ ├── electron-browser # 78 | │ ├── services # 79 | │ └── test # 80 | ├── css.build.js # 用于插件构建的CSS loader 81 | ├── css.js # CSS loader 82 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持 83 | ├── loader.js # AMD loader(用于异步加载AMD模块) 84 | ├── nls.build.js # 用于插件构建的NLS loader 85 | └── nls.js # NLS(National Language Support)多语言loader 86 | ``` 87 | ### 核心层 88 | * base: 提供通用服务和构建用户界面 89 | * platform: 注入服务和基础服务代码 90 | * editor: 微软Monaco编辑器,也可独立运行使用 91 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序 92 | 93 | ### 核心环境 94 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排, 95 | 运行的环境分为几类: 96 | * common: 只使用javascritp api的代码,能在任何环境下运行 97 | * browser: 浏览器api, 如操作dom; 可以调用common 98 | * node: 需要使用node的api,比如文件io操作 99 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page) 100 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process) 101 | 102 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-welcome.png) 103 | -------------------------------------------------------------------------------- /all.md: -------------------------------------------------------------------------------- 1 | # VSCode源码分析 2 | ## 目录 3 | * 简介 4 | * 技术架构 5 | * 启动主进程 6 | * 实例化服务 7 | * 事件分发 8 | * 进程通信 9 | * 主要窗口 10 | * 开发调试 11 | 12 | ## 简介 13 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。 14 | 15 | git仓库地址: https://github.com/microsoft/vscode 16 | 17 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。 18 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用 19 | 20 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。 21 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-vside-zh.png) 22 | 23 | ## 编译安装 24 | 下载最新版本,目前我用的是1.37.1版本 25 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank) 26 | 27 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档, 28 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。 29 | 30 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行) 31 | 32 | 33 | ## 技术架构 34 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-framework.png) 35 | 36 | ### Electron 37 | 38 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API) 39 | 40 | ### Monaco Editor 41 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。 42 | 43 | ### TypeScript 44 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程 45 | 46 | ## 目录结构 47 | ``` 48 | ├── build # gulp编译构建脚本 49 | ├── extensions # 内置插件 50 | ├── product.json # App meta信息 51 | ├── resources # 平台相关静态资源 52 | ├── scripts # 工具脚本,开发/测试 53 | ├── src # 源码目录 54 | └── typings # 函数语法补全定义 55 | └── vs 56 | ├── base # 通用工具/协议和UI库 57 | │ ├── browser # 基础UI组件,DOM操作 58 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数 59 | │ ├── node # Node工具函数 60 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件 61 | │ ├── test # base单测用例 62 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco) 63 | ├── code # VSCode主运行窗口 64 | ├── editor # IDE代码编辑器 65 | | ├── browser # 代码编辑器核心 66 | | ├── common # 代码编辑器核心 67 | | ├── contrib # vscode 与独立 IDE共享的代码 68 | | └── standalone # 独立 IDE 独有的代码 69 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏) 70 | ├── workbench # 工作区UI布局,功能主界面 71 | │ ├── api # 72 | │ ├── browser # 73 | │ ├── common # 74 | │ ├── contrib # 75 | │ ├── electron-browser # 76 | │ ├── services # 77 | │ └── test # 78 | ├── css.build.js # 用于插件构建的CSS loader 79 | ├── css.js # CSS loader 80 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持 81 | ├── loader.js # AMD loader(用于异步加载AMD模块) 82 | ├── nls.build.js # 用于插件构建的NLS loader 83 | └── nls.js # NLS(National Language Support)多语言loader 84 | ``` 85 | ### 核心层 86 | * base: 提供通用服务和构建用户界面 87 | * platform: 注入服务和基础服务代码 88 | * editor: 微软Monaco编辑器,也可独立运行使用 89 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序 90 | 91 | ### 核心环境 92 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排, 93 | 运行的环境分为几类: 94 | * common: 只使用javascritp api的代码,能在任何环境下运行 95 | * browser: 浏览器api, 如操作dom; 可以调用common 96 | * node: 需要使用node的api,比如文件io操作 97 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page) 98 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process) 99 | 100 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-welcome.png) 101 | 102 | ## 启动主进程 103 | 104 | ### Electron通过package.json中的main字段来定义应用入口。 105 | main.js是vscode的入口。 106 | 107 | * src/main.js 108 | * vs/code/electron-main/main.ts 109 | * vs/code/electron-main/app.ts 110 | * vs/code/electron-main/windows.ts 111 | * vs/workbench/electron-browser/desktop.main.ts 112 | * vs/workbench/browser/workbench.ts 113 | 114 | ```js 115 | app.once('ready', function () { 116 | //启动追踪,后面会讲到,跟性能检测优化相关。 117 | if (args['trace']) { 118 | // @ts-ignore 119 | const contentTracing = require('electron').contentTracing; 120 | 121 | const traceOptions = { 122 | categoryFilter: args['trace-category-filter'] || '*', 123 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' 124 | }; 125 | 126 | contentTracing.startRecording(traceOptions, () => onReady()); 127 | } else { 128 | onReady(); 129 | } 130 | }); 131 | function onReady() { 132 | perf.mark('main:appReady'); 133 | 134 | Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => { 135 | //1. 这里尝试获取本地配置信息,如果有的话会传递到startup 136 | if (locale && !nlsConfiguration) { 137 | nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale); 138 | } 139 | 140 | if (!nlsConfiguration) { 141 | nlsConfiguration = Promise.resolve(undefined); 142 | } 143 | 144 | 145 | nlsConfiguration.then(nlsConfig => { 146 | 147 | //4. 首先会检查用户语言环境配置,如果没有设置默认使用英语 148 | const startup = nlsConfig => { 149 | nlsConfig._languagePackSupport = true; 150 | process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); 151 | process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; 152 | 153 | perf.mark('willLoadMainBundle'); 154 | //使用微软的loader组件加载electron-main/main文件 155 | require('./bootstrap-amd').load('vs/code/electron-main/main', () => { 156 | perf.mark('didLoadMainBundle'); 157 | }); 158 | }; 159 | 160 | // 2. 接收到有效的配置传入是其生效,调用startup 161 | if (nlsConfig) { 162 | startup(nlsConfig); 163 | } 164 | 165 | // 3. 这里尝试使用本地的应用程序 166 | // 应用程序设置区域在ready事件后才有效 167 | else { 168 | let appLocale = app.getLocale(); 169 | if (!appLocale) { 170 | startup({ locale: 'en', availableLanguages: {} }); 171 | } else { 172 | 173 | // 配置兼容大小写敏感,所以统一转换成小写 174 | appLocale = appLocale.toLowerCase(); 175 | // 这里就会调用config服务,把本地配置加载进来再调用startup 176 | lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => { 177 | if (!nlsConfig) { 178 | nlsConfig = { locale: appLocale, availableLanguages: {} }; 179 | } 180 | 181 | startup(nlsConfig); 182 | }); 183 | } 184 | } 185 | }); 186 | }, console.error); 187 | } 188 | ``` 189 | 190 | ### vs/code/electron-main/main.ts 191 | electron-main/main 是程序真正启动的入口,进入main process初始化流程. 192 | #### 这里主要做了两件事情: 193 | 1. 初始化Service 194 | 2. 启动主实例 195 | 196 | 直接看startup方法的实现,基础服务初始化完成后会加载 CodeApplication, mainIpcServer, instanceEnvironment,调用 startup 方法启动APP 197 | ```js 198 | private async startup(args: ParsedArgs): Promise { 199 | 200 | //spdlog 日志服务 201 | const bufferLogService = new BufferLogService(); 202 | 203 | // 1. 调用 createServices 204 | const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService); 205 | try { 206 | 207 | // 1.1 初始化Service服务 208 | await instantiationService.invokeFunction(async accessor => { 209 | // 基础服务,包括一些用户数据,缓存目录 210 | const environmentService = accessor.get(IEnvironmentService); 211 | // 配置服务 212 | const configurationService = accessor.get(IConfigurationService); 213 | // 持久化数据 214 | const stateService = accessor.get(IStateService); 215 | 216 | try { 217 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService); 218 | } catch (error) { 219 | 220 | // 抛出错误对话框 221 | this.handleStartupDataDirError(environmentService, error); 222 | 223 | throw error; 224 | } 225 | }); 226 | 227 | // 1.2 启动实例 228 | await instantiationService.invokeFunction(async accessor => { 229 | const environmentService = accessor.get(IEnvironmentService); 230 | const logService = accessor.get(ILogService); 231 | const lifecycleService = accessor.get(ILifecycleService); 232 | const configurationService = accessor.get(IConfigurationService); 233 | 234 | const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true); 235 | 236 | bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel()); 237 | once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose()); 238 | 239 | return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); 240 | }); 241 | } catch (error) { 242 | instantiationService.invokeFunction(this.quit, error); 243 | } 244 | } 245 | ``` 246 | 247 | ### Service 248 | 这里通过createService创建一些基础的Service 249 | 250 | ### 运行环境服务 EnvironmentService 251 | src/vs/platform/environment/node/environmentService.ts 252 | 253 | 通过这个服务获取当前启动目录,日志目录,操作系统信息,配置文件目录,用户目录等。 254 | 255 | ### 日志服务 MultiplexLogService 256 | src/vs/platform/log/common/log.ts 257 | 258 | 默认使用控制台日志ConsoleLogMainService 259 | 其中包含性能追踪和释放信息,日志输出级别 260 | 261 | ### 配置服务 ConfigurationService 262 | src/vs/platform/configuration/node/configurationService.ts 263 | 264 | 从运行环境服务获取内容 265 | 266 | 267 | ### 生命周期服务 LifecycleService 268 | src/vs/platform/lifecycle/common/lifecycleService.ts 269 | 270 | 监听事件,electron app模块 比如:ready, window-all-closed,before-quit 271 | 272 | 可以参考官方[electron app文档](https://www.w3cschool.cn/electronmanual/electronmanual-electronapp.html) 273 | 274 | ### 状态服务 StateService 275 | src/vs/platform/state/node/stateService.ts 276 | 277 | 通过FileStorage读写storage.json存储,里记录一些与程序运行状态有关的键值对 278 | 279 | ### 请求服务 RequestService 280 | src/vs/platform/request/browser/requestService.ts 281 | 282 | 这里使用的是原生ajax请求,实现了request方法 283 | 284 | ### 主题服务 ThemeMainService 285 | src/vs/platform/theme/electron-main/themeMainService.ts 286 | 287 | 这里只设置背景颜色,通过getBackgroundColor方法 IStateService存储 288 | 289 | ### 签名服务 SignService 290 | src/vs/platform/sign/node/signService.ts 291 | 292 | ```js 293 | private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] { 294 | 295 | //服务注册容器 296 | const services = new ServiceCollection(); 297 | 298 | const environmentService = new EnvironmentService(args, process.execPath); 299 | const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment 300 | services.set(IEnvironmentService, environmentService); 301 | 302 | const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]); 303 | process.once('exit', () => logService.dispose()); 304 | //日志服务 305 | services.set(ILogService, logService); 306 | //配置服务 307 | services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource)); 308 | //生命周期 309 | services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); 310 | //状态存储 311 | services.set(IStateService, new SyncDescriptor(StateService)); 312 | //网络请求 313 | services.set(IRequestService, new SyncDescriptor(RequestService)); 314 | //主题设定 315 | services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); 316 | //签名服务 317 | services.set(ISignService, new SyncDescriptor(SignService)); 318 | 319 | return [new InstantiationService(services, true), instanceEnvironment]; 320 | } 321 | ``` 322 | 323 | ## 实例化服务 324 | 325 | SyncDescriptor负责注册这些服务,当用到该服务时进程实例化使用 326 | 327 | src/vs/platform/instantiation/common/descriptors.ts 328 | 329 | ```js 330 | export class SyncDescriptor { 331 | readonly ctor: any; 332 | readonly staticArguments: any[]; 333 | readonly supportsDelayedInstantiation: boolean; 334 | constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) { 335 | this.ctor = ctor; 336 | this.staticArguments = staticArguments; 337 | this.supportsDelayedInstantiation = supportsDelayedInstantiation; 338 | } 339 | } 340 | ``` 341 | 342 | main.ts中startup方法调用invokeFunction.get实例化服务 343 | ```js 344 | await instantiationService.invokeFunction(async accessor => { 345 | const environmentService = accessor.get(IEnvironmentService); 346 | const configurationService = accessor.get(IConfigurationService); 347 | const stateService = accessor.get(IStateService); 348 | try { 349 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService); 350 | } catch (error) { 351 | 352 | // Show a dialog for errors that can be resolved by the user 353 | this.handleStartupDataDirError(environmentService, error); 354 | 355 | throw error; 356 | } 357 | }); 358 | ``` 359 | 360 | get方法调用_getOrCreateServiceInstance,这里第一次创建会存入缓存中 361 | 下次实例化对象时会优先从缓存中获取对象。 362 | 363 | src/vs/platform/instantiation/common/instantiationService.ts 364 | 365 | ```js 366 | invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { 367 | let _trace = Trace.traceInvocation(fn); 368 | let _done = false; 369 | try { 370 | const accessor: ServicesAccessor = { 371 | get: (id: ServiceIdentifier, isOptional?: typeof optional) => { 372 | 373 | if (_done) { 374 | throw illegalState('service accessor is only valid during the invocation of its target method'); 375 | } 376 | 377 | const result = this._getOrCreateServiceInstance(id, _trace); 378 | if (!result && isOptional !== optional) { 379 | throw new Error(`[invokeFunction] unknown service '${id}'`); 380 | } 381 | return result; 382 | } 383 | }; 384 | return fn.apply(undefined, [accessor, ...args]); 385 | } finally { 386 | _done = true; 387 | _trace.stop(); 388 | } 389 | } 390 | private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { 391 | let thing = this._getServiceInstanceOrDescriptor(id); 392 | if (thing instanceof SyncDescriptor) { 393 | return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true)); 394 | } else { 395 | _trace.branch(id, false); 396 | return thing; 397 | } 398 | } 399 | ``` 400 | 401 | #### vs/code/electron-main/app.ts 402 | 这里首先触发CodeApplication.startup()方法, 在第一个窗口打开3秒后成为共享进程, 403 | ```js 404 | async startup(): Promise { 405 | ... 406 | 407 | // 1. 第一个窗口创建共享进程 408 | const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); 409 | const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); 410 | this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { 411 | this._register(new RunOnceScheduler(async () => { 412 | const userEnv = await getShellEnvironment(this.logService, this.environmentService); 413 | 414 | sharedProcess.spawn(userEnv); 415 | }, 3000)).schedule(); 416 | }); 417 | // 2. 创建app实例 418 | const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient); 419 | 420 | 421 | // 3. 打开一个窗口 调用 422 | 423 | const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); 424 | 425 | // 4. 窗口打开后执行生命周期和授权操作 426 | this.afterWindowOpen(); 427 | ... 428 | 429 | //vscode结束了性能问题的追踪 430 | if (this.environmentService.args.trace) { 431 | this.stopTracingEventually(windows); 432 | } 433 | } 434 | ``` 435 | 436 | 437 | openFirstWindow 主要实现 438 | CodeApplication.openFirstWindow 首次开启窗口时,创建 Electron 的 IPC,使主进程和渲染进程间通信。 439 | window会被注册到sharedProcessClient,主进程和共享进程通信 440 | 根据 environmentService 提供的参数(path,uri)调用windowsMainService.open 方法打开窗口 441 | ```js 442 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] { 443 | 444 | ... 445 | // 1. 注入Electron IPC Service, windows窗口管理,菜单栏等服务 446 | 447 | // 2. 根据environmentService进行参数配置 448 | const macOpenFiles: string[] = (global).macOpenFiles; 449 | const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; 450 | const hasCliArgs = hasArgs(args._); 451 | const hasFolderURIs = hasArgs(args['folder-uri']); 452 | const hasFileURIs = hasArgs(args['file-uri']); 453 | const noRecentEntry = args['skip-add-to-recently-opened'] === true; 454 | const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; 455 | 456 | ... 457 | 458 | // 打开主窗口,默认从执行命令行中读取参数 459 | return windowsMainService.open({ 460 | context, 461 | cli: args, 462 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), 463 | diffMode: args.diff, 464 | noRecentEntry, 465 | waitMarkerFileURI, 466 | gotoLineMode: args.goto, 467 | initialStartup: true 468 | }); 469 | } 470 | ``` 471 | 472 | 473 | 474 | #### vs/code/electron-main/windows.ts 475 | 接下来到了electron的windows窗口,open方法在doOpen中执行窗口配置初始化,最终调用openInBrowserWindow -> 执行doOpenInBrowserWindow是其打开window,主要步骤如下: 476 | ```js 477 | private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { 478 | 479 | ... 480 | // New window 481 | if (!window) { 482 | //1.判断是否全屏创建窗口 483 | ... 484 | // 2. 创建实例窗口 485 | window = this.instantiationService.createInstance(CodeWindow, { 486 | state, 487 | extensionDevelopmentPath: configuration.extensionDevelopmentPath, 488 | isExtensionTestHost: !!configuration.extensionTestsPath 489 | }); 490 | 491 | // 3.添加到当前窗口控制器 492 | WindowsManager.WINDOWS.push(window); 493 | 494 | // 4.窗口监听器 495 | window.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own 496 | window.win.webContents.on('devtools-reload-page', () => this.reload(window!)); 497 | window.win.webContents.on('crashed', () => this.onWindowError(window!, WindowError.CRASHED)); 498 | window.win.on('unresponsive', () => this.onWindowError(window!, WindowError.UNRESPONSIVE)); 499 | window.win.on('closed', () => this.onWindowClosed(window!)); 500 | 501 | // 5.注册窗口生命周期 502 | (this.lifecycleService as LifecycleService).registerWindow(window); 503 | } 504 | 505 | ... 506 | 507 | return window; 508 | } 509 | ``` 510 | doOpenInBrowserWindow会调用window.load方法 在window.ts中实现 511 | ```js 512 | load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { 513 | 514 | ... 515 | 516 | // Load URL 517 | perf.mark('main:loadWindow'); 518 | this._win.loadURL(this.getUrl(configuration)); 519 | 520 | ... 521 | } 522 | 523 | private getUrl(windowConfiguration: IWindowConfiguration): string { 524 | 525 | ... 526 | //加载欢迎屏幕的html 527 | let configUrl = this.doGetUrl(config); 528 | ... 529 | return configUrl; 530 | } 531 | 532 | //默认加载 vs/code/electron-browser/workbench/workbench.html 533 | private doGetUrl(config: object): string { 534 | return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; 535 | } 536 | ``` 537 | main process的使命完成, 主界面进行构建布局。 538 | 539 | 540 | 在workbench.html中加载了workbench.js, 541 | 这里调用return require('vs/workbench/electron-browser/desktop.main').main(configuration);实现对主界面的展示 542 | 543 | 544 | #### vs/workbench/electron-browser/desktop.main.ts 545 | 创建工作区,调用workbench.startup()方法,构建主界面展示布局 546 | ```js 547 | ... 548 | async open(): Promise { 549 | const services = await this.initServices(); 550 | await domContentLoaded(); 551 | mark('willStartWorkbench'); 552 | 553 | // 1.创建工作区 554 | const workbench = new Workbench(document.body, services.serviceCollection, services.logService); 555 | 556 | // 2.监听窗口变化 557 | this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench))); 558 | 559 | // 3.工作台生命周期 560 | this._register(workbench.onShutdown(() => this.dispose())); 561 | this._register(workbench.onWillShutdown(event => event.join(services.storageService.close()))); 562 | 563 | // 3.启动工作区 564 | const instantiationService = workbench.startup(); 565 | 566 | ... 567 | } 568 | ... 569 | ``` 570 | 571 | #### vs/workbench/browser/workbench.ts 572 | 工作区继承自layout类,主要作用是构建工作区,创建界面布局。 573 | ```js 574 | export class Workbench extends Layout { 575 | ... 576 | startup(): IInstantiationService { 577 | try { 578 | ... 579 | 580 | // Services 581 | const instantiationService = this.initServices(this.serviceCollection); 582 | 583 | instantiationService.invokeFunction(async accessor => { 584 | const lifecycleService = accessor.get(ILifecycleService); 585 | const storageService = accessor.get(IStorageService); 586 | const configurationService = accessor.get(IConfigurationService); 587 | 588 | // Layout 589 | this.initLayout(accessor); 590 | 591 | // Registries 592 | this.startRegistries(accessor); 593 | 594 | // Context Keys 595 | this._register(instantiationService.createInstance(WorkbenchContextKeysHandler)); 596 | 597 | // 注册监听事件 598 | this.registerListeners(lifecycleService, storageService, configurationService); 599 | 600 | // 渲染工作区 601 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); 602 | 603 | // 创建工作区布局 604 | this.createWorkbenchLayout(instantiationService); 605 | 606 | // 布局构建 607 | this.layout(); 608 | 609 | // Restore 610 | try { 611 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); 612 | } catch (error) { 613 | onUnexpectedError(error); 614 | } 615 | }); 616 | 617 | return instantiationService; 618 | } catch (error) { 619 | onUnexpectedError(error); 620 | 621 | throw error; // rethrow because this is a critical issue we cannot handle properly here 622 | } 623 | } 624 | ... 625 | } 626 | ``` 627 | 628 | ## 事件分发 629 | ### event 630 | src/vs/base/common/event.ts 631 | 632 | 程序中常见使用once方法进行事件绑定, 给定一个事件,返回一个只触发一次的事件,放在匿名函数返回 633 | ```js 634 | export function once(event: Event): Event { 635 | return (listener, thisArgs = null, disposables?) => { 636 | // 设置次变量,防止事件重复触发造成事件污染 637 | let didFire = false; 638 | let result: IDisposable; 639 | result = event(e => { 640 | if (didFire) { 641 | return; 642 | } else if (result) { 643 | result.dispose(); 644 | } else { 645 | didFire = true; 646 | } 647 | 648 | return listener.call(thisArgs, e); 649 | }, null, disposables); 650 | 651 | if (didFire) { 652 | result.dispose(); 653 | } 654 | 655 | return result; 656 | }; 657 | } 658 | ``` 659 | 循环派发了所有注册的事件, 事件会存储到一个事件队列,通过fire方法触发事件 660 | 661 | private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存储队列 662 | 663 | ```js 664 | fire(event: T): void { 665 | if (this._listeners) { 666 | // 将所有事件传入 delivery queue 667 | // 内部/嵌套方式通过emit发出. 668 | // this调用事件驱动 669 | 670 | if (!this._deliveryQueue) { 671 | this._deliveryQueue = new LinkedList(); 672 | } 673 | 674 | for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) { 675 | this._deliveryQueue.push([e.value, event]); 676 | } 677 | 678 | while (this._deliveryQueue.size > 0) { 679 | const [listener, event] = this._deliveryQueue.shift()!; 680 | try { 681 | if (typeof listener === 'function') { 682 | listener.call(undefined, event); 683 | } else { 684 | listener[0].call(listener[1], event); 685 | } 686 | } catch (e) { 687 | onUnexpectedError(e); 688 | } 689 | } 690 | } 691 | } 692 | ``` 693 | 694 | ## 进程通信 695 | 696 | ### 主进程 697 | src/vs/code/electron-main/main.ts 698 | 699 | main.ts在启动应用后就创建了一个主进程 main process,它可以通过electron中的一些模块直接与原生GUI交互。 700 | ```js 701 | server = await serve(environmentService.mainIPCHandle); 702 | once(lifecycleService.onWillShutdown)(() => server.dispose()); 703 | ``` 704 | ### 渲染进程 705 | 仅启动主进程并不能给你的应用创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。 706 | ### 主进程与渲染进程之间的通信 707 | 在electron中,主进程与渲染进程有很多通信的方法。比如ipcRenderer和ipcMain,还可以在渲染进程使用remote模块。 708 | 709 | ### ipcMain & ipcRenderer 710 | * 主进程:ipcMain 711 | * 渲染进程:ipcRenderer 712 | 713 | ipcMain模块和ipcRenderer是类EventEmitter的实例。 714 | 715 | 在主进程中使用ipcMain接收渲染线程发送过来的异步或同步消息,发送过来的消息将触发事件。 716 | 717 | 在渲染进程中使用ipcRenderer向主进程发送同步或异步消息,也可以接收到主进程的消息。 718 | 719 | * 发送消息,事件名为 channel . 720 | * 回应同步消息, 你可以设置 event.returnValue . 721 | * 回应异步消息, 你可以使用 event.sender.send(...) 722 | 723 | 创建IPC服务 724 | src/vs/base/parts/ipc/node/ipc.net.ts 725 | 726 | 这里返回一个promise对象,成功则createServer 727 | 728 | ```js 729 | export function serve(hook: any): Promise { 730 | return new Promise((c, e) => { 731 | const server = createServer(); 732 | 733 | server.on('error', e); 734 | server.listen(hook, () => { 735 | server.removeListener('error', e); 736 | c(new Server(server)); 737 | }); 738 | }); 739 | } 740 | ``` 741 | 742 | #### 创建信道 743 | 744 | src/vs/code/electron-main/app.ts 745 | 746 | * mainIpcServer 747 | * launchChannel 748 | 749 | * electronIpcServer 750 | * updateChannel 751 | * issueChannel 752 | * workspacesChannel 753 | * windowsChannel 754 | * menubarChannel 755 | * urlChannel 756 | * storageChannel 757 | * logLevelChannel 758 | 759 | ```js 760 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] { 761 | 762 | // Register more Main IPC services 763 | const launchService = accessor.get(ILaunchService); 764 | const launchChannel = new LaunchChannel(launchService); 765 | this.mainIpcServer.registerChannel('launch', launchChannel); 766 | 767 | // Register more Electron IPC services 768 | const updateService = accessor.get(IUpdateService); 769 | const updateChannel = new UpdateChannel(updateService); 770 | electronIpcServer.registerChannel('update', updateChannel); 771 | 772 | const issueService = accessor.get(IIssueService); 773 | const issueChannel = new IssueChannel(issueService); 774 | electronIpcServer.registerChannel('issue', issueChannel); 775 | 776 | const workspacesService = accessor.get(IWorkspacesMainService); 777 | const workspacesChannel = new WorkspacesChannel(workspacesService); 778 | electronIpcServer.registerChannel('workspaces', workspacesChannel); 779 | 780 | const windowsService = accessor.get(IWindowsService); 781 | const windowsChannel = new WindowsChannel(windowsService); 782 | electronIpcServer.registerChannel('windows', windowsChannel); 783 | sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); 784 | 785 | const menubarService = accessor.get(IMenubarService); 786 | const menubarChannel = new MenubarChannel(menubarService); 787 | electronIpcServer.registerChannel('menubar', menubarChannel); 788 | 789 | const urlService = accessor.get(IURLService); 790 | const urlChannel = new URLServiceChannel(urlService); 791 | electronIpcServer.registerChannel('url', urlChannel); 792 | 793 | const storageMainService = accessor.get(IStorageMainService); 794 | const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService)); 795 | electronIpcServer.registerChannel('storage', storageChannel); 796 | 797 | // Log level management 798 | const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService)); 799 | electronIpcServer.registerChannel('loglevel', logLevelChannel); 800 | sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); 801 | 802 | ... 803 | 804 | // default: read paths from cli 805 | return windowsMainService.open({ 806 | context, 807 | cli: args, 808 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), 809 | diffMode: args.diff, 810 | noRecentEntry, 811 | waitMarkerFileURI, 812 | gotoLineMode: args.goto, 813 | initialStartup: true 814 | }); 815 | } 816 | ``` 817 | 818 | 每一个信道,内部实现两个方法 listen和call 819 | 820 | 例如:src/vs/platform/localizations/node/localizationsIpc.ts 821 | 822 | 构造函数绑定事件 823 | ```js 824 | export class LocalizationsChannel implements IServerChannel { 825 | 826 | onDidLanguagesChange: Event; 827 | 828 | constructor(private service: ILocalizationsService) { 829 | this.onDidLanguagesChange = Event.buffer(service.onDidLanguagesChange, true); 830 | } 831 | 832 | listen(_: unknown, event: string): Event { 833 | switch (event) { 834 | case 'onDidLanguagesChange': return this.onDidLanguagesChange; 835 | } 836 | 837 | throw new Error(`Event not found: ${event}`); 838 | } 839 | 840 | call(_: unknown, command: string, arg?: any): Promise { 841 | switch (command) { 842 | case 'getLanguageIds': return this.service.getLanguageIds(arg); 843 | } 844 | 845 | throw new Error(`Call not found: ${command}`); 846 | } 847 | } 848 | 849 | 850 | ``` 851 | 852 | ## 主要窗口 853 | 854 | workbench.ts中startup里面Workbench负责创建主界面 855 | src/vs/workbench/browser/workbench.ts 856 | 857 | ```js 858 | startup(): IInstantiationService { 859 | try { 860 | 861 | ... 862 | 863 | instantiationService.invokeFunction(async accessor => { 864 | 865 | // 渲染主工作界面 866 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); 867 | 868 | // 界面布局 869 | this.createWorkbenchLayout(instantiationService); 870 | 871 | // Layout 872 | this.layout(); 873 | 874 | // Restore 875 | try { 876 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); 877 | } catch (error) { 878 | onUnexpectedError(error); 879 | } 880 | }); 881 | 882 | return instantiationService; 883 | } catch (error) { 884 | onUnexpectedError(error); 885 | 886 | throw error; // rethrow because this is a critical issue we cannot handle properly here 887 | } 888 | } 889 | ``` 890 | 891 | 渲染主工作台,渲染完之后加入到container中,container加入到parent, parent就是body了。 892 | 893 | this.parent.appendChild(this.container); 894 | 895 | ```js 896 | private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { 897 | 898 | ... 899 | 900 | //TITLEBAR_PART 顶部操作栏 901 | //ACTIVITYBAR_PART 最左侧菜单选项卡 902 | //SIDEBAR_PART 左侧边栏,显示文件,结果展示等 903 | //EDITOR_PART 右侧窗口,代码编写,欢迎界面等 904 | //STATUSBAR_PART 底部状态栏 905 | [ 906 | { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, 907 | { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, 908 | { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, 909 | { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, 910 | { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] }, 911 | { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] } 912 | ].forEach(({ id, role, classes, options }) => { 913 | const partContainer = this.createPart(id, role, classes); 914 | 915 | if (!configurationService.getValue('workbench.useExperimentalGridLayout')) { 916 | // TODO@Ben cleanup once moved to grid 917 | // Insert all workbench parts at the beginning. Issue #52531 918 | // This is primarily for the title bar to allow overriding -webkit-app-region 919 | this.container.insertBefore(partContainer, this.container.lastChild); 920 | } 921 | 922 | this.getPart(id).create(partContainer, options); 923 | }); 924 | 925 | // 将工作台添加至container dom渲染 926 | this.parent.appendChild(this.container); 927 | } 928 | ``` 929 | 930 | workbench最后调用this.layout()方法,将窗口占据整个界面,渲染完成 931 | ```js 932 | layout(options?: ILayoutOptions): void { 933 | if (!this.disposed) { 934 | this._dimension = getClientArea(this.parent); 935 | 936 | if (this.workbenchGrid instanceof Grid) { 937 | position(this.container, 0, 0, 0, 0, 'relative'); 938 | size(this.container, this._dimension.width, this._dimension.height); 939 | 940 | // Layout the grid widget 941 | this.workbenchGrid.layout(this._dimension.width, this._dimension.height); 942 | } else { 943 | this.workbenchGrid.layout(options); 944 | } 945 | 946 | // Emit as event 947 | this._onLayout.fire(this._dimension); 948 | } 949 | } 950 | ``` 951 | 952 | ## 开发调试 953 | 954 | ```js 955 | app.once('ready', function () { 956 | //启动追踪 957 | if (args['trace']) { 958 | // @ts-ignore 959 | const contentTracing = require('electron').contentTracing; 960 | 961 | const traceOptions = { 962 | categoryFilter: args['trace-category-filter'] || '*', 963 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' 964 | }; 965 | 966 | contentTracing.startRecording(traceOptions, () => onReady()); 967 | } else { 968 | onReady(); 969 | } 970 | }); 971 | ``` 972 | ### 启动追踪 973 | 这里如果传入trace参数,在onReady启动之前会调用chromium的收集跟踪数据, 974 | 提供的底层的追踪工具允许我们深度了解 V8 的解析以及其他时间消耗情况, 975 | 976 | 一旦收到可以开始记录的请求,记录将会立马启动并且在子进程是异步记录听的. 当所有的子进程都收到 startRecording 请求的时候,callback 将会被调用. 977 | 978 | categoryFilter是一个过滤器,它用来控制那些分类组应该被用来查找.过滤器应当有一个可选的 - 前缀来排除匹配的分类组.不允许同一个列表既是包含又是排斥. 979 | 980 | #### contentTracing.startRecording(options, callback) 981 | * options Object 982 | * categoryFilter String 983 | * traceOptions String 984 | * callback Function 985 | 986 | [关于trace的详细介绍](https://www.w3cschool.cn/electronmanual/electronmanual-content-tracing.html) 987 | 988 | ### 结束追踪 989 | #### contentTracing.stopRecording(resultFilePath, callback) 990 | * resultFilePath String 991 | * callback Function 992 | 在成功启动窗口后,程序结束性能追踪,停止对所有子进程的记录. 993 | 994 | 子进程通常缓存查找数据,并且仅仅将数据截取和发送给主进程.这有利于在通过 IPC 发送查找数据之前减小查找时的运行开销,这样做很有价值.因此,发送查找数据,我们应当异步通知所有子进程来截取任何待查找的数据. 995 | 996 | 一旦所有子进程接收到了 stopRecording 请求,将调用 callback ,并且返回一个包含查找数据的文件. 997 | 998 | 如果 resultFilePath 不为空,那么将把查找数据写入其中,否则写入一个临时文件.实际文件路径如果不为空,则将调用 callback . 999 | 1000 | ### debug 1001 | 调试界面在菜单栏找到 Help->Toggle Developers Tools 1002 | 1003 | 调出Chrome开发者调试工具进行调试 1004 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-debugg.png) 1005 | 1006 | 1007 | ## 参考 1008 | https://electronjs.org/docs 1009 | 1010 | https://github.com/microsoft/vscode/wiki/How-to-Contribute 1011 | 1012 | https://github.com/Microsoft/vscode/wiki/Code-Organization 1013 | 1014 | http://xzper.com/2016/04/17/vscode%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/ 1015 | 1016 | http://www.ayqy.net/blog/vs-code%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90/ 1017 | 1018 | -------------------------------------------------------------------------------- /chapter-1.md: -------------------------------------------------------------------------------- 1 | # VSCode源码分析 - 主启动流程 2 | ## 目录 3 | * 启动主进程 4 | * 实例化服务 5 | * 事件分发 6 | * 进程通信 7 | * 主要窗口 8 | * 开发调试 9 | 10 | ## 简介 11 | Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。 12 | 13 | git仓库地址: https://github.com/microsoft/vscode 14 | 15 | 通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。 16 | 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用 17 | 18 | 微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。 19 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-vside-zh.png) 20 | 21 | ## 编译安装 22 | 下载最新版本,目前我用的是1.37.1版本 23 | 官方的wiki中有编译安装的说明 [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute?_blank) 24 | 25 | Linux, Window, MacOS三个系统编译时有些差别,参考官方文档, 26 | 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。 27 | 28 | > 需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行) 29 | 30 | 31 | ## 技术架构 32 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-framework.png) 33 | 34 | ### Electron 35 | 36 | > [Electron](https://electronjs.org/?_blank) 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API) 37 | 38 | ### Monaco Editor 39 | > [Monaco Editor](https://github.com/microsoft/monaco-editor?_blank)是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。 40 | 41 | ### TypeScript 42 | > TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程 43 | 44 | ## 目录结构 45 | ``` 46 | ├── build # gulp编译构建脚本 47 | ├── extensions # 内置插件 48 | ├── product.json # App meta信息 49 | ├── resources # 平台相关静态资源 50 | ├── scripts # 工具脚本,开发/测试 51 | ├── src # 源码目录 52 | └── typings # 函数语法补全定义 53 | └── vs 54 | ├── base # 通用工具/协议和UI库 55 | │ ├── browser # 基础UI组件,DOM操作 56 | │ ├── common # diff描述,markdown解析器,worker协议,各种工具函数 57 | │ ├── node # Node工具函数 58 | │ ├── parts # IPC协议(Electron、Node),quickopen、tree组件 59 | │ ├── test # base单测用例 60 | │ └── worker # Worker factory和main Worker(运行IDE Core:Monaco) 61 | ├── code # VSCode主运行窗口 62 | ├── editor # IDE代码编辑器 63 | | ├── browser # 代码编辑器核心 64 | | ├── common # 代码编辑器核心 65 | | ├── contrib # vscode 与独立 IDE共享的代码 66 | | └── standalone # 独立 IDE 独有的代码 67 | ├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏) 68 | ├── workbench # 工作区UI布局,功能主界面 69 | │ ├── api # 70 | │ ├── browser # 71 | │ ├── common # 72 | │ ├── contrib # 73 | │ ├── electron-browser # 74 | │ ├── services # 75 | │ └── test # 76 | ├── css.build.js # 用于插件构建的CSS loader 77 | ├── css.js # CSS loader 78 | ├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持 79 | ├── loader.js # AMD loader(用于异步加载AMD模块) 80 | ├── nls.build.js # 用于插件构建的NLS loader 81 | └── nls.js # NLS(National Language Support)多语言loader 82 | ``` 83 | ### 核心层 84 | * base: 提供通用服务和构建用户界面 85 | * platform: 注入服务和基础服务代码 86 | * editor: 微软Monaco编辑器,也可独立运行使用 87 | * wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序 88 | 89 | ### 核心环境 90 | 整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排, 91 | 运行的环境分为几类: 92 | * common: 只使用javascritp api的代码,能在任何环境下运行 93 | * browser: 浏览器api, 如操作dom; 可以调用common 94 | * node: 需要使用node的api,比如文件io操作 95 | * electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖[electron renderer-process API](https://github.com/electron/electron/tree/master/docs#modules-for-the-renderer-process-web-page) 96 | * electron-main: 主进程api, 可以调用: common, node 依赖于[electron main-process AP](https://github.com/electron/electron/tree/master/docs#modules-for-the-main-process) 97 | 98 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-welcome.png) 99 | 100 | 101 | ## 启动主进程 102 | 103 | ### Electron通过package.json中的main字段来定义应用入口。 104 | main.js是vscode的入口。 105 | 106 | * src/main.js 107 | * vs/code/electron-main/main.ts 108 | * vs/code/electron-main/app.ts 109 | * vs/code/electron-main/windows.ts 110 | * vs/workbench/electron-browser/desktop.main.ts 111 | * vs/workbench/browser/workbench.ts 112 | 113 | ```js 114 | app.once('ready', function () { 115 | //启动追踪,后面会讲到,跟性能检测优化相关。 116 | if (args['trace']) { 117 | // @ts-ignore 118 | const contentTracing = require('electron').contentTracing; 119 | 120 | const traceOptions = { 121 | categoryFilter: args['trace-category-filter'] || '*', 122 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' 123 | }; 124 | 125 | contentTracing.startRecording(traceOptions, () => onReady()); 126 | } else { 127 | onReady(); 128 | } 129 | }); 130 | function onReady() { 131 | perf.mark('main:appReady'); 132 | 133 | Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => { 134 | //1. 这里尝试获取本地配置信息,如果有的话会传递到startup 135 | if (locale && !nlsConfiguration) { 136 | nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale); 137 | } 138 | 139 | if (!nlsConfiguration) { 140 | nlsConfiguration = Promise.resolve(undefined); 141 | } 142 | 143 | 144 | nlsConfiguration.then(nlsConfig => { 145 | 146 | //4. 首先会检查用户语言环境配置,如果没有设置默认使用英语 147 | const startup = nlsConfig => { 148 | nlsConfig._languagePackSupport = true; 149 | process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig); 150 | process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || ''; 151 | 152 | perf.mark('willLoadMainBundle'); 153 | //使用微软的loader组件加载electron-main/main文件 154 | require('./bootstrap-amd').load('vs/code/electron-main/main', () => { 155 | perf.mark('didLoadMainBundle'); 156 | }); 157 | }; 158 | 159 | // 2. 接收到有效的配置传入是其生效,调用startup 160 | if (nlsConfig) { 161 | startup(nlsConfig); 162 | } 163 | 164 | // 3. 这里尝试使用本地的应用程序 165 | // 应用程序设置区域在ready事件后才有效 166 | else { 167 | let appLocale = app.getLocale(); 168 | if (!appLocale) { 169 | startup({ locale: 'en', availableLanguages: {} }); 170 | } else { 171 | 172 | // 配置兼容大小写敏感,所以统一转换成小写 173 | appLocale = appLocale.toLowerCase(); 174 | // 这里就会调用config服务,把本地配置加载进来再调用startup 175 | lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => { 176 | if (!nlsConfig) { 177 | nlsConfig = { locale: appLocale, availableLanguages: {} }; 178 | } 179 | 180 | startup(nlsConfig); 181 | }); 182 | } 183 | } 184 | }); 185 | }, console.error); 186 | } 187 | ``` 188 | 189 | ### vs/code/electron-main/main.ts 190 | electron-main/main 是程序真正启动的入口,进入main process初始化流程. 191 | #### 这里主要做了两件事情: 192 | 1. 初始化Service 193 | 2. 启动主实例 194 | 195 | 直接看startup方法的实现,基础服务初始化完成后会加载 CodeApplication, mainIpcServer, instanceEnvironment,调用 startup 方法启动APP 196 | ```js 197 | private async startup(args: ParsedArgs): Promise { 198 | 199 | //spdlog 日志服务 200 | const bufferLogService = new BufferLogService(); 201 | 202 | // 1. 调用 createServices 203 | const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService); 204 | try { 205 | 206 | // 1.1 初始化Service服务 207 | await instantiationService.invokeFunction(async accessor => { 208 | // 基础服务,包括一些用户数据,缓存目录 209 | const environmentService = accessor.get(IEnvironmentService); 210 | // 配置服务 211 | const configurationService = accessor.get(IConfigurationService); 212 | // 持久化数据 213 | const stateService = accessor.get(IStateService); 214 | 215 | try { 216 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService); 217 | } catch (error) { 218 | 219 | // 抛出错误对话框 220 | this.handleStartupDataDirError(environmentService, error); 221 | 222 | throw error; 223 | } 224 | }); 225 | 226 | // 1.2 启动实例 227 | await instantiationService.invokeFunction(async accessor => { 228 | const environmentService = accessor.get(IEnvironmentService); 229 | const logService = accessor.get(ILogService); 230 | const lifecycleService = accessor.get(ILifecycleService); 231 | const configurationService = accessor.get(IConfigurationService); 232 | 233 | const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true); 234 | 235 | bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel()); 236 | once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose()); 237 | 238 | return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup(); 239 | }); 240 | } catch (error) { 241 | instantiationService.invokeFunction(this.quit, error); 242 | } 243 | } 244 | ``` 245 | 246 | 247 | #### vs/code/electron-main/app.ts 248 | 这里首先触发CodeApplication.startup()方法, 在第一个窗口打开3秒后成为共享进程, 249 | ```js 250 | async startup(): Promise { 251 | ... 252 | 253 | // 1. 第一个窗口创建共享进程 254 | const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); 255 | const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); 256 | this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { 257 | this._register(new RunOnceScheduler(async () => { 258 | const userEnv = await getShellEnvironment(this.logService, this.environmentService); 259 | 260 | sharedProcess.spawn(userEnv); 261 | }, 3000)).schedule(); 262 | }); 263 | // 2. 创建app实例 264 | const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient); 265 | 266 | 267 | // 3. 打开一个窗口 调用 268 | 269 | const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); 270 | 271 | // 4. 窗口打开后执行生命周期和授权操作 272 | this.afterWindowOpen(); 273 | ... 274 | 275 | //vscode结束了性能问题的追踪 276 | if (this.environmentService.args.trace) { 277 | this.stopTracingEventually(windows); 278 | } 279 | } 280 | ``` 281 | 282 | 283 | openFirstWindow 主要实现 284 | CodeApplication.openFirstWindow 首次开启窗口时,创建 Electron 的 IPC,使主进程和渲染进程间通信。 285 | window会被注册到sharedProcessClient,主进程和共享进程通信 286 | 根据 environmentService 提供的参数(path,uri)调用windowsMainService.open 方法打开窗口 287 | ```js 288 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] { 289 | 290 | ... 291 | // 1. 注入Electron IPC Service, windows窗口管理,菜单栏等服务 292 | 293 | // 2. 根据environmentService进行参数配置 294 | const macOpenFiles: string[] = (global).macOpenFiles; 295 | const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; 296 | const hasCliArgs = hasArgs(args._); 297 | const hasFolderURIs = hasArgs(args['folder-uri']); 298 | const hasFileURIs = hasArgs(args['file-uri']); 299 | const noRecentEntry = args['skip-add-to-recently-opened'] === true; 300 | const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; 301 | 302 | ... 303 | 304 | // 打开主窗口,默认从执行命令行中读取参数 305 | return windowsMainService.open({ 306 | context, 307 | cli: args, 308 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), 309 | diffMode: args.diff, 310 | noRecentEntry, 311 | waitMarkerFileURI, 312 | gotoLineMode: args.goto, 313 | initialStartup: true 314 | }); 315 | } 316 | ``` 317 | 318 | 319 | 320 | #### vs/code/electron-main/windows.ts 321 | 接下来到了electron的windows窗口,open方法在doOpen中执行窗口配置初始化,最终调用openInBrowserWindow -> 执行doOpenInBrowserWindow是其打开window,主要步骤如下: 322 | ```js 323 | private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { 324 | 325 | ... 326 | // New window 327 | if (!window) { 328 | //1.判断是否全屏创建窗口 329 | ... 330 | // 2. 创建实例窗口 331 | window = this.instantiationService.createInstance(CodeWindow, { 332 | state, 333 | extensionDevelopmentPath: configuration.extensionDevelopmentPath, 334 | isExtensionTestHost: !!configuration.extensionTestsPath 335 | }); 336 | 337 | // 3.添加到当前窗口控制器 338 | WindowsManager.WINDOWS.push(window); 339 | 340 | // 4.窗口监听器 341 | window.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own 342 | window.win.webContents.on('devtools-reload-page', () => this.reload(window!)); 343 | window.win.webContents.on('crashed', () => this.onWindowError(window!, WindowError.CRASHED)); 344 | window.win.on('unresponsive', () => this.onWindowError(window!, WindowError.UNRESPONSIVE)); 345 | window.win.on('closed', () => this.onWindowClosed(window!)); 346 | 347 | // 5.注册窗口生命周期 348 | (this.lifecycleService as LifecycleService).registerWindow(window); 349 | } 350 | 351 | ... 352 | 353 | return window; 354 | } 355 | ``` 356 | doOpenInBrowserWindow会调用window.load方法 在window.ts中实现 357 | ```js 358 | load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { 359 | 360 | ... 361 | 362 | // Load URL 363 | perf.mark('main:loadWindow'); 364 | this._win.loadURL(this.getUrl(configuration)); 365 | 366 | ... 367 | } 368 | 369 | private getUrl(windowConfiguration: IWindowConfiguration): string { 370 | 371 | ... 372 | //加载欢迎屏幕的html 373 | let configUrl = this.doGetUrl(config); 374 | ... 375 | return configUrl; 376 | } 377 | 378 | //默认加载 vs/code/electron-browser/workbench/workbench.html 379 | private doGetUrl(config: object): string { 380 | return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; 381 | } 382 | ``` 383 | main process的使命完成, 主界面进行构建布局。 384 | 385 | 386 | 在workbench.html中加载了workbench.js, 387 | 这里调用return require('vs/workbench/electron-browser/desktop.main').main(configuration);实现对主界面的展示 388 | 389 | 390 | #### vs/workbench/electron-browser/desktop.main.ts 391 | 创建工作区,调用workbench.startup()方法,构建主界面展示布局 392 | ```js 393 | ... 394 | async open(): Promise { 395 | const services = await this.initServices(); 396 | await domContentLoaded(); 397 | mark('willStartWorkbench'); 398 | 399 | // 1.创建工作区 400 | const workbench = new Workbench(document.body, services.serviceCollection, services.logService); 401 | 402 | // 2.监听窗口变化 403 | this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench))); 404 | 405 | // 3.工作台生命周期 406 | this._register(workbench.onShutdown(() => this.dispose())); 407 | this._register(workbench.onWillShutdown(event => event.join(services.storageService.close()))); 408 | 409 | // 3.启动工作区 410 | const instantiationService = workbench.startup(); 411 | 412 | ... 413 | } 414 | ... 415 | ``` 416 | 417 | #### vs/workbench/browser/workbench.ts 418 | 工作区继承自layout类,主要作用是构建工作区,创建界面布局。 419 | ```js 420 | export class Workbench extends Layout { 421 | ... 422 | startup(): IInstantiationService { 423 | try { 424 | ... 425 | 426 | // Services 427 | const instantiationService = this.initServices(this.serviceCollection); 428 | 429 | instantiationService.invokeFunction(async accessor => { 430 | const lifecycleService = accessor.get(ILifecycleService); 431 | const storageService = accessor.get(IStorageService); 432 | const configurationService = accessor.get(IConfigurationService); 433 | 434 | // Layout 435 | this.initLayout(accessor); 436 | 437 | // Registries 438 | this.startRegistries(accessor); 439 | 440 | // Context Keys 441 | this._register(instantiationService.createInstance(WorkbenchContextKeysHandler)); 442 | 443 | // 注册监听事件 444 | this.registerListeners(lifecycleService, storageService, configurationService); 445 | 446 | // 渲染工作区 447 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); 448 | 449 | // 创建工作区布局 450 | this.createWorkbenchLayout(instantiationService); 451 | 452 | // 布局构建 453 | this.layout(); 454 | 455 | // Restore 456 | try { 457 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); 458 | } catch (error) { 459 | onUnexpectedError(error); 460 | } 461 | }); 462 | 463 | return instantiationService; 464 | } catch (error) { 465 | onUnexpectedError(error); 466 | 467 | throw error; // rethrow because this is a critical issue we cannot handle properly here 468 | } 469 | } 470 | ... 471 | } 472 | ``` 473 | -------------------------------------------------------------------------------- /chapter-2.md: -------------------------------------------------------------------------------- 1 | 2 | # VSCode源码分析 - 实例化服务 3 | 4 | ## 目录 5 | * 启动主进程 6 | * 实例化服务 7 | * 事件分发 8 | * 进程通信 9 | * 主要窗口 10 | * 开发调试 11 | 12 | 13 | ## 实例化服务 14 | 15 | SyncDescriptor负责注册这些服务,当用到该服务时进程实例化使用 16 | 17 | src/vs/platform/instantiation/common/descriptors.ts 18 | 19 | ```js 20 | export class SyncDescriptor { 21 | readonly ctor: any; 22 | readonly staticArguments: any[]; 23 | readonly supportsDelayedInstantiation: boolean; 24 | constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) { 25 | this.ctor = ctor; 26 | this.staticArguments = staticArguments; 27 | this.supportsDelayedInstantiation = supportsDelayedInstantiation; 28 | } 29 | } 30 | ``` 31 | 32 | main.ts中startup方法调用invokeFunction.get实例化服务 33 | ```js 34 | await instantiationService.invokeFunction(async accessor => { 35 | const environmentService = accessor.get(IEnvironmentService); 36 | const configurationService = accessor.get(IConfigurationService); 37 | const stateService = accessor.get(IStateService); 38 | try { 39 | await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService); 40 | } catch (error) { 41 | 42 | // Show a dialog for errors that can be resolved by the user 43 | this.handleStartupDataDirError(environmentService, error); 44 | 45 | throw error; 46 | } 47 | }); 48 | ``` 49 | 50 | get方法调用_getOrCreateServiceInstance,这里第一次创建会存入缓存中 51 | 下次实例化对象时会优先从缓存中获取对象。 52 | 53 | src/vs/platform/instantiation/common/instantiationService.ts 54 | 55 | ```js 56 | invokeFunction(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R { 57 | let _trace = Trace.traceInvocation(fn); 58 | let _done = false; 59 | try { 60 | const accessor: ServicesAccessor = { 61 | get: (id: ServiceIdentifier, isOptional?: typeof optional) => { 62 | 63 | if (_done) { 64 | throw illegalState('service accessor is only valid during the invocation of its target method'); 65 | } 66 | 67 | const result = this._getOrCreateServiceInstance(id, _trace); 68 | if (!result && isOptional !== optional) { 69 | throw new Error(`[invokeFunction] unknown service '${id}'`); 70 | } 71 | return result; 72 | } 73 | }; 74 | return fn.apply(undefined, [accessor, ...args]); 75 | } finally { 76 | _done = true; 77 | _trace.stop(); 78 | } 79 | } 80 | private _getOrCreateServiceInstance(id: ServiceIdentifier, _trace: Trace): T { 81 | let thing = this._getServiceInstanceOrDescriptor(id); 82 | if (thing instanceof SyncDescriptor) { 83 | return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true)); 84 | } else { 85 | _trace.branch(id, false); 86 | return thing; 87 | } 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /chapter-3.md: -------------------------------------------------------------------------------- 1 | # VSCode源码分析 - 事件分发 2 | ## 目录 3 | * 启动主进程 4 | * 实例化服务 5 | * 事件分发 6 | * 进程通信 7 | * 主要窗口 8 | * 开发调试 9 | 10 | ## 事件分发 11 | ### event 12 | src/vs/base/common/event.ts 13 | 14 | 程序中常见使用once方法进行事件绑定, 给定一个事件,返回一个只触发一次的事件,放在匿名函数返回 15 | ```js 16 | export function once(event: Event): Event { 17 | return (listener, thisArgs = null, disposables?) => { 18 | // 设置次变量,防止事件重复触发造成事件污染 19 | let didFire = false; 20 | let result: IDisposable; 21 | result = event(e => { 22 | if (didFire) { 23 | return; 24 | } else if (result) { 25 | result.dispose(); 26 | } else { 27 | didFire = true; 28 | } 29 | 30 | return listener.call(thisArgs, e); 31 | }, null, disposables); 32 | 33 | if (didFire) { 34 | result.dispose(); 35 | } 36 | 37 | return result; 38 | }; 39 | } 40 | ``` 41 | 循环派发了所有注册的事件, 事件会存储到一个事件队列,通过fire方法触发事件 42 | 43 | private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存储队列 44 | 45 | ```js 46 | fire(event: T): void { 47 | if (this._listeners) { 48 | // 将所有事件传入 delivery queue 49 | // 内部/嵌套方式通过emit发出. 50 | // this调用事件驱动 51 | 52 | if (!this._deliveryQueue) { 53 | this._deliveryQueue = new LinkedList(); 54 | } 55 | 56 | for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) { 57 | this._deliveryQueue.push([e.value, event]); 58 | } 59 | 60 | while (this._deliveryQueue.size > 0) { 61 | const [listener, event] = this._deliveryQueue.shift()!; 62 | try { 63 | if (typeof listener === 'function') { 64 | listener.call(undefined, event); 65 | } else { 66 | listener[0].call(listener[1], event); 67 | } 68 | } catch (e) { 69 | onUnexpectedError(e); 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /chapter-4.md: -------------------------------------------------------------------------------- 1 | # VSCode源码分析 - 进程通信 2 | ## 目录 3 | * 启动主进程 4 | * 实例化服务 5 | * 事件分发 6 | * 进程通信 7 | * 主要窗口 8 | * 开发调试 9 | 10 | ## 进程通信 11 | 12 | ### 主进程 13 | src/vs/code/electron-main/main.ts 14 | 15 | main.ts在启动应用后就创建了一个主进程 main process,它可以通过electron中的一些模块直接与原生GUI交互。 16 | ```js 17 | server = await serve(environmentService.mainIPCHandle); 18 | once(lifecycleService.onWillShutdown)(() => server.dispose()); 19 | ``` 20 | ### 渲染进程 21 | 仅启动主进程并不能给你的应用创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。 22 | ### 主进程与渲染进程之间的通信 23 | 在electron中,主进程与渲染进程有很多通信的方法。比如ipcRenderer和ipcMain,还可以在渲染进程使用remote模块。 24 | 25 | ### ipcMain & ipcRenderer 26 | * 主进程:ipcMain 27 | * 渲染进程:ipcRenderer 28 | 29 | ipcMain模块和ipcRenderer是类EventEmitter的实例。 30 | 31 | 在主进程中使用ipcMain接收渲染线程发送过来的异步或同步消息,发送过来的消息将触发事件。 32 | 33 | 在渲染进程中使用ipcRenderer向主进程发送同步或异步消息,也可以接收到主进程的消息。 34 | 35 | * 发送消息,事件名为 channel . 36 | * 回应同步消息, 你可以设置 event.returnValue . 37 | * 回应异步消息, 你可以使用 event.sender.send(...) 38 | 39 | 创建IPC服务 40 | src/vs/base/parts/ipc/node/ipc.net.ts 41 | 42 | 这里返回一个promise对象,成功则createServer 43 | 44 | ```js 45 | export function serve(hook: any): Promise { 46 | return new Promise((c, e) => { 47 | const server = createServer(); 48 | 49 | server.on('error', e); 50 | server.listen(hook, () => { 51 | server.removeListener('error', e); 52 | c(new Server(server)); 53 | }); 54 | }); 55 | } 56 | ``` 57 | 58 | #### 创建信道 59 | 60 | src/vs/code/electron-main/app.ts 61 | 62 | * mainIpcServer 63 | * launchChannel 64 | 65 | * electronIpcServer 66 | * updateChannel 67 | * issueChannel 68 | * workspacesChannel 69 | * windowsChannel 70 | * menubarChannel 71 | * urlChannel 72 | * storageChannel 73 | * logLevelChannel 74 | 75 | ```js 76 | private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise>): ICodeWindow[] { 77 | 78 | // Register more Main IPC services 79 | const launchService = accessor.get(ILaunchService); 80 | const launchChannel = new LaunchChannel(launchService); 81 | this.mainIpcServer.registerChannel('launch', launchChannel); 82 | 83 | // Register more Electron IPC services 84 | const updateService = accessor.get(IUpdateService); 85 | const updateChannel = new UpdateChannel(updateService); 86 | electronIpcServer.registerChannel('update', updateChannel); 87 | 88 | const issueService = accessor.get(IIssueService); 89 | const issueChannel = new IssueChannel(issueService); 90 | electronIpcServer.registerChannel('issue', issueChannel); 91 | 92 | const workspacesService = accessor.get(IWorkspacesMainService); 93 | const workspacesChannel = new WorkspacesChannel(workspacesService); 94 | electronIpcServer.registerChannel('workspaces', workspacesChannel); 95 | 96 | const windowsService = accessor.get(IWindowsService); 97 | const windowsChannel = new WindowsChannel(windowsService); 98 | electronIpcServer.registerChannel('windows', windowsChannel); 99 | sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel)); 100 | 101 | const menubarService = accessor.get(IMenubarService); 102 | const menubarChannel = new MenubarChannel(menubarService); 103 | electronIpcServer.registerChannel('menubar', menubarChannel); 104 | 105 | const urlService = accessor.get(IURLService); 106 | const urlChannel = new URLServiceChannel(urlService); 107 | electronIpcServer.registerChannel('url', urlChannel); 108 | 109 | const storageMainService = accessor.get(IStorageMainService); 110 | const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService)); 111 | electronIpcServer.registerChannel('storage', storageChannel); 112 | 113 | // Log level management 114 | const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService)); 115 | electronIpcServer.registerChannel('loglevel', logLevelChannel); 116 | sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel)); 117 | 118 | ... 119 | 120 | // default: read paths from cli 121 | return windowsMainService.open({ 122 | context, 123 | cli: args, 124 | forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), 125 | diffMode: args.diff, 126 | noRecentEntry, 127 | waitMarkerFileURI, 128 | gotoLineMode: args.goto, 129 | initialStartup: true 130 | }); 131 | } 132 | ``` 133 | 134 | 每一个信道,内部实现两个方法 listen和call 135 | 136 | 例如:src/vs/platform/localizations/node/localizationsIpc.ts 137 | 138 | 构造函数绑定事件 139 | ```js 140 | export class LocalizationsChannel implements IServerChannel { 141 | 142 | onDidLanguagesChange: Event; 143 | 144 | constructor(private service: ILocalizationsService) { 145 | this.onDidLanguagesChange = Event.buffer(service.onDidLanguagesChange, true); 146 | } 147 | 148 | listen(_: unknown, event: string): Event { 149 | switch (event) { 150 | case 'onDidLanguagesChange': return this.onDidLanguagesChange; 151 | } 152 | 153 | throw new Error(`Event not found: ${event}`); 154 | } 155 | 156 | call(_: unknown, command: string, arg?: any): Promise { 157 | switch (command) { 158 | case 'getLanguageIds': return this.service.getLanguageIds(arg); 159 | } 160 | 161 | throw new Error(`Call not found: ${command}`); 162 | } 163 | } 164 | 165 | 166 | ``` 167 | -------------------------------------------------------------------------------- /chapter-5.md: -------------------------------------------------------------------------------- 1 | # VSCode源码分析 - 主要窗口 2 | ## 目录 3 | * 启动主进程 4 | * 实例化服务 5 | * 事件分发 6 | * 进程通信 7 | * 主要窗口 8 | * 开发调试 9 | 10 | ## 主要窗口 11 | 12 | workbench.ts中startup里面Workbench负责创建主界面 13 | src/vs/workbench/browser/workbench.ts 14 | 15 | ```js 16 | startup(): IInstantiationService { 17 | try { 18 | 19 | ... 20 | 21 | instantiationService.invokeFunction(async accessor => { 22 | 23 | // 渲染主工作界面 24 | this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); 25 | 26 | // 界面布局 27 | this.createWorkbenchLayout(instantiationService); 28 | 29 | // Layout 30 | this.layout(); 31 | 32 | // Restore 33 | try { 34 | await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService); 35 | } catch (error) { 36 | onUnexpectedError(error); 37 | } 38 | }); 39 | 40 | return instantiationService; 41 | } catch (error) { 42 | onUnexpectedError(error); 43 | 44 | throw error; // rethrow because this is a critical issue we cannot handle properly here 45 | } 46 | } 47 | ``` 48 | 49 | 渲染主工作台,渲染完之后加入到container中,container加入到parent, parent就是body了。 50 | 51 | this.parent.appendChild(this.container); 52 | 53 | ```js 54 | private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { 55 | 56 | ... 57 | 58 | //TITLEBAR_PART 顶部操作栏 59 | //ACTIVITYBAR_PART 最左侧菜单选项卡 60 | //SIDEBAR_PART 左侧边栏,显示文件,结果展示等 61 | //EDITOR_PART 右侧窗口,代码编写,欢迎界面等 62 | //STATUSBAR_PART 底部状态栏 63 | [ 64 | { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, 65 | { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, 66 | { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, 67 | { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, 68 | { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] }, 69 | { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] } 70 | ].forEach(({ id, role, classes, options }) => { 71 | const partContainer = this.createPart(id, role, classes); 72 | 73 | if (!configurationService.getValue('workbench.useExperimentalGridLayout')) { 74 | // TODO@Ben cleanup once moved to grid 75 | // Insert all workbench parts at the beginning. Issue #52531 76 | // This is primarily for the title bar to allow overriding -webkit-app-region 77 | this.container.insertBefore(partContainer, this.container.lastChild); 78 | } 79 | 80 | this.getPart(id).create(partContainer, options); 81 | }); 82 | 83 | // 将工作台添加至container dom渲染 84 | this.parent.appendChild(this.container); 85 | } 86 | ``` 87 | 88 | workbench最后调用this.layout()方法,将窗口占据整个界面,渲染完成 89 | ```js 90 | layout(options?: ILayoutOptions): void { 91 | if (!this.disposed) { 92 | this._dimension = getClientArea(this.parent); 93 | 94 | if (this.workbenchGrid instanceof Grid) { 95 | position(this.container, 0, 0, 0, 0, 'relative'); 96 | size(this.container, this._dimension.width, this._dimension.height); 97 | 98 | // Layout the grid widget 99 | this.workbenchGrid.layout(this._dimension.width, this._dimension.height); 100 | } else { 101 | this.workbenchGrid.layout(options); 102 | } 103 | 104 | // Emit as event 105 | this._onLayout.fire(this._dimension); 106 | } 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /chapter-6.md: -------------------------------------------------------------------------------- 1 | 2 | # VSCode源码分析 - 开发调试 3 | 4 | ## 目录 5 | * 启动主进程 6 | * 实例化服务 7 | * 事件分发 8 | * 进程通信 9 | * 主要窗口 10 | * 开发调试 11 | 12 | ## 开发调试 13 | 14 | ```js 15 | app.once('ready', function () { 16 | //启动追踪 17 | if (args['trace']) { 18 | // @ts-ignore 19 | const contentTracing = require('electron').contentTracing; 20 | 21 | const traceOptions = { 22 | categoryFilter: args['trace-category-filter'] || '*', 23 | traceOptions: args['trace-options'] || 'record-until-full,enable-sampling' 24 | }; 25 | 26 | contentTracing.startRecording(traceOptions, () => onReady()); 27 | } else { 28 | onReady(); 29 | } 30 | }); 31 | ``` 32 | ### 启动追踪 33 | 这里如果传入trace参数,在onReady启动之前会调用chromium的收集跟踪数据, 34 | 提供的底层的追踪工具允许我们深度了解 V8 的解析以及其他时间消耗情况, 35 | 36 | 一旦收到可以开始记录的请求,记录将会立马启动并且在子进程是异步记录听的. 当所有的子进程都收到 startRecording 请求的时候,callback 将会被调用. 37 | 38 | categoryFilter是一个过滤器,它用来控制那些分类组应该被用来查找.过滤器应当有一个可选的 - 前缀来排除匹配的分类组.不允许同一个列表既是包含又是排斥. 39 | 40 | #### contentTracing.startRecording(options, callback) 41 | * options Object 42 | * categoryFilter String 43 | * traceOptions String 44 | * callback Function 45 | 46 | [关于trace的详细介绍](https://www.w3cschool.cn/electronmanual/electronmanual-content-tracing.html) 47 | 48 | ### 结束追踪 49 | #### contentTracing.stopRecording(resultFilePath, callback) 50 | * resultFilePath String 51 | * callback Function 52 | 在成功启动窗口后,程序结束性能追踪,停止对所有子进程的记录. 53 | 54 | 子进程通常缓存查找数据,并且仅仅将数据截取和发送给主进程.这有利于在通过 IPC 发送查找数据之前减小查找时的运行开销,这样做很有价值.因此,发送查找数据,我们应当异步通知所有子进程来截取任何待查找的数据. 55 | 56 | 一旦所有子进程接收到了 stopRecording 请求,将调用 callback ,并且返回一个包含查找数据的文件. 57 | 58 | 如果 resultFilePath 不为空,那么将把查找数据写入其中,否则写入一个临时文件.实际文件路径如果不为空,则将调用 callback . 59 | 60 | ### debug 61 | 调试界面在菜单栏找到 Help->Toggle Developers Tools 62 | 63 | 调出Chrome开发者调试工具进行调试 64 | ![img](https://github.com/fzxa/VSCode-sourcecode-analysis/blob/master/vscode-debugg.png) 65 | -------------------------------------------------------------------------------- /vscode-debugg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-debugg.png -------------------------------------------------------------------------------- /vscode-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-framework.png -------------------------------------------------------------------------------- /vscode-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-source.png -------------------------------------------------------------------------------- /vscode-vside-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-vside-zh.png -------------------------------------------------------------------------------- /vscode-vside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-vside.png -------------------------------------------------------------------------------- /vscode-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzxa/VSCode-sourcecode-analysis/fdb5deee742334d72a7e8a1a1aec2b2d9c5f6661/vscode-welcome.png --------------------------------------------------------------------------------