Path: blob/main/src/vs/platform/configuration/common/configurationService.ts
3296 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 { distinct, equals as arrayEquals } from '../../../base/common/arrays.js';6import { Queue, RunOnceScheduler } from '../../../base/common/async.js';7import { VSBuffer } from '../../../base/common/buffer.js';8import { Emitter, Event } from '../../../base/common/event.js';9import { JSONPath, ParseError, parse } from '../../../base/common/json.js';10import { applyEdits, setProperty } from '../../../base/common/jsonEdit.js';11import { Edit, FormattingOptions } from '../../../base/common/jsonFormatter.js';12import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';13import { ResourceMap } from '../../../base/common/map.js';14import { equals } from '../../../base/common/objects.js';15import { OS, OperatingSystem } from '../../../base/common/platform.js';16import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';17import { URI } from '../../../base/common/uri.js';18import { ConfigurationTarget, IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationUpdateOptions, IConfigurationUpdateOverrides, IConfigurationValue, isConfigurationOverrides, isConfigurationUpdateOverrides } from './configuration.js';19import { Configuration, ConfigurationChangeEvent, ConfigurationModel, UserSettings } from './configurationModels.js';20import { keyFromOverrideIdentifiers } from './configurationRegistry.js';21import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from './configurations.js';22import { FileOperationError, FileOperationResult, IFileService } from '../../files/common/files.js';23import { ILogService } from '../../log/common/log.js';24import { IPolicyService, NullPolicyService } from '../../policy/common/policy.js';2526export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable {2728declare readonly _serviceBrand: undefined;2930private configuration: Configuration;31private readonly defaultConfiguration: DefaultConfiguration;32private readonly policyConfiguration: IPolicyConfiguration;33private readonly userConfiguration: UserSettings;34private readonly reloadConfigurationScheduler: RunOnceScheduler;3536private readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());37readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;3839private readonly configurationEditing: ConfigurationEditing;4041constructor(42private readonly settingsResource: URI,43fileService: IFileService,44policyService: IPolicyService,45private readonly logService: ILogService,46) {47super();48this.defaultConfiguration = this._register(new DefaultConfiguration(logService));49this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService));50this.userConfiguration = this._register(new UserSettings(this.settingsResource, {}, extUriBiasedIgnorePathCase, fileService, logService));51this.configuration = new Configuration(52this.defaultConfiguration.configurationModel,53this.policyConfiguration.configurationModel,54ConfigurationModel.createEmptyModel(logService),55ConfigurationModel.createEmptyModel(logService),56ConfigurationModel.createEmptyModel(logService),57ConfigurationModel.createEmptyModel(logService),58new ResourceMap<ConfigurationModel>(),59ConfigurationModel.createEmptyModel(logService),60new ResourceMap<ConfigurationModel>(),61logService62);63this.configurationEditing = new ConfigurationEditing(settingsResource, fileService, this);6465this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reloadConfiguration(), 50));66this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDidDefaultConfigurationChange(defaults, properties)));67this._register(this.policyConfiguration.onDidChangeConfiguration(model => this.onDidPolicyConfigurationChange(model)));68this._register(this.userConfiguration.onDidChange(() => this.reloadConfigurationScheduler.schedule()));69}7071async initialize(): Promise<void> {72const [defaultModel, policyModel, userModel] = await Promise.all([this.defaultConfiguration.initialize(), this.policyConfiguration.initialize(), this.userConfiguration.loadConfiguration()]);73this.configuration = new Configuration(74defaultModel,75policyModel,76ConfigurationModel.createEmptyModel(this.logService),77userModel,78ConfigurationModel.createEmptyModel(this.logService),79ConfigurationModel.createEmptyModel(this.logService),80new ResourceMap<ConfigurationModel>(),81ConfigurationModel.createEmptyModel(this.logService),82new ResourceMap<ConfigurationModel>(),83this.logService84);85}8687getConfigurationData(): IConfigurationData {88return this.configuration.toData();89}9091getValue<T>(): T;92getValue<T>(section: string): T;93getValue<T>(overrides: IConfigurationOverrides): T;94getValue<T>(section: string, overrides: IConfigurationOverrides): T;95getValue(arg1?: any, arg2?: any): any {96const section = typeof arg1 === 'string' ? arg1 : undefined;97const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : {};98return this.configuration.getValue(section, overrides, undefined);99}100101updateValue(key: string, value: any): Promise<void>;102updateValue(key: string, value: any, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;103updateValue(key: string, value: any, target: ConfigurationTarget): Promise<void>;104updateValue(key: string, value: any, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise<void>;105async updateValue(key: string, value: any, arg3?: any, arg4?: any, options?: any): Promise<void> {106const overrides: IConfigurationUpdateOverrides | undefined = isConfigurationUpdateOverrides(arg3) ? arg3107: isConfigurationOverrides(arg3) ? { resource: arg3.resource, overrideIdentifiers: arg3.overrideIdentifier ? [arg3.overrideIdentifier] : undefined } : undefined;108109const target: ConfigurationTarget | undefined = overrides ? arg4 : arg3;110if (target !== undefined) {111if (target !== ConfigurationTarget.USER_LOCAL && target !== ConfigurationTarget.USER) {112throw new Error(`Unable to write ${key} to target ${target}.`);113}114}115116if (overrides?.overrideIdentifiers) {117overrides.overrideIdentifiers = distinct(overrides.overrideIdentifiers);118overrides.overrideIdentifiers = overrides.overrideIdentifiers.length ? overrides.overrideIdentifiers : undefined;119}120121const inspect = this.inspect(key, { resource: overrides?.resource, overrideIdentifier: overrides?.overrideIdentifiers ? overrides.overrideIdentifiers[0] : undefined });122if (inspect.policyValue !== undefined) {123throw new Error(`Unable to write ${key} because it is configured in system policy.`);124}125126// Remove the setting, if the value is same as default value127if (equals(value, inspect.defaultValue)) {128value = undefined;129}130131if (overrides?.overrideIdentifiers?.length && overrides.overrideIdentifiers.length > 1) {132const overrideIdentifiers = overrides.overrideIdentifiers.sort();133const existingOverrides = this.configuration.localUserConfiguration.overrides.find(override => arrayEquals([...override.identifiers].sort(), overrideIdentifiers));134if (existingOverrides) {135overrides.overrideIdentifiers = existingOverrides.identifiers;136}137}138139const path = overrides?.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key];140141await this.configurationEditing.write(path, value);142await this.reloadConfiguration();143}144145inspect<T>(key: string, overrides: IConfigurationOverrides = {}): IConfigurationValue<T> {146return this.configuration.inspect<T>(key, overrides, undefined);147}148149keys(): {150default: string[];151user: string[];152workspace: string[];153workspaceFolder: string[];154} {155return this.configuration.keys(undefined);156}157158async reloadConfiguration(): Promise<void> {159const configurationModel = await this.userConfiguration.loadConfiguration();160this.onDidChangeUserConfiguration(configurationModel);161}162163private onDidChangeUserConfiguration(userConfigurationModel: ConfigurationModel): void {164const previous = this.configuration.toData();165const change = this.configuration.compareAndUpdateLocalUserConfiguration(userConfigurationModel);166this.trigger(change, previous, ConfigurationTarget.USER);167}168169private onDidDefaultConfigurationChange(defaultConfigurationModel: ConfigurationModel, properties: string[]): void {170const previous = this.configuration.toData();171const change = this.configuration.compareAndUpdateDefaultConfiguration(defaultConfigurationModel, properties);172this.trigger(change, previous, ConfigurationTarget.DEFAULT);173}174175private onDidPolicyConfigurationChange(policyConfiguration: ConfigurationModel): void {176const previous = this.configuration.toData();177const change = this.configuration.compareAndUpdatePolicyConfiguration(policyConfiguration);178this.trigger(change, previous, ConfigurationTarget.DEFAULT);179}180181private trigger(configurationChange: IConfigurationChange, previous: IConfigurationData, source: ConfigurationTarget): void {182const event = new ConfigurationChangeEvent(configurationChange, { data: previous }, this.configuration, undefined, this.logService);183event.source = source;184this._onDidChangeConfiguration.fire(event);185}186}187188class ConfigurationEditing {189190private readonly queue: Queue<void>;191192constructor(193private readonly settingsResource: URI,194private readonly fileService: IFileService,195private readonly configurationService: IConfigurationService,196) {197this.queue = new Queue<void>();198}199200write(path: JSONPath, value: any): Promise<void> {201return this.queue.queue(() => this.doWriteConfiguration(path, value)); // queue up writes to prevent race conditions202}203204private async doWriteConfiguration(path: JSONPath, value: any): Promise<void> {205let content: string;206try {207const fileContent = await this.fileService.readFile(this.settingsResource);208content = fileContent.value.toString();209} catch (error) {210if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {211content = '{}';212} else {213throw error;214}215}216217const parseErrors: ParseError[] = [];218parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true });219if (parseErrors.length > 0) {220throw new Error('Unable to write into the settings file. Please open the file to correct errors/warnings in the file and try again.');221}222223const edits = this.getEdits(content, path, value);224content = applyEdits(content, edits);225226await this.fileService.writeFile(this.settingsResource, VSBuffer.fromString(content));227}228229private getEdits(content: string, path: JSONPath, value: any): Edit[] {230const { tabSize, insertSpaces, eol } = this.formattingOptions;231232// With empty path the entire file is being replaced, so we just use JSON.stringify233if (!path.length) {234const content = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');235return [{236content,237length: content.length,238offset: 0239}];240}241242return setProperty(content, path, value, { tabSize, insertSpaces, eol });243}244245private _formattingOptions: Required<FormattingOptions> | undefined;246private get formattingOptions(): Required<FormattingOptions> {247if (!this._formattingOptions) {248let eol = OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n';249const configuredEol = this.configurationService.getValue('files.eol', { overrideIdentifier: 'jsonc' });250if (configuredEol && typeof configuredEol === 'string' && configuredEol !== 'auto') {251eol = configuredEol;252}253this._formattingOptions = {254eol,255insertSpaces: !!this.configurationService.getValue('editor.insertSpaces', { overrideIdentifier: 'jsonc' }),256tabSize: this.configurationService.getValue('editor.tabSize', { overrideIdentifier: 'jsonc' })257};258}259return this._formattingOptions;260}261}262263264