|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "[EN] VSCode Series [System]: Configuration System" |
| 4 | +categories: [VSCode, Systems] |
| 5 | +--- |
| 6 | + |
| 7 | +# Configuration System |
| 8 | + |
| 9 | +> Before diving in, let me just say, I personally believe the configuration system is among the top 10 or even top 5 most complex systems in VSCode. |
| 10 | +> |
| 11 | +> The business logic is intricate and tedious, and the codebase is extensive. Some parts are so challenging that without GPT-4’s help, it would be hard to comprehend them entirely. |
| 12 | +> |
| 13 | +> After reading through the basic framework, it feels like this system evolved over time as the project progressed, resulting in its current state. Many names and designs are <u>not intuitive</u>, showcasing various design patterns and coding styles. |
| 14 | +> |
| 15 | +> VSCode's configuration system includes numerous functionalities. Besides the most apparent "reading native configuration files," it also involves: |
| 16 | +> |
| 17 | +> - Cross-process configuration read/write handling, |
| 18 | +> - Configuration registration: how different processes register configurations, how they are managed, categorized, and unified, including handling overrides and identifying plugin configurations, |
| 19 | +> - Reading non-native configuration files (related to remote features), |
| 20 | +> - And more... |
| 21 | +> |
| 22 | +> However, most of these aspects won't be covered here 😅. |
| 23 | +> |
| 24 | +> Since my learning process was selective, I didn’t read the entire system systematically. Naturally, I skipped parts that I didn’t find immediately relevant. |
| 25 | +> |
| 26 | +> - For example, I overlooked parts like `PolicyConfiguration`, `WorkspaceConfiguration`, `RemoteUserConfiguration`, and `FolderConfiguration` in `ConfigurationService` because they are deeply tied to VSCode as a product. When designing your own configuration system, you can decide how to structure it without necessarily referencing VSCode. |
| 27 | +> - On the other hand, general concepts like cross-process configuration communication, default/user configuration registration, and integration are more practical and worth learning from. |
| 28 | +> |
| 29 | +> My writing style is technical and focused on low-level details. I didn’t simplify much or summarize broadly, instead describing the code as it is. Apologies if it's dry—I may write a summary later. |
| 30 | +
|
| 31 | +I divided the configuration system into the following critical components: |
| 32 | + |
| 33 | +1. `ConfigurationRegistry` class - Registers configurations for different software components. |
| 34 | +2. `ConfigurationService` - Main process microservice. |
| 35 | +3. `WorkspaceService` - Renderer process microservice. |
| 36 | + |
| 37 | +## 1. Configuration Registration |
| 38 | + |
| 39 | +> Prerequisites: Familiarity with `IJSONSchema` and the concept of `Registry`. |
| 40 | +
|
| 41 | +Let’s first introduce some basic interfaces. |
| 42 | + |
| 43 | +### `IConfigurationPropertySchema` Interface |
| 44 | + |
| 45 | +- `IConfigurationPropertySchema` extends `IJSONSchema` by adding new fields. |
| 46 | +- Each `IConfigurationPropertySchema` can be understood as a single configuration item. |
| 47 | + - For instance, `font size: 12px` is a single configuration. |
| 48 | + |
| 49 | +### `IConfigurationNode` Interface |
| 50 | + |
| 51 | +- Each node contains a set of `IConfigurationPropertySchema` (stored in the `properties` field). Thus, a node can be seen as a group of schemas or a configuration set. |
| 52 | + - Nodes can also be nested through the `allOf` field. |
| 53 | +- `IConfigurationNode` is the fundamental unit for registering default configurations in `ConfigurationRegistry`. The APIs in `ConfigurationRegistry` generally take this interface as input. |
| 54 | + |
| 55 | +### `ConfigurationRegistry` Class |
| 56 | + |
| 57 | +- VSCode uses `ConfigurationRegistry` to register configurations (specifically `IConfigurationNode`). |
| 58 | + - **Note:** Configurations stored here are default configurations. The registry does not support updating configurations. |
| 59 | + |
| 60 | +The actual configuration modification microservice in VSCode is called `ConfigurationService`. |
| 61 | + |
| 62 | +#### Class Fields |
| 63 | + |
| 64 | +- `configurationProperties` |
| 65 | + |
| 66 | + - Stores all registered configuration properties, including their default values. Related fields like `applicationSettings`, `machineSettings`, and `machineOverridableSettings` categorize properties by scope. |
| 67 | + - The `getConfigurationProperties` API returns this field. Other components access configurations through this frequently used API. |
| 68 | + |
| 69 | +- `configurationDefaultsOverrides` |
| 70 | + |
| 71 | + - Stores default configuration overrides. When accessing a property, the system checks for override values in this field and uses them if available. |
| 72 | + |
| 73 | + > This enables extensions or other system parts to alter default behaviors without modifying the underlying code. |
| 74 | +
|
| 75 | +- `configurationContributors` |
| 76 | + |
| 77 | + - Tracks registered `IConfigurationNode` instances. Configurations are added via `register` and removed via `deregister`. The `getConfigurations` API provides access to this field, though it’s rarely used. |
| 78 | + |
| 79 | +## 2. `ConfigurationService` Main Process Microservice |
| 80 | + |
| 81 | +- Created early in the program in the **main process** within the `CodeMain` class (though its files are located in the `common` directory). |
| 82 | +- `ConfigurationService` has minimal code as most logic is delegated to other classes. |
| 83 | + |
| 84 | +- This microservice does **not support** the `updateValue` API, meaning configuration modifications require reloading or editing the source configuration file (JSON). |
| 85 | + |
| 86 | + - The `WorkspaceService`, introduced later, supports the `updateValue` API. |
| 87 | + |
| 88 | +- Key classes: `DefaultConfiguration`, `UserSettings`, and `Configuration` (I didn’t delve into `IPolicyConfiguration` as it wasn’t critical for understanding the system). |
| 89 | + |
| 90 | + - External programs fetch all configuration data from the `Configuration` class. |
| 91 | + |
| 92 | +- Below is the constructor for `ConfigurationService` to give you a sense of the setup sequence: |
| 93 | + |
| 94 | +```typescript |
| 95 | +export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { |
| 96 | + declare readonly _serviceBrand: undefined; |
| 97 | + |
| 98 | + private configuration: Configuration; |
| 99 | + private readonly defaultConfiguration: DefaultConfiguration; |
| 100 | + private readonly policyConfiguration: IPolicyConfiguration; |
| 101 | + private readonly userConfiguration: UserSettings; |
| 102 | + private readonly reloadConfigurationScheduler: RunOnceScheduler; |
| 103 | + |
| 104 | + private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>()); |
| 105 | + readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event; |
| 106 | + |
| 107 | + constructor( |
| 108 | + private readonly settingsResource: URI, |
| 109 | + fileService: IFileService, |
| 110 | + policyService: IPolicyService, |
| 111 | + logService: ILogService, |
| 112 | + ) { |
| 113 | + super(); |
| 114 | + this.defaultConfiguration = this._register(new DefaultConfiguration()); |
| 115 | + this.policyConfiguration = policyService instanceof NullPolicyService ? |
| 116 | + new NullPolicyConfiguration() : |
| 117 | + this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); |
| 118 | + this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService)); |
| 119 | + this.configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel()); |
| 120 | + |
| 121 | + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50)); |
| 122 | + this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDidDefaultConfigurationChange(defaults, properties))); |
| 123 | + this._register(this.policyConfiguration.onDidChangeConfiguration(model => this.onDidPolicyConfigurationChange(model))); |
| 124 | + this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule())); |
| 125 | + } |
| 126 | + |
| 127 | + async initialize(): Promise<void> { |
| 128 | + const [defaultModel, policyModel, userModel] = await Promise.all([ |
| 129 | + this.defaultConfiguration.initialize(), |
| 130 | + this.policyConfiguration.initialize(), |
| 131 | + this.userConfiguration.loadConfiguration() |
| 132 | + ]); |
| 133 | + this.configuration = new Configuration(defaultModel, policyModel, new ConfigurationModel(), userModel); |
| 134 | + } |
| 135 | + // ... |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +> To understand the three classes mentioned earlier, let’s first explore their shared base class: `ConfigurationModel`. |
| 140 | +
|
| 141 | +#### `ConfigurationModel` Class |
| 142 | + |
| 143 | +- Stores configurations as an object. |
| 144 | +- Adding values requires key-value pairs. Keys follow a predefined format: |
| 145 | + - Must be strings separated by `.`. |
| 146 | + - Example: `vscode` or `vscode.workspace.language`. |
| 147 | + - Data structure after adding values: |
| 148 | + |
| 149 | +```typescript |
| 150 | +{ |
| 151 | + 'vscode': { |
| 152 | + size: 5, |
| 153 | + name: 'hello world', |
| 154 | + 'workspace': { |
| 155 | + 'language': { |
| 156 | + lang: 'C++', |
| 157 | + restricted: true, |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +#### `DefaultConfiguration` Class |
| 165 | + |
| 166 | +- Located in `vscode\src\vs\platform\configuration\common\configurations.ts`. |
| 167 | +- **`reset` API** |
| 168 | + - Fetches all default configurations via `ConfigurationRegistry.getConfigurationProperties()` and creates a `ConfigurationModel` to store these defaults. |
| 169 | +- **`initialize` API** |
| 170 | + - Calls `reset` and listens to the `ConfigurationRegistry.onDidUpdateConfiguration` event to keep its model updated. |
| 171 | + |
| 172 | +#### `UserSettings` Class |
| 173 | + |
| 174 | +- Takes a `settingsResource` URI and uses an embedded `ConfigurationModelParser` to read the configuration file. |
| 175 | + - Parsing ensures data conforms to the schema defined in `ConfigurationRegistry`. |
| 176 | + - Invalid fields are filtered out during the parsing process. |
| 177 | +- The parsed data is stored in a `ConfigurationModel`. |
| 178 | + |
| 179 | +#### `Configuration` Class |
| 180 | + |
| 181 | +Take a look at the initial lines of this class (I believe renaming it to something like `ConfigurationCollection` or `AllConfiguration` would be more fitting): |
| 182 | + |
| 183 | +```typescript |
| 184 | +export class Configuration { |
| 185 | + |
| 186 | + private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null; |
| 187 | + private _foldersConsolidatedConfigurations = new ResourceMap<ConfigurationModel>(); |
| 188 | + |
| 189 | + constructor( |
| 190 | + private _defaultConfiguration: ConfigurationModel, |
| 191 | + private _policyConfiguration: ConfigurationModel, |
| 192 | + private _applicationConfiguration: ConfigurationModel, |
| 193 | + private _localUserConfiguration: ConfigurationModel, |
| 194 | + private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), |
| 195 | + private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), |
| 196 | + private _folderConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(), |
| 197 | + private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), |
| 198 | + private _memoryConfigurationByResource: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>() |
| 199 | + ) { |
| 200 | + } |
| 201 | + // ... |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +- During `ConfigurationService.initialize()`, `DefaultConfiguration` and `UserSettings` provide their `ConfigurationModel` instances to this class. |
| 206 | +- The `Configuration` class consolidates all models based on the `DefaultConfiguration` model. Other configurations are merged into it. |
| 207 | +- While verbose, most of the code handles integration and merging. |
| 208 | + |
| 209 | +## 3. `WorkspaceService` Renderer Process Microservice |
| 210 | + |
| 211 | +- <u>Only appears in the renderer process</u>, specifically in `web.main.ts` and `desktop.main.ts`. |
| 212 | +- Despite its name, `WorkspaceService` is essentially the renderer process version of `ConfigurationService`. |
| 213 | +- Unlike `ConfigurationService`, this service supports the `updateValue` API. See the `ConfigurationEditing` class for details. |
| 214 | + |
| 215 | +### `ConfigurationCache` Class |
| 216 | + |
| 217 | +- Since reading/writing configuration files in the renderer process (native or non-native/remote) involves asynchronous operations, all its interfaces return Promises. |
| 218 | + |
| 219 | +```typescript |
| 220 | +export type ConfigurationKey = { type: 'defaults' | 'user' | 'workspaces' | 'folder'; key: string; }; |
| 221 | + |
| 222 | +export interface IConfigurationCache { |
| 223 | + needsCaching(resource: URI): boolean; |
| 224 | + read(key: ConfigurationKey): Promise<string>; |
| 225 | + write(key: ConfigurationKey, content: string): Promise<void>; |
| 226 | + remove(key: ConfigurationKey): Promise<void>; |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +- A simple utility class offering `read`, `write`, and `remove` APIs for non-native configuration files. |
| 231 | + - Operates on the entire file (`content` in `write` is the full file content). |
| 232 | + - Each non-native configuration file is paired with a dedicated queue to ensure read/write operations are sequential. |
| 233 | + - Used by classes like `RemoteUserConfiguration`, `WorkspaceConfiguration`, and `FolderConfiguration`. |
| 234 | + - `DefaultConfiguration` and `UserConfiguration` rarely use this as their data is usually native. |
| 235 | + |
| 236 | +- <u>Only appears in the renderer process</u>, specifically in `web.main.ts` and `desktop.main.ts`. |
| 237 | + |
| 238 | + - In `desktop.main.ts`, it's constructed like this: |
| 239 | + |
| 240 | +```typescript |
| 241 | +const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non-native resources */, /* ... */); |
| 242 | +const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, /* ... */); |
| 243 | +``` |
| 244 | + |
| 245 | +### `DefaultConfiguration` Class |
| 246 | + |
| 247 | +- Located in `vscode\src\vs\workbench\services\configuration\browser\configuration.ts`. |
| 248 | +- Extends the `DefaultConfiguration` class from `\common\` but with no significant changes worth noting. |
| 249 | + |
| 250 | +### `UserConfiguration` Class |
| 251 | + |
| 252 | +- This class resides in the `browser` directory and primarily wraps the `UserSettings` class from the `common` folder. |
| 253 | +- Notable during reloads: the embedded `UserSettings` can be replaced with a `FileServiceBasedConfiguration`. The exact purpose of this was not explored. |
| 254 | + |
| 255 | +### `Configuration` Class |
| 256 | + |
| 257 | +- The renderer process uses the same `Configuration` class as the main process, so there’s no need for repetition here. |
| 258 | + |
| 259 | +### `ConfigurationEditing` Class - How Configurations Are Edited |
| 260 | + |
| 261 | +- `WorkspaceService` enables updating configurations using `updateValue`, which relies on the key-value pair mechanism. The core function is `writeConfigurationValue`. |
| 262 | + - Does not support modifying default configurations. |
| 263 | + - Internally, `writeConfigurationValue` initializes a `ConfigurationEditing` instance and calls its API. |
| 264 | +- `ConfigurationEditing` exposes a single API called `writeConfiguration`. |
0 commit comments