import { ResourceMap } from '../../../../vs/base/common/map'; import * as arrays from '../../../../vs/base/common/arrays'; import * as types from '../../../../vs/base/common/types'; import * as objects from '../../../../vs/base/common/objects'; import { URI, UriComponents } from '../../../../vs/base/common/uri'; import { OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey, } from '../../../../vs/platform/configuration/common/configurationRegistry'; import { IOverrides, addToValueTree, toValuesTree, IConfigurationModel, getConfigurationValue, IConfigurationOverrides, IConfigurationData, getDefaultValues, getConfigurationKeys, removeFromValueTree, ConfigurationTarget, IConfigurationChangeEvent, IConfigurationChange, } from '../../../../vs/platform/configuration/common/configuration'; import { Workspace } from '../../../../vs/platform/workspace/common/workspace'; export class ConfigurationModel implements IConfigurationModel { private isFrozen: boolean = false; constructor( private _contents: any = {}, private _keys: string[] = [], private _overrides: IOverrides[] = [] ) {} get contents(): any { return this.checkAndFreeze(this._contents); } get overrides(): IOverrides[] { return this.checkAndFreeze(this._overrides); } get keys(): string[] { return this.checkAndFreeze(this._keys); } isEmpty(): boolean { return ( this._keys.length === 0 && Object.keys(this._contents).length === 0 && this._overrides.length === 0 ); } getValue(section: string | undefined): V { return section ? getConfigurationValue(this.contents, section) : this.contents; } override(identifier: string): ConfigurationModel { const overrideContents = this.getContentsForOverrideIdentifer(identifier); if ( !overrideContents || typeof overrideContents !== 'object' || !Object.keys(overrideContents).length ) { // If there are no valid overrides, return self return this; } let contents: any = {}; for (const key of arrays.distinct([ ...Object.keys(this.contents), ...Object.keys(overrideContents), ])) { let contentsForKey = this.contents[key]; let overrideContentsForKey = overrideContents[key]; // If there are override contents for the key, clone and merge otherwise use base contents if (overrideContentsForKey) { // Clone and merge only if base contents and override contents are of type object otherwise just override if ( typeof contentsForKey === 'object' && typeof overrideContentsForKey === 'object' ) { contentsForKey = objects.deepClone(contentsForKey); this.mergeContents(contentsForKey, overrideContentsForKey); } else { contentsForKey = overrideContentsForKey; } } contents[key] = contentsForKey; } return new ConfigurationModel(contents, this.keys, this.overrides); } merge(...others: ConfigurationModel[]): ConfigurationModel { const contents = objects.deepClone(this.contents); const overrides = objects.deepClone(this.overrides); const keys = [...this.keys]; for (const other of others) { this.mergeContents(contents, other.contents); for (const otherOverride of other.overrides) { const [override] = overrides.filter((o) => arrays.equals(o.identifiers, otherOverride.identifiers) ); if (override) { this.mergeContents(override.contents, otherOverride.contents); } else { overrides.push(objects.deepClone(otherOverride)); } } for (const key of other.keys) { if (keys.indexOf(key) === -1) { keys.push(key); } } } return new ConfigurationModel(contents, keys, overrides); } freeze(): ConfigurationModel { this.isFrozen = true; return this; } private mergeContents(source: any, target: any): void { for (const key of Object.keys(target)) { if (key in source) { if (types.isObject(source[key]) && types.isObject(target[key])) { this.mergeContents(source[key], target[key]); continue; } } source[key] = objects.deepClone(target[key]); } } private checkAndFreeze(data: T): T { if (this.isFrozen && !Object.isFrozen(data)) { return objects.deepFreeze(data); } return data; } private getContentsForOverrideIdentifer(identifier: string): any { for (const override of this.overrides) { if (override.identifiers.indexOf(identifier) !== -1) { return override.contents; } } return null; } toJSON(): IConfigurationModel { return { contents: this.contents, overrides: this.overrides, keys: this.keys, }; } // Update methods public setValue(key: string, value: any) { this.addKey(key); addToValueTree(this.contents, key, value, (e) => { throw new Error(e); }); } public removeValue(key: string): void { if (this.removeKey(key)) { removeFromValueTree(this.contents, key); } } private addKey(key: string): void { let index = this.keys.length; for (let i = 0; i < index; i++) { if (key.indexOf(this.keys[i]) === 0) { index = i; } } this.keys.splice(index, 1, key); } private removeKey(key: string): boolean { let index = this.keys.indexOf(key); if (index !== -1) { this.keys.splice(index, 1); return true; } return false; } } export class DefaultConfigurationModel extends ConfigurationModel { constructor() { const contents = getDefaultValues(); const keys = getConfigurationKeys(); const overrides: IOverrides[] = []; for (const key of Object.keys(contents)) { if (OVERRIDE_PROPERTY_PATTERN.test(key)) { overrides.push({ identifiers: [overrideIdentifierFromKey(key).trim()], keys: Object.keys(contents[key]), contents: toValuesTree(contents[key], (message) => console.error(`Conflict in default settings file: ${message}`) ), }); } } super(contents, keys, overrides); } } export class Configuration { private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null; private _foldersConsolidatedConfigurations: ResourceMap = new ResourceMap(); constructor( private _defaultConfiguration: ConfigurationModel, private _localUserConfiguration: ConfigurationModel, private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), private _folderConfigurations: ResourceMap = new ResourceMap(), private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), private _memoryConfigurationByResource: ResourceMap = new ResourceMap(), private _freeze: boolean = true ) {} getValue( section: string | undefined, overrides: IConfigurationOverrides, workspace: Workspace | undefined ): any { const consolidateConfigurationModel = this.getConsolidateConfigurationModel( overrides, workspace ); return consolidateConfigurationModel.getValue(section); } updateValue( key: string, value: any, overrides: IConfigurationOverrides = {} ): void { let memoryConfiguration: ConfigurationModel | undefined; if (overrides.resource) { memoryConfiguration = this._memoryConfigurationByResource.get( overrides.resource ); if (!memoryConfiguration) { memoryConfiguration = new ConfigurationModel(); this._memoryConfigurationByResource.set( overrides.resource, memoryConfiguration ); } } else { memoryConfiguration = this._memoryConfiguration; } if (value === undefined) { memoryConfiguration.removeValue(key); } else { memoryConfiguration.setValue(key, value); } if (!overrides.resource) { this._workspaceConsolidatedConfiguration = null; } } private _userConfiguration: ConfigurationModel | null = null; get userConfiguration(): ConfigurationModel { if (!this._userConfiguration) { this._userConfiguration = this._remoteUserConfiguration.isEmpty() ? this._localUserConfiguration : this._localUserConfiguration.merge(this._remoteUserConfiguration); if (this._freeze) { this._userConfiguration.freeze(); } } return this._userConfiguration; } private getConsolidateConfigurationModel( overrides: IConfigurationOverrides, workspace: Workspace | undefined ): ConfigurationModel { let configurationModel = this.getConsolidatedConfigurationModelForResource( overrides, workspace ); return overrides.overrideIdentifier ? configurationModel.override(overrides.overrideIdentifier) : configurationModel; } private getConsolidatedConfigurationModelForResource( { resource }: IConfigurationOverrides, workspace: Workspace | undefined ): ConfigurationModel { let consolidateConfiguration = this.getWorkspaceConsolidatedConfiguration(); if (workspace && resource) { const root = workspace.getFolder(resource); if (root) { consolidateConfiguration = this.getFolderConsolidatedConfiguration(root.uri) || consolidateConfiguration; } const memoryConfigurationForResource = this._memoryConfigurationByResource.get(resource); if (memoryConfigurationForResource) { consolidateConfiguration = consolidateConfiguration.merge( memoryConfigurationForResource ); } } return consolidateConfiguration; } private getWorkspaceConsolidatedConfiguration(): ConfigurationModel { if (!this._workspaceConsolidatedConfiguration) { this._workspaceConsolidatedConfiguration = this._defaultConfiguration.merge( this.userConfiguration, this._workspaceConfiguration, this._memoryConfiguration ); if (this._freeze) { this._workspaceConfiguration = this._workspaceConfiguration.freeze(); } } return this._workspaceConsolidatedConfiguration; } private getFolderConsolidatedConfiguration(folder: URI): ConfigurationModel { let folderConsolidatedConfiguration = this._foldersConsolidatedConfigurations.get(folder); if (!folderConsolidatedConfiguration) { const workspaceConsolidateConfiguration = this.getWorkspaceConsolidatedConfiguration(); const folderConfiguration = this._folderConfigurations.get(folder); if (folderConfiguration) { folderConsolidatedConfiguration = workspaceConsolidateConfiguration.merge(folderConfiguration); if (this._freeze) { folderConsolidatedConfiguration = folderConsolidatedConfiguration.freeze(); } this._foldersConsolidatedConfigurations.set( folder, folderConsolidatedConfiguration ); } else { folderConsolidatedConfiguration = workspaceConsolidateConfiguration; } } return folderConsolidatedConfiguration; } toData(): IConfigurationData { return { defaults: { contents: this._defaultConfiguration.contents, overrides: this._defaultConfiguration.overrides, keys: this._defaultConfiguration.keys, }, user: { contents: this.userConfiguration.contents, overrides: this.userConfiguration.overrides, keys: this.userConfiguration.keys, }, workspace: { contents: this._workspaceConfiguration.contents, overrides: this._workspaceConfiguration.overrides, keys: this._workspaceConfiguration.keys, }, folders: [...this._folderConfigurations.keys()].reduce< [UriComponents, IConfigurationModel][] >((result, folder) => { const { contents, overrides, keys } = this._folderConfigurations.get(folder)!; result.push([folder, { contents, overrides, keys }]); return result; }, []), }; } static parse(data: IConfigurationData): Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults); const userConfiguration = this.parseConfigurationModel(data.user); const workspaceConfiguration = this.parseConfigurationModel(data.workspace); const folders: ResourceMap = data.folders.reduce( (result, value) => { result.set( URI.revive(value[0]), this.parseConfigurationModel(value[1]) ); return result; }, new ResourceMap() ); return new Configuration( defaultConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, folders, new ConfigurationModel(), new ResourceMap(), false ); } private static parseConfigurationModel( model: IConfigurationModel ): ConfigurationModel { return new ConfigurationModel( model.contents, model.keys, model.overrides ).freeze(); } } export class ConfigurationChangeEvent implements IConfigurationChangeEvent { private readonly affectedKeysTree: any; readonly affectedKeys: string[]; source!: ConfigurationTarget; sourceConfig: any; constructor( readonly change: IConfigurationChange, private readonly previous: | { workspace?: Workspace; data: IConfigurationData } | undefined, private readonly currentConfiguraiton: Configuration, private readonly currentWorkspace?: Workspace ) { const keysSet = new Set(); change.keys.forEach((key) => keysSet.add(key)); change.overrides.forEach(([, keys]) => keys.forEach((key) => keysSet.add(key)) ); this.affectedKeys = [...keysSet.values()]; const configurationModel = new ConfigurationModel(); this.affectedKeys.forEach((key) => configurationModel.setValue(key, {})); this.affectedKeysTree = configurationModel.contents; } private _previousConfiguration: Configuration | undefined = undefined; get previousConfiguration(): Configuration | undefined { if (!this._previousConfiguration && this.previous) { this._previousConfiguration = Configuration.parse(this.previous.data); } return this._previousConfiguration; } affectsConfiguration( section: string, overrides?: IConfigurationOverrides ): boolean { if (this.doesAffectedKeysTreeContains(this.affectedKeysTree, section)) { if (overrides) { const value1 = this.previousConfiguration ? this.previousConfiguration.getValue( section, overrides, this.previous?.workspace ) : undefined; const value2 = this.currentConfiguraiton.getValue( section, overrides, this.currentWorkspace ); return !objects.equals(value1, value2); } return true; } return false; } private doesAffectedKeysTreeContains( affectedKeysTree: any, section: string ): boolean { let requestedTree = toValuesTree({ [section]: true }, () => {}); let key; while ( typeof requestedTree === 'object' && (key = Object.keys(requestedTree)[0]) ) { // Only one key should present, since we added only one property affectedKeysTree = affectedKeysTree[key]; if (!affectedKeysTree) { return false; // Requested tree is not found } requestedTree = requestedTree[key]; } return true; } }