Post

[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, 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.
  • 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:

  1. ConfigurationRegistry class - Registers configurations for different software components.
  2. ConfigurationService - Main process microservice.
  3. WorkspaceService - Renderer process microservice.

1. Configuration Registration

Prerequisites: Familiarity with IJSONSchema and the concept of Registry.

Let’s first introduce some basic interfaces.

IConfigurationPropertySchema Interface

  • IConfigurationPropertySchema extends IJSONSchema by adding new fields.
  • Each IConfigurationPropertySchema can be understood as a single configuration item.
    • For instance, font size: 12px is a single configuration.

IConfigurationNode Interface

  • 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.
    • Nodes can also be nested through the allOf field.
  • IConfigurationNode is the fundamental unit for registering default configurations in ConfigurationRegistry. The APIs in ConfigurationRegistry generally take this interface as input.

ConfigurationRegistry Class

  • VSCode uses ConfigurationRegistry to register configurations (specifically IConfigurationNode).
    • 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, and machineOverridableSettings categorize properties by scope.
    • The getConfigurationProperties API returns this field. Other components access configurations through this frequently used API.
  • 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 via register and removed via deregister. The getConfigurations API provides access to this field, though it’s rarely used.

2. ConfigurationService Main Process Microservice

  • Created early in the program in the main process within the CodeMain class (though its files are located in the common 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 the updateValue API.
  • Key classes: DefaultConfiguration, UserSettings, and Configuration (I didn’t delve into IPolicyConfiguration as it wasn’t critical for understanding the system).

    • External programs fetch all configuration data from the Configuration class.
  • 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 or vscode.workspace.language.
    • Data structure after adding values:
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 a ConfigurationModel to store these defaults.
  • initialize API
    • Calls reset and listens to the ConfigurationRegistry.onDidUpdateConfiguration event to keep its model updated.

UserSettings Class

  • Takes a settingsResource URI and uses an embedded ConfigurationModelParser to read the configuration file.
    • Parsing ensures data conforms to the schema defined in ConfigurationRegistry.
    • Invalid fields are filtered out during the parsing process.
  • 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 and UserSettings provide their ConfigurationModel instances to this class.
  • The Configuration class consolidates all models based on the DefaultConfiguration 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 and desktop.main.ts.
  • Despite its name, WorkspaceService is essentially the renderer process version of ConfigurationService.
  • Unlike ConfigurationService, this service supports the updateValue API. See the ConfigurationEditing 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, and remove APIs for non-native configuration files.
    • Operates on the entire file (content in write 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, and FolderConfiguration.
    • DefaultConfiguration and UserConfiguration rarely use this as their data is usually native.
  • Only appears in the renderer process, specifically in web.main.ts and desktop.main.ts.

    • In desktop.main.ts, it’s constructed like this:
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 the UserSettings class from the common folder.
  • Notable during reloads: the embedded UserSettings can be replaced with a FileServiceBasedConfiguration. 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 using updateValue, which relies on the key-value pair mechanism. The core function is writeConfigurationValue.
    • Does not support modifying default configurations.
    • Internally, writeConfigurationValue initializes a ConfigurationEditing instance and calls its API.
  • ConfigurationEditing exposes a single API called writeConfiguration.
This post is licensed under CC BY 4.0 by the author.