Path: blob/main/src/vs/sessions/services/configuration/browser/configurationService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { onUnexpectedError } from '../../../../base/common/errors.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';8import { ResourceMap } from '../../../../base/common/map.js';9import { URI } from '../../../../base/common/uri.js';10import { Queue } from '../../../../base/common/async.js';11import { VSBuffer } from '../../../../base/common/buffer.js';12import { JSONPath, ParseError, parse } from '../../../../base/common/json.js';13import { applyEdits, setProperty } from '../../../../base/common/jsonEdit.js';14import { Edit, FormattingOptions } from '../../../../base/common/jsonFormatter.js';15import { equals } from '../../../../base/common/objects.js';16import { distinct, equals as arrayEquals } from '../../../../base/common/arrays.js';17import { OS, OperatingSystem } from '../../../../base/common/platform.js';18import { IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationUpdateOptions, IConfigurationUpdateOverrides, IConfigurationValue, ConfigurationTarget, isConfigurationOverrides, isConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js';19import { ConfigurationChangeEvent, ConfigurationModel } from '../../../../platform/configuration/common/configurationModels.js';20import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from '../../../../platform/configuration/common/configurations.js';21import { Extensions, IConfigurationRegistry, keyFromOverrideIdentifiers } from '../../../../platform/configuration/common/configurationRegistry.js';22import { IFileService, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js';23import { ILogService } from '../../../../platform/log/common/log.js';24import { IPolicyService, NullPolicyService } from '../../../../platform/policy/common/policy.js';25import { Registry } from '../../../../platform/registry/common/platform.js';26import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';27import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceFolder, WorkbenchState, Workspace } from '../../../../platform/workspace/common/workspace.js';28import { FolderConfiguration, UserConfiguration } from '../../../../workbench/services/configuration/browser/configuration.js';29import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, FOLDER_CONFIG_FOLDER_NAME, FOLDER_SETTINGS_PATH, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js';30import { Configuration } from '../../../../workbench/services/configuration/common/configurationModels.js';31import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js';3233// Import to register configuration contributions34import '../../../../workbench/services/configuration/browser/configurationService.js';3536export class ConfigurationService extends Disposable implements IWorkbenchConfigurationService {3738declare readonly _serviceBrand: undefined;3940private _configuration: Configuration;41private readonly defaultConfiguration: DefaultConfiguration;42private readonly policyConfiguration: IPolicyConfiguration;43private readonly userConfiguration: UserConfiguration;44private readonly cachedFolderConfigs = this._register(new DisposableMap<URI, FolderConfiguration>(new ResourceMap()));4546private readonly _onDidChangeConfiguration = this._register(new Emitter<IConfigurationChangeEvent>());47readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;4849readonly onDidChangeRestrictedSettings = Event.None;50readonly restrictedSettings: RestrictedSettings = { default: [] };5152private readonly configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);5354private readonly settingsResource: URI;55private readonly configurationEditing: ConfigurationEditing;5657constructor(58userDataProfileService: IUserDataProfileService,59private readonly workspaceService: IWorkspaceContextService,60private readonly uriIdentityService: IUriIdentityService,61private readonly fileService: IFileService,62policyService: IPolicyService,63private readonly logService: ILogService,64) {65super();6667this.settingsResource = userDataProfileService.currentProfile.settingsResource;68this.defaultConfiguration = this._register(new DefaultConfiguration(logService));69this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService));70this.userConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, userDataProfileService.currentProfile.mcpResource, {}, fileService, uriIdentityService, logService));71this.configurationEditing = new ConfigurationEditing(fileService, this);7273this._configuration = new Configuration(74ConfigurationModel.createEmptyModel(logService),75ConfigurationModel.createEmptyModel(logService),76ConfigurationModel.createEmptyModel(logService),77ConfigurationModel.createEmptyModel(logService),78ConfigurationModel.createEmptyModel(logService),79ConfigurationModel.createEmptyModel(logService),80new ResourceMap(),81ConfigurationModel.createEmptyModel(logService),82new ResourceMap<ConfigurationModel>(),83this.workspaceService.getWorkspace() as Workspace,84this.logService85);8687this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDefaultConfigurationChanged(defaults, properties)));88this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel)));89this._register(this.userConfiguration.onDidChangeConfiguration(userConfiguration => this.onUserConfigurationChanged(userConfiguration)));90this._register(this.workspaceService.onWillChangeWorkspaceFolders(e => e.join(this.loadFolderConfigurations(e.changes.added))));91this._register(this.workspaceService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));92}9394async initialize(): Promise<void> {95const [defaultModel, policyModel, userModel] = await Promise.all([96this.defaultConfiguration.initialize(),97this.policyConfiguration.initialize(),98this.userConfiguration.initialize()99]);100const workspace = this.workspaceService.getWorkspace() as Workspace;101this._configuration = new Configuration(102defaultModel,103policyModel,104ConfigurationModel.createEmptyModel(this.logService),105userModel,106ConfigurationModel.createEmptyModel(this.logService),107ConfigurationModel.createEmptyModel(this.logService),108new ResourceMap(),109ConfigurationModel.createEmptyModel(this.logService),110new ResourceMap<ConfigurationModel>(),111workspace,112this.logService113);114await this.loadFolderConfigurations(workspace.folders);115}116117// #region IWorkbenchConfigurationService118119getConfigurationData(): IConfigurationData {120return this._configuration.toData();121}122123getValue<T>(): T;124getValue<T>(section: string): T;125getValue<T>(overrides: IConfigurationOverrides): T;126getValue<T>(section: string, overrides: IConfigurationOverrides): T;127getValue(arg1?: unknown, arg2?: unknown): unknown {128const section = typeof arg1 === 'string' ? arg1 : undefined;129const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : undefined;130return this._configuration.getValue(section, overrides);131}132133updateValue(key: string, value: unknown): Promise<void>;134updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;135updateValue(key: string, value: unknown, target: ConfigurationTarget): Promise<void>;136updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise<void>;137async updateValue(key: string, value: unknown, arg3?: unknown, arg4?: unknown, _options?: IConfigurationUpdateOptions): Promise<void> {138const overrides: IConfigurationUpdateOverrides | undefined = isConfigurationUpdateOverrides(arg3) ? arg3139: isConfigurationOverrides(arg3) ? { resource: arg3.resource, overrideIdentifiers: arg3.overrideIdentifier ? [arg3.overrideIdentifier] : undefined } : undefined;140const target: ConfigurationTarget | undefined = (overrides ? arg4 : arg3) as ConfigurationTarget | undefined;141142if (overrides?.overrideIdentifiers) {143overrides.overrideIdentifiers = distinct(overrides.overrideIdentifiers);144overrides.overrideIdentifiers = overrides.overrideIdentifiers.length ? overrides.overrideIdentifiers : undefined;145}146147const inspect = this.inspect(key, { resource: overrides?.resource, overrideIdentifier: overrides?.overrideIdentifiers ? overrides.overrideIdentifiers[0] : undefined });148if (inspect.policyValue !== undefined) {149throw new Error(`Unable to write ${key} because it is configured in system policy.`);150}151152// Remove the setting, if the value is same as default value153if (equals(value, inspect.defaultValue)) {154value = undefined;155}156157if (overrides?.overrideIdentifiers?.length && overrides.overrideIdentifiers.length > 1) {158const overrideIdentifiers = overrides.overrideIdentifiers.sort();159const existingOverrides = this._configuration.localUserConfiguration.overrides.find(override => arrayEquals([...override.identifiers].sort(), overrideIdentifiers));160if (existingOverrides) {161overrides.overrideIdentifiers = existingOverrides.identifiers;162}163}164165const path = overrides?.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key];166167const settingsResource = this.getSettingsResource(target, overrides?.resource ?? undefined);168await this.configurationEditing.write(settingsResource, path, value);169await this.reloadConfiguration();170}171172private getSettingsResource(target: ConfigurationTarget | undefined, resource: URI | undefined): URI {173if (target === ConfigurationTarget.WORKSPACE_FOLDER || target === ConfigurationTarget.WORKSPACE) {174if (resource) {175const folder = this.workspaceService.getWorkspaceFolder(resource);176if (folder) {177return this.uriIdentityService.extUri.joinPath(folder.uri, FOLDER_SETTINGS_PATH);178}179}180}181return this.settingsResource;182}183184inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {185return this._configuration.inspect<T>(key, overrides);186}187188keys(): { default: string[]; policy: string[]; user: string[]; workspace: string[]; workspaceFolder: string[] } {189return this._configuration.keys();190}191192async reloadConfiguration(_target?: ConfigurationTarget | IWorkspaceFolder): Promise<void> {193const userModel = await this.userConfiguration.initialize();194const previousData = this._configuration.toData();195const change = this._configuration.compareAndUpdateLocalUserConfiguration(userModel);196197// Reload folder configurations198for (const folder of this.workspaceService.getWorkspace().folders) {199const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);200if (folderConfiguration) {201const folderModel = await folderConfiguration.loadConfiguration();202const folderChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderModel);203change.keys.push(...folderChange.keys);204change.overrides.push(...folderChange.overrides);205}206}207208this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER);209}210211hasCachedConfigurationDefaultsOverrides(): boolean {212return false;213}214215async whenRemoteConfigurationLoaded(): Promise<void> { }216217isSettingAppliedForAllProfiles(key: string): boolean {218const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope;219if (scope && APPLICATION_SCOPES.includes(scope)) {220return true;221}222const allProfilesSettings = this.getValue<string[]>(APPLY_ALL_PROFILES_SETTING) ?? [];223return Array.isArray(allProfilesSettings) && allProfilesSettings.includes(key);224}225226// #endregion227228// #region Configuration change handlers229230private onDefaultConfigurationChanged(defaults: ConfigurationModel, properties?: string[]): void {231const previousData = this._configuration.toData();232const change = this._configuration.compareAndUpdateDefaultConfiguration(defaults, properties);233this._configuration.updateLocalUserConfiguration(this.userConfiguration.reparse());234for (const folder of this.workspaceService.getWorkspace().folders) {235const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);236if (folderConfiguration) {237this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration.reparse());238}239}240this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT);241}242243private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void {244const previousData = this._configuration.toData();245const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration);246this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT);247}248249private onUserConfigurationChanged(userConfiguration: ConfigurationModel): void {250const previousData = this._configuration.toData();251const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration);252this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER);253}254255private onWorkspaceFoldersChanged(e: IWorkspaceFoldersChangeEvent): void {256// Remove configurations for removed folders257const previousData = this._configuration.toData();258const keys: string[] = [];259const overrides: [string, string[]][] = [];260for (const folder of e.removed) {261const change = this._configuration.compareAndDeleteFolderConfiguration(folder.uri);262keys.push(...change.keys);263overrides.push(...change.overrides);264this.cachedFolderConfigs.deleteAndDispose(folder.uri);265}266if (keys.length || overrides.length) {267this.triggerConfigurationChange({ keys, overrides }, previousData, ConfigurationTarget.WORKSPACE_FOLDER);268}269}270271private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder): void {272const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);273if (folderConfiguration) {274folderConfiguration.loadConfiguration().then(configurationModel => {275const previousData = this._configuration.toData();276const change = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, configurationModel);277this.triggerConfigurationChange(change, previousData, ConfigurationTarget.WORKSPACE_FOLDER);278}, onUnexpectedError);279}280}281282private async loadFolderConfigurations(folders: readonly IWorkspaceFolder[]): Promise<void> {283for (const folder of folders) {284let folderConfiguration = this.cachedFolderConfigs.get(folder.uri);285if (!folderConfiguration) {286folderConfiguration = new FolderConfiguration(false, folder, FOLDER_CONFIG_FOLDER_NAME, WorkbenchState.WORKSPACE, true, this.fileService, this.uriIdentityService, this.logService, { needsCaching: () => false, read: async () => '', write: async () => { }, remove: async () => { } });287folderConfiguration.addRelated(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder)));288this.cachedFolderConfigs.set(folder.uri, folderConfiguration);289}290const configurationModel = await folderConfiguration.loadConfiguration();291this._configuration.updateFolderConfiguration(folder.uri, configurationModel);292}293}294295private triggerConfigurationChange(change: IConfigurationChange, previousData: IConfigurationData, target: ConfigurationTarget): void {296if (change.keys.length) {297const workspace = this.workspaceService.getWorkspace() as Workspace;298const event = new ConfigurationChangeEvent(change, { data: previousData, workspace }, this._configuration, workspace, this.logService);299event.source = target;300this._onDidChangeConfiguration.fire(event);301}302}303304// #endregion305}306307class ConfigurationEditing {308309private readonly queue = new Queue<void>();310311constructor(312private readonly fileService: IFileService,313private readonly configurationService: ConfigurationService,314) { }315316write(settingsResource: URI, path: JSONPath, value: unknown): Promise<void> {317return this.queue.queue(() => this.doWriteConfiguration(settingsResource, path, value));318}319320private async doWriteConfiguration(settingsResource: URI, path: JSONPath, value: unknown): Promise<void> {321let content: string;322try {323const fileContent = await this.fileService.readFile(settingsResource);324content = fileContent.value.toString();325} catch (error) {326if ((error as FileOperationError).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {327content = '{}';328} else {329throw error;330}331}332333const parseErrors: ParseError[] = [];334parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true });335if (parseErrors.length > 0) {336throw new Error('Unable to write into the settings file. Please open the file to correct errors/warnings in the file and try again.');337}338339const edits = this.getEdits(content, path, value);340content = applyEdits(content, edits);341342await this.fileService.writeFile(settingsResource, VSBuffer.fromString(content));343}344345private getEdits(content: string, path: JSONPath, value: unknown): Edit[] {346const { tabSize, insertSpaces, eol } = this.formattingOptions;347348if (!path.length) {349const newContent = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');350return [{351content: newContent,352length: content.length,353offset: 0354}];355}356357return setProperty(content, path, value, { tabSize, insertSpaces, eol });358}359360private _formattingOptions: Required<FormattingOptions> | undefined;361private get formattingOptions(): Required<FormattingOptions> {362if (!this._formattingOptions) {363let eol = OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n';364const configuredEol = this.configurationService.getValue<string>('files.eol', { overrideIdentifier: 'jsonc' });365if (configuredEol && typeof configuredEol === 'string' && configuredEol !== 'auto') {366eol = configuredEol;367}368this._formattingOptions = {369eol,370insertSpaces: !!this.configurationService.getValue('editor.insertSpaces', { overrideIdentifier: 'jsonc' }),371tabSize: this.configurationService.getValue('editor.tabSize', { overrideIdentifier: 'jsonc' })372};373}374return this._formattingOptions;375}376}377378379