VSCode源码解析(一)- 程序的启动逻辑
学前先了解:
Visual Studio Code / Egret Wing 技术架构:基础
VS Code源码简析
仅为自学梳理流程。
1. 查找项目入口文件
在项目根目录package.json
中找到项目入口文件
在此处可以看到入口文件地址。out
文件的内容为编译后出现的,如果没有编译则找不到此文件。
编译参考:VSCode源码环境安装及运行
2. 查看electron的ready事件
追踪out\main.js
或src\main.js
文件。
不编译也没关系,不编译就查找src\main.js
。out\main.js
是src\main.js
的JavaScript编译版。
找到其中的electron的ready事件app.once('ready')
继续追踪onReady
继续追踪startUp事件
其中的加载了主程序文件vs/code/electron-main/main.ts
3. 创建初始化服务并启动VSCode实例
追踪vs/code/electron-main/main.ts
执行了当前类中的main
方法
main
方法中主要调用了startup
方法。
继续看startup
,主要是创建和初始化服务,启动VSCode实例。
private async startup(): Promise<void> {
// 尽早设置错误处理程序,以避免弹出 Electron 默认的错误对话框
setUnexpectedErrorHandler(err => console.error(err));
// Create services
// 1. 调用 createServices
const [instantiationService, instanceEnvironment, environmentMainService, configurationService, stateMainService, bufferLogger,
productService, userDataProfilesMainService] = this.createServices();
try {
// Init services
// 1.1 初始化Service服务
//HD
try {
await this.initServices(environmentMainService, userDataProfilesMainService, configurationService, stateMainService,
productService);
} catch (error) {
// Show a dialog for errors that can be resolved by the user
this.handleStartupDataDirError(environmentMainService, productService, error);
throw error;
}
// Startup
/* HD main process入口文件对应源码 // 1.2 启动实例*/
await instantiationService.invokeFunction(async accessor => {
const logService = accessor.get(ILogService);
const lifecycleMainService = accessor.get(ILifecycleMainService);
const fileService = accessor.get(IFileService);
const loggerService = accessor.get(ILoggerService);
/*
* 这个方法负责尝试启动一个主 IPC(进程间通信)服务器。
* 如果成功,则表示这是第一个运行的 VS Code 实例。
* 如果失败,则表示我们不是第一个运行的 VS Code 实例,
* 因此将会退出。
* 其主要功能是确保同一时间只有一个 VS Code 实例在运行。
* 如果检测到另一个实例,它会连接到该实例并发送必要的环境信息,然后终止自身。
* 如果没有实例在运行,则启动一个新的 IPC 服务器并声称该实例。
*/
const mainProcessNodeIpcServer = await this.claimInstance(logService, environmentMainService, lifecycleMainService, instantiationService, productService, true);
// Write a lockfile to indicate an instance is running
//写一个锁文件来指示一个实例正在运行
// (https://github.com/microsoft/vscode/issues/127861#issuecomment-877417451)
FSPromises.writeFile(environmentMainService.mainLockfile, String(process.pid)).catch(err => {
logService.warn(`app#startup(): Error writing main lockfile: ${err.stack}`);
});
// Delay creation of spdlog for perf reasons (https://github.com/microsoft/vscode/issues/72906)
//延迟spdlog的创建
bufferLogger.logger = loggerService.createLogger('main', { name: localize('mainLog', "Main") });
// Lifecycle
/*负责在程序退出前释放资源并清理锁文件,确保程序能正常关闭。
* 使用Event.once监听lifecycleMainService.onWillShutdown一次性事件
* 监听到事件后释放资源并清理锁文件
* 通过evt.join()异步删除主锁文件(mainLockfile),并忽略可能的删除错误
*/
Event.once(lifecycleMainService.onWillShutdown)(evt => {
fileService.dispose();
configurationService.dispose();
evt.join('instanceLockfile', promises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ }));
});
return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup();
});
} catch (error) {
instantiationService.invokeFunction(this.quit, error);
}
}
下列这句代码使用instantiationService(依赖注入容器)创建一个CodeApplication类的实例,创建时传入两个参数:mainProcessNodeIpcServer(主进程IPC通信服务)和instanceEnvironment(实例环境配置),最后调用该实例的startup()方法启动应用
。:
return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup();
简而言之就是:创建并启动了一个VS Code类型的应用程序实例,并为其配置了进程通信和环境变量。
instantiationService
:InstantiationService 是依赖注入(DI)容器,用于创建和管理类的实例。核心功能:
依赖解析:自动解析构造函数中的依赖项
实例管理:创建并缓存单例实例
接口实现:提供createInstance()方法来实例化类
CodeApplication
:地址src\vs\code\electron-main\app.ts
CodeApplication类是 VS Code 桌面版的核心,协调了所有的底层 Electron 功能与 VS Code 自身业务逻辑的集成。负责整个 Electron 应用的初始化和生命周期管理。
核心职责
- 应用入口:作为 VS Code 主进程的入口点,管理整个应用的启动流程
注释说明"将始终只有一个实例",即使用户启动了多个实例- 服务初始化:初始化各种核心服务(窗口管理、更新、诊断、加密等)
使用依赖注入系统(IInstantiationService)管理服务实例- 安全配置:配置 Electron 会话的安全策略(权限请求、请求过滤等)
处理 UNC 访问限制(Windows 特有)
主要组件- 窗口系统:IWindowsMainService:管理主窗口
IAuxiliaryWindowsMainService:管理辅助窗口- 协议处理:处理 vscode:// 等自定义协议 URL
支持打开文件/文件夹/工作区- 生命周期管理:使用 ILifecycleMainService 管理应用的不同阶段(Ready, AfterWindowOpen, Eventually)
- 共享进程:创建和管理共享进程(SharedProcess)
通过 MessagePort 与共享进程通信
关键方法- startup():
主启动流程,包括:
设置应用标识
初始化共享进程
初始化服务集合
打开第一个窗口
进入不同生命周期阶段- openFirstWindow():
处理首次窗口打开的多种情况:
从命令行参数打开
从协议 URL 打开
从 macOS 的 dock 打开文件
安全相关:- configureSession():配置 Electron 会话的权限检查和请求过滤
- shouldBlockOpenable():检查是否应阻止可疑的文件/文件夹打开请求
4. 启动实例
查看src\vs\code\electron-main\app.ts
中startup
方法:
...
//初始化服务,IWindowsMainservice接口的实现类由此initServices可得知为WindowsMainService【步骤5中传入的】
const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady);
...
//`openFirstWindow`是启动主窗口的方法
await appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, initialProtocolUrls));
initServices
方法:
//由此句IWindowsMainservice接口的实现类由此initServices可得知为WindowsMainService
services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, devDeviceId, this.userEnv], false));
SyncDescriptor 是 VS Code 依赖注入系统 中的核心工具类,用于延迟实例化服务并管理依赖关系。存储服务类的构造函数和依赖项,当通过 InstantiationService#createInstance 请求服务时,才会执行实际构造
openFirstWindow
方法主要逻辑分为以下几点:
协议链接处理:优先检查是否有通过协议链接(如vscode://)打开的请求,若有则直接打开对应窗>口或特殊处理windowId=_blank的情况。
无参数启动:若启动时无文件/文件夹参数,根据条件(如–new-window或macOS的open-file事件)决定是否打开空窗口或处理macOS文件打开请求。
默认处理:若无特殊条件,则根据命令行参数(如–diff、–merge等)打开对应窗口。
openFirstWindow
方法:
//获取窗口管理服务实例
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
....
return windowsMainService.open({
context,
cli: args,
forceNewWindow: args['new-window'],
diffMode: args.diff, // 差异对比模式
mergeMode: args.merge,// 合并模式
noRecentEntry,
waitMarkerFileURI,
gotoLineMode: args.goto,// 行跳转模式
initialStartup: true,
remoteAuthority,
forceProfile,
forceTempProfile
});
通过accessor.get(IWindowsMainService)
获取窗口管理服务实例。
然后通过windowsMainService.open
方法统一处理不同场景的窗口打开逻辑,并考虑了远程连接、临时配置等特殊情况。
5.打开窗口
找到src\vs\platform\windows\electron-main\windowsMainService.ts
方法,查看其中的open
方法。
open
是VS Code窗口管理器的核心功能,主要处理打开窗口/文件/工作区的逻辑。
主要功能分点如下:
- 参数校验:检查addMode/removeMode是否在有效场景下使用
- 路径分类:将待打开的路径分为文件夹、工作区、文件、备份窗口等不同类型
- 特殊模式处理:处理–diff/–merge/–wait等命令行参数的特殊逻辑
- 窗口恢复:在初始启动时恢复未保存的工作区和空窗口
- 窗口聚焦:多窗口打开时智能选择聚焦窗口
- 历史记录:将打开项添加到最近使用列表(排除开发模式等特殊情况)
其中doOpen
方法在处理根据配置打开窗口。
const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, maybeOpenEmptyWindow, filesToOpen, foldersToAdd, foldersToRemove);
跟踪doOpen
方法
doOpen
主要功能是处理各种打开场景(工作区、文件夹、文件等)的窗口分配策略。
主要逻辑分5点:
- 跟踪已使用的窗口和文件打开状态(usedWindows和filesOpenedInWindow)
- 处理文件夹的添加/移除操作(通过doAddRemoveFoldersInExistingWindow)
- 优先在现有匹配窗口中打开文件(通过findWindowOnFile查找)
- 按优先级处理三种打开类型:
- 工作区(workspacesToOpen)
- 单文件夹(foldersToOpen)
- 空窗口恢复(emptyToRestore)- 最后处理剩余未打开的文件或强制空窗口情况
所有操作都遵循"是否在新窗口打开"的配置规则,并确保远程连接权限匹配。
addUsedWindow(await this.openInBrowserWindow({
userEnv: openConfig.userEnv,
cli: openConfig.cli,
initialStartup: openConfig.initialStartup,
filesToOpen,
forceNewWindow: true,
remoteAuthority: filesToOpen.remoteAuthority,
forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
forceProfile: openConfig.forceProfile,
forceTempProfile: openConfig.forceTempProfile
}), true);
addUsedWindow
:该函数用于管理窗口使用状态,并在文件打开时更新相关标记。
将传入的窗口对象(window)添加到usedWindows数组中,用于记录已使用的窗口。
如果openedFiles参数为true,则:
将当前窗口设置为filesOpenedInWindow(标记为已打开文件的窗口)
清空filesToOpen变量(表示文件已打开,无需再处理待打开文件)
6. 创建窗口
在src\vs\platform\windows\electron-main\windowsMainService.ts
跟踪openInBrowserWindow
openInBrowserWindow
的核心功能是管理VS Code窗口的生命周期和配置,打开一个新的浏览器窗口或和重用现有窗口。
主要逻辑如下:
配置获取:从服务中获取窗口配置和用户数据配置
窗口选择:根据选项决定是重用现有窗口还是创建新窗口
配置构建:组合环境变量、CLI参数等构建完整的窗口配置
窗口创建:如需新窗口,创建并设置事件监听
窗口重用:如重用现有窗口,继承部分开发相关配置
窗口加载:根据窗口状态决定直接加载或先卸载再加载。
const createdWindow = window = this.instantiationService.createInstance(CodeWindow, {
state,
extensionDevelopmentPath: configuration.extensionDevelopmentPath,
isExtensionTestHost: !!configuration.extensionTestsPath
});
this.instantiationService.createInstance
创建了一个CodeWindow的实例。
这个类型src\vs\platform\windows\electron-main\windowImpl.ts
中定义
在这个方法里完成了窗口的创建:
this._win = new electron.BrowserWindow(options);
VSCode窗口创建出来了
7.加载窗体内容
在src\vs\platform\windows\electron-main\windowImpl.ts
的openInBrowserWindow
方法继续往下读。
// 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(async veto => {
if (!veto) {
await this.doOpenInBrowserWindow(window, configuration, options, defaultProfile);
}
});
} else {
await this.doOpenInBrowserWindow(window, configuration, options, defaultProfile);
}
这段代码的功能是根据窗口状态决定如何打开浏览器窗口:
如果窗口已准备就绪(window.isReady),先尝试卸载窗口(通过lifecycleMainService.unload),如果卸载未被阻止(!veto),则调用doOpenInBrowserWindow打开新窗口
如果窗口未就绪,直接调用doOpenInBrowserWindow打开新窗口
核心逻辑:窗口准备状态不同时采用不同的打开策略,已就绪窗口需要先安全卸载再打开
追踪doOpenInBrowserWindow
方法。
这段TypeScript代码的功能是:在浏览器窗口中打开VS Code时处理备份和用户配置。主要逻辑分三点:
- 备份注册:根据工作区类型(普通工作区/单文件夹/空窗口)向备份服务注册备份路径,排除扩展开发路径的情况。
- 用户配置处理:解析并设置窗口的用户配置文件,如果是普通工作区(非扩展开发)则持久化配置关联。
- 窗口加载:最终将配置加载到目标窗口。
代码涉及工作区类型判断、异步配置解析和备份服务协作,核心是为窗口启动提供完整的初始化配置。
// Load it
window.load(configuration);
其中load
方法根据配置进行加载窗体内容。
8.加载HTML入口页面
跟踪load
放,上文6提到过在src\vs\platform\windows\electron-main\windowsMainService.ts
中this.instantiationService.createInstance
创建了一个CodeWindow的实例。
所以此处的_win对应的实现类为src\vs\platform\windows\electron-main\windowImpl.ts
。
找到其中的load
this._win.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true));
loadURL
在加载HTML入口文件。
9 构建工作区对象
在src\vs\code\electron-sandbox\workbench\workbench.ts
中加载了<script src="./workbench.js" type="module"></script>
,对应的ts便是src\vs\code\electron-sandbox\workbench\workbench.ts
。
查看这个`workbench.ts``的逻辑
通过load
方法异步加载了vs/workbench/workbench.desktop.main
模块,result返回的就是vs/workbench/workbench.desktop.main
模块。
接下来看result.main(configuration)
。
在src\vs\workbench\electron-sandbox\desktop.main.ts
的main
函数中打开了工作区。
继续追踪open
方法
/* HD624 打开工作区 */
async open(): Promise<void> {
// Init services and wait for DOM to be ready in parallel
//初始化服务并等待DOM准备好
const [services] = await Promise.all([this.initServices(), domContentLoaded(mainWindow)]);
//一旦我们有了配置服务,就尽早应用缩放级别
//在工作台创建之前,以防止闪烁。
//我们还需要尊重缩放级别可以配置
//工作空间,所以我们需要解析的配置服务。
//最后,窗口可以有一个自定义
//不是从设置中派生的缩放级别。
this.applyWindowZoomLevel(services.configurationService);
// Create Workbench
const workbench = new Workbench(mainWindow.document.body, { extraClasses: this.getExtraClasses() }, services.serviceCollection, services.logService);
// Listeners
this.registerListeners(workbench, services.storageService);
// Startup
const instantiationService = workbench.startup();
// Window
this._register(instantiationService.createInstance(NativeWindow));
}
此处的const workbench = new Workbench(mainWindow.document.body, { extraClasses: this.getExtraClasses() }, services.serviceCollection, services.logService)
构建了工作区对象。
继续查看workbench.startup()
方法做了什么事情。
workbench.startup()
是VS Code工作台启动的核心逻辑,主要功能如下:
初始化服务:配置事件监听泄漏阈值(175),创建依赖注入容器(instantiationService)
关键服务注册:获取生命周期、存储、配置等核心服务,设置默认悬浮框行为
工作台构建:初始化布局、注册编辑器工厂、添加上下文键处理器
渲染流程:注册监听器→渲染工作台→创建布局→恢复上次状态
异常时会触发全局错误处理,若启动失败会重新抛出致命错误。整个过程通过依赖注入协调各模块初始化顺序。
// Render Workbench 呈现工作台
this.renderWorkbench(instantiationService, notificationService, storageService, configurationService);
10 加载工作台内容
在src\vs\code\electron-sandbox\workbench\workbench.ts
中通过renderWorkbench
函数渲染工作台。
/*
*HD624
*这段TypeScript代码是用于渲染工作台(Workbench)的主要逻辑,主要功能包括:
*无障碍设置:配置ARIA容器和进度条信号调度器
*平台样式设置:根据操作系统和浏览器添加对应的CSS类名
*字体处理:更新字体抗锯齿设置并恢复字体缓存
*创建工作台组件:生成标题栏、活动栏、编辑器等各个UI部件
*通知处理:创建通知处理器
*DOM插入:将工作台添加到父容器中
*代码通过instantiationService等依赖注入服务来创建和管理各个组件,并考虑了不同平台和浏览器的兼容性问题。
*/
private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {
// ARIA & Signals
/*HD625
* 创建并配置一个ARIA(无障碍访问)容器,主要包含以下部分:
* 创建一个div作为ARIA容器,并添加到指定的父元素中
* 创建两个警报容器(alert),设置role = "alert"和aria - atomic="true"属性,用于紧急通知
* 创建两个状态容器(status),设置aria - live="polite"和aria - atomic="true"属性,用于非紧急状态更新
*/
setARIAContainer(this.mainContainer);
//设置一个进度条的无障碍信号调度器
setProgressAcccessibilitySignalScheduler((msDelayTime: number, msLoopTime?: number) => instantiationService.createInstance(AccessibilityProgressSignalScheduler, msDelayTime, msLoopTime));
// State specific classes
const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';
//根据运行平台、浏览器类型和额外配置类动态生成一组CSS类名
const workbenchClasses = coalesce([
'monaco-workbench',
platformClass,
isWeb ? 'web' : undefined,
isChrome ? 'chromium' : isFirefox ? 'firefox' : isSafari ? 'safari' : undefined,
...this.getLayoutClasses(),
...(this.options?.extraClasses ? this.options.extraClasses : [])
]);
this.mainContainer.classList.add(...workbenchClasses);
// Apply font aliasing,更新字体抗锯齿设置
this.updateFontAliasing(undefined, configurationService);
// Warm up font cache information before building up too many dom elements
//预热字体缓存信息,以避免在创建大量DOM元素时因字体加载导致性能问题
this.restoreFontInfo(storageService, configurationService);
// Create Parts 创建并初始化多个UI部件(parts)
for (const { id, role, classes, options } of [
{ id: Parts.TITLEBAR_PART, role: 'none', classes: ['titlebar'] },
{ id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] },
{ id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892
{ id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] },
{ id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.willRestoreEditors() } },
{ id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] },
{ id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] },
{ id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] }
]) {
const partContainer = this.createPart(id, role, classes);
mark(`code/willCreatePart/${id}`);
//从管理器中获取指定部件,并在目标容器中初始化该部件
this.getPart(id).create(partContainer, options);
mark(`code/didCreatePart/${id}`);
}
// Notification Handlers,初始化通知相关的处理器
this.createNotificationsHandlers(instantiationService, notificationService);
// Add Workbench to DOM
this.parent.appendChild(this.mainContainer);
}
回到src\vs\workbench\browser\workbench.ts
,其中的this.createWorkbenchLayout();
在构建工作区内部内容
,this.layout();
调整工作台布局。