[EN] VSCode - Systems: Configuration System
Configuration System
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.
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.
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 not intuitive, showcasing various design patterns and coding styles.
VSCode’s configuration system includes numerous functionalities. Besides the most apparent “reading native configuration files,” it also involves:
- Cross-process configuration read/write handling,
- Configuration registration: how different processes register configurations, how they are managed, categorized, and unified, including handling overrides and identifying plugin configurations,
- Reading non-native configuration files (related to remote features),
- And more…
However, most of these aspects won’t be covered here 😅.
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.
- For example, I overlooked parts like
PolicyConfiguration
,WorkspaceConfiguration
,RemoteUserConfiguration
, andFolderConfiguration
inConfigurationService
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.- On the other hand, general concepts like cross-process configuration communication, default/user configuration registration, and integration are more practical and worth learning from.
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.
I divided the configuration system into the following critical components:
ConfigurationRegistry
class - Registers configurations for different software components.ConfigurationService
- Main process microservice.WorkspaceService
- Renderer process microservice.
1. Configuration Registration
Prerequisites: Familiarity with
IJSONSchema
and the concept ofRegistry
.
Let’s first introduce some basic interfaces.
IConfigurationPropertySchema
Interface
IConfigurationPropertySchema
extendsIJSONSchema
by adding new fields.- Each
IConfigurationPropertySchema
can be understood as a single configuration item.- For instance,
font size: 12px
is a single configuration.
- For instance,
IConfigurationNode
Interface
- Each node contains a set of
IConfigurationPropertySchema
(stored in theproperties
field). Thus, a node can be seen as a group of schemas or a configuration set.- Nodes can also be nested through the
allOf
field.
- Nodes can also be nested through the
IConfigurationNode
is the fundamental unit for registering default configurations inConfigurationRegistry
. The APIs inConfigurationRegistry
generally take this interface as input.
ConfigurationRegistry
Class
- VSCode uses
ConfigurationRegistry
to register configurations (specificallyIConfigurationNode
).- Note: Configurations stored here are default configurations. The registry does not support updating configurations.
The actual configuration modification microservice in VSCode is called ConfigurationService
.
Class Fields
configurationProperties
- Stores all registered configuration properties, including their default values. Related fields like
applicationSettings
,machineSettings
, andmachineOverridableSettings
categorize properties by scope. - The
getConfigurationProperties
API returns this field. Other components access configurations through this frequently used API.
- Stores all registered configuration properties, including their default values. Related fields like
configurationDefaultsOverrides
- Stores default configuration overrides. When accessing a property, the system checks for override values in this field and uses them if available.
This enables extensions or other system parts to alter default behaviors without modifying the underlying code.
configurationContributors
- Tracks registered
IConfigurationNode
instances. Configurations are added viaregister
and removed viaderegister
. ThegetConfigurations
API provides access to this field, though it’s rarely used.
- Tracks registered
2. ConfigurationService
Main Process Microservice
- Created early in the program in the main process within the
CodeMain
class (though its files are located in thecommon
directory). ConfigurationService
has minimal code as most logic is delegated to other classes.This microservice does not support the
updateValue
API, meaning configuration modifications require reloading or editing the source configuration file (JSON).- The
WorkspaceService
, introduced later, supports theupdateValue
API.
- The
Key classes:
DefaultConfiguration
,UserSettings
, andConfiguration
(I didn’t delve intoIPolicyConfiguration
as it wasn’t critical for understanding the system).- External programs fetch all configuration data from the
Configuration
class.
- External programs fetch all configuration data from the
- Below is the constructor for
ConfigurationService
to give you a sense of the setup sequence:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {
declare readonly _serviceBrand: undefined;
private configuration: Configuration;
private readonly defaultConfiguration: DefaultConfiguration;
private readonly policyConfiguration: IPolicyConfiguration;
private readonly userConfiguration: UserSettings;
private readonly reloadConfigurationScheduler: RunOnceScheduler;
private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
constructor(
private readonly settingsResource: URI,
fileService: IFileService,
policyService: IPolicyService,
logService: ILogService,
) {
super();
this.defaultConfiguration = this._register(new DefaultConfiguration());
this.policyConfiguration = policyService instanceof NullPolicyService ?
new NullPolicyConfiguration() :
this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService));
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, extUriBiasedIgnorePathCase, fileService));
this.configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel());
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50));
this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDidDefaultConfigurationChange(defaults, properties)));
this._register(this.policyConfiguration.onDidChangeConfiguration(model => this.onDidPolicyConfigurationChange(model)));
this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
}
async initialize(): Promise<void> {
const [defaultModel, policyModel, userModel] = await Promise.all([
this.defaultConfiguration.initialize(),
this.policyConfiguration.initialize(),
this.userConfiguration.loadConfiguration()
]);
this.configuration = new Configuration(defaultModel, policyModel, new ConfigurationModel(), userModel);
}
// ...
}
To understand the three classes mentioned earlier, let’s first explore their shared base class:
ConfigurationModel
.
ConfigurationModel
Class
- Stores configurations as an object.
- Adding values requires key-value pairs. Keys follow a predefined format:
- Must be strings separated by
.
. - Example:
vscode
orvscode.workspace.language
. - Data structure after adding values:
- Must be strings separated by
1
2
3
4
5
6
7
8
9
10
11
12
{
'vscode': {
size: 5,
name: 'hello world',
'workspace': {
'language': {
lang: 'C++',
restricted: true,
}
}
}
}
DefaultConfiguration
Class
- Located in
vscode\src\vs\platform\configuration\common\configurations.ts
. reset
API- Fetches all default configurations via
ConfigurationRegistry.getConfigurationProperties()
and creates aConfigurationModel
to store these defaults.
- Fetches all default configurations via
initialize
API- Calls
reset
and listens to theConfigurationRegistry.onDidUpdateConfiguration
event to keep its model updated.
- Calls
UserSettings
Class
- Takes a
settingsResource
URI and uses an embeddedConfigurationModelParser
to read the configuration file.- Parsing ensures data conforms to the schema defined in
ConfigurationRegistry
. - Invalid fields are filtered out during the parsing process.
- Parsing ensures data conforms to the schema defined in
- The parsed data is stored in a
ConfigurationModel
.
Configuration
Class
Take a look at the initial lines of this class (I believe renaming it to something like ConfigurationCollection
or AllConfiguration
would be more fitting):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class Configuration {
private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null;
private _foldersConsolidatedConfigurations = new ResourceMap<ConfigurationModel>();
constructor(
private _defaultConfiguration: ConfigurationModel,
private _policyConfiguration: ConfigurationModel,
private _applicationConfiguration: ConfigurationModel,
private _localUserConfiguration: ConfigurationModel,
private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(),
private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(),
private _folderConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(),
private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(),
private _memoryConfigurationByResource: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>()
) {
}
// ...
}
- During
ConfigurationService.initialize()
,DefaultConfiguration
andUserSettings
provide theirConfigurationModel
instances to this class. - The
Configuration
class consolidates all models based on theDefaultConfiguration
model. Other configurations are merged into it. - While verbose, most of the code handles integration and merging.
3. WorkspaceService
Renderer Process Microservice
- Only appears in the renderer process, specifically in
web.main.ts
anddesktop.main.ts
. - Despite its name,
WorkspaceService
is essentially the renderer process version ofConfigurationService
. - Unlike
ConfigurationService
, this service supports theupdateValue
API. See theConfigurationEditing
class for details.
ConfigurationCache
Class
- Since reading/writing configuration files in the renderer process (native or non-native/remote) involves asynchronous operations, all its interfaces return Promises.
1
2
3
4
5
6
7
8
export type ConfigurationKey = { type: 'defaults' | 'user' | 'workspaces' | 'folder'; key: string; };
export interface IConfigurationCache {
needsCaching(resource: URI): boolean;
read(key: ConfigurationKey): Promise<string>;
write(key: ConfigurationKey, content: string): Promise<void>;
remove(key: ConfigurationKey): Promise<void>;
}
- A simple utility class offering
read
,write
, andremove
APIs for non-native configuration files.- Operates on the entire file (
content
inwrite
is the full file content). - Each non-native configuration file is paired with a dedicated queue to ensure read/write operations are sequential.
- Used by classes like
RemoteUserConfiguration
,WorkspaceConfiguration
, andFolderConfiguration
. DefaultConfiguration
andUserConfiguration
rarely use this as their data is usually native.
- Operates on the entire file (
Only appears in the renderer process, specifically in
web.main.ts
anddesktop.main.ts
.- In
desktop.main.ts
, it’s constructed like this:
- In
1
2
const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non-native resources */, /* ... */);
const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, /* ... */);
DefaultConfiguration
Class
- Located in
vscode\src\vs\workbench\services\configuration\browser\configuration.ts
. - Extends the
DefaultConfiguration
class from\common\
but with no significant changes worth noting.
UserConfiguration
Class
- This class resides in the
browser
directory and primarily wraps theUserSettings
class from thecommon
folder. - Notable during reloads: the embedded
UserSettings
can be replaced with aFileServiceBasedConfiguration
. The exact purpose of this was not explored.
Configuration
Class
- The renderer process uses the same
Configuration
class as the main process, so there’s no need for repetition here.
ConfigurationEditing
Class - How Configurations Are Edited
WorkspaceService
enables updating configurations usingupdateValue
, which relies on the key-value pair mechanism. The core function iswriteConfigurationValue
.- Does not support modifying default configurations.
- Internally,
writeConfigurationValue
initializes aConfigurationEditing
instance and calls its API.
ConfigurationEditing
exposes a single API calledwriteConfiguration
.