UNPKG

@difizen/mana-core

Version:

963 lines (859 loc) 29.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from '@difizen/mana-common'; import { types } from '@difizen/mana-common'; import { objects } from '@difizen/mana-common'; import type { URI } from '@difizen/mana-common'; import type { Event } from '@difizen/mana-common'; import { getOrSet, ResourceMap, VSDisposable } from '../../../utils'; import { Registry } from '../platform'; import { addToValueTree, toValuesTree, getConfigurationValue, getDefaultValues, getConfigurationKeys, removeFromValueTree, toOverrides, compare, } from './configuration'; import type { IOverrides, IConfigurationModel, IConfigurationOverrides, IConfigurationData, IConfigurationValue, ConfigurationTarget, IConfigurationChangeEvent, IConfigurationChange, } from './configuration'; import { OVERRIDE_PROPERTY_PATTERN, ConfigurationScope, Extensions, overrideIdentifierFromKey, } from './configurationRegistry'; import type { IConfigurationRegistry, IConfigurationPropertySchema, } from './configurationRegistry'; import * as json from './json'; //TODO interface Workspace { getFolder: (_resource: URI) => { uri: URI } | null; } export function distinct<T>(array: readonly T[], keyFn?: (_t: T) => string): T[] { if (!keyFn) { return array.filter((element, position) => { return array.indexOf(element) === position; }); } const seen: Record<string, boolean> = Object.create(null); return array.filter((elem) => { const key = keyFn(elem); if (seen[key]) { return false; } seen[key] = true; return true; }); } export function equals<T>( one: readonly T[] | undefined, other: readonly T[] | undefined, itemEquals: (_a: T, _b: T) => boolean = (a, b) => a === b, ): boolean { if (one === other) { return true; } if (!one || !other) { return false; } if (one.length !== other.length) { return false; } for (let i = 0, len = one.length; i < len; i++) { if (!itemEquals(one[i], other[i])) { return false; } } return true; } export class ConfigurationModel implements IConfigurationModel { private isFrozen = false; private _contents: any = {}; private _keys: string[] = []; private _overrides: IOverrides[] = []; constructor( _contents: any = {}, _keys: string[] = [], _overrides: IOverrides[] = [], ) { this._contents = _contents; this._keys = _keys; this._overrides = _overrides; } 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<V>(section: string | undefined): V { return section ? getConfigurationValue<any>(this.contents, section) : this.contents; } getOverrideValue<V>( section: string | undefined, overrideIdentifier: string, ): V | undefined { const overrideContents = this.getContentsForOverrideIdentifer(overrideIdentifier); return overrideContents ? section ? getConfigurationValue<any>(overrideContents, section) : overrideContents : undefined; } getKeysForOverrideIdentifier(identifier: string): string[] { for (const override of this.overrides) { if (override.identifiers.indexOf(identifier) !== -1) { return override.keys; } } return []; } 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; } const contents: any = {}; for (const key of distinct([ ...Object.keys(this.contents), ...Object.keys(overrideContents), ])) { let contentsForKey = this.contents[key]; const 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) => 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<T>(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 { const 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 ConfigurationModelParser { private _raw: any = null; private _configurationModel: ConfigurationModel | null = null; private _parseErrors: any[] = []; protected readonly _name: string; private _scopes?: ConfigurationScope[] | undefined; constructor(_name: string, _scopes?: ConfigurationScope[]) { this._name = _name; this._scopes = _scopes; } get configurationModel(): ConfigurationModel { return this._configurationModel || new ConfigurationModel(); } get errors(): any[] { return this._parseErrors; } public parseContent(content: string | null | undefined): void { if (!types.isUndefinedOrNull(content)) { const raw = this.doParseContent(content); this.parseRaw(raw); } } public parseRaw(raw: any): void { this._raw = raw; const configurationModel = this.doParseRaw(raw); this._configurationModel = new ConfigurationModel( configurationModel.contents, configurationModel.keys, configurationModel.overrides, ); } public parse(): void { if (this._raw) { this.parseRaw(this._raw); } } protected doParseContent(content: string): any { let raw: any = {}; let currentProperty: string | null = null; let currentParent: any = []; const previousParents: any[] = []; const parseErrors: json.ParseError[] = []; function onValue(value: any) { if (Array.isArray(currentParent)) { (<any[]>currentParent).push(value); } else if (currentProperty) { currentParent[currentProperty] = value; } } const visitor: json.JSONVisitor = { onObjectBegin: () => { const object = {}; onValue(object); previousParents.push(currentParent); currentParent = object; currentProperty = null; }, onObjectProperty: (name: string) => { currentProperty = name; }, onObjectEnd: () => { currentParent = previousParents.pop(); }, onArrayBegin: () => { const array: any[] = []; onValue(array); previousParents.push(currentParent); currentParent = array; currentProperty = null; }, onArrayEnd: () => { currentParent = previousParents.pop(); }, onLiteralValue: onValue, onError: (error: json.ParseErrorCode, offset: number, length: number) => { parseErrors.push({ error, offset, length }); }, }; if (content) { try { json.visit(content, visitor); raw = currentParent[0] || {}; } catch (e) { console.error(`Error while parsing settings file ${this._name}: ${e}`); this._parseErrors = [e]; } } return raw; } protected doParseRaw(raw: any): IConfigurationModel { if (this._scopes) { const configurationProperties = Registry.as<IConfigurationRegistry>( Extensions.Configuration, ).getConfigurationProperties(); raw = this.filterByScope(raw, configurationProperties, true, this._scopes); } const contents = toValuesTree(raw, (message) => console.error(`Conflict in settings file ${this._name}: ${message}`), ); const keys = Object.keys(raw); const overrides: IOverrides[] = toOverrides(raw, (message) => console.error(`Conflict in settings file ${this._name}: ${message}`), ); return { contents, keys, overrides }; } private filterByScope( properties: any, configurationProperties: Record<string, IConfigurationPropertySchema>, filterOverriddenProperties: boolean, scopes: ConfigurationScope[], ): any { const result: any = {}; for (const key in properties) { if (OVERRIDE_PROPERTY_PATTERN.test(key) && filterOverriddenProperties) { result[key] = this.filterByScope( properties[key], configurationProperties, false, scopes, ); } else { const scope = this.getScope(key, configurationProperties); // Load unregistered configurations always. if (scope === undefined || scopes.indexOf(scope) !== -1) { result[key] = properties[key]; } } } return result; } private getScope( key: string, configurationProperties: Record<string, IConfigurationPropertySchema>, ): ConfigurationScope | undefined { const propertySchema = configurationProperties[key]; return propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined; } } export class UserSettings extends VSDisposable { private readonly parser: ConfigurationModelParser; protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>()); readonly onDidChange: Event<void> = this._onDidChange.event; private readonly scopes: ConfigurationScope[] | undefined; constructor(scopes: ConfigurationScope[] | undefined) { super(); this.scopes = scopes; this.parser = new ConfigurationModelParser('UserSettings', this.scopes); } async loadConfiguration(): Promise<ConfigurationModel> { try { this.parser.parseContent('{}'); return this.parser.configurationModel; } catch (e) { return new ConfigurationModel(); } } reprocess(): ConfigurationModel { this.parser.parse(); return this.parser.configurationModel; } } export class Configuration { private _workspaceConsolidatedConfiguration: ConfigurationModel | null = null; private _foldersConsolidatedConfigurations: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(); private _defaultConfiguration: ConfigurationModel; private _localUserConfiguration: ConfigurationModel; private _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(); private _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(); private _memoryConfiguration: ConfigurationModel = new ConfigurationModel(); private _memoryConfigurationByResource: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(); private _freeze = true; constructor( _defaultConfiguration: ConfigurationModel, _localUserConfiguration: ConfigurationModel, _remoteUserConfiguration: ConfigurationModel = new ConfigurationModel(), _workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), _memoryConfiguration: ConfigurationModel = new ConfigurationModel(), _memoryConfigurationByResource: ResourceMap<ConfigurationModel> = new ResourceMap<ConfigurationModel>(), _freeze = true, ) { this._memoryConfigurationByResource; this._defaultConfiguration = _defaultConfiguration; this._localUserConfiguration = _localUserConfiguration; this._remoteUserConfiguration = _remoteUserConfiguration; this._workspaceConfiguration = _workspaceConfiguration; this._memoryConfiguration = _memoryConfiguration; this._memoryConfigurationByResource = _memoryConfigurationByResource; this._freeze = _freeze; } 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 { // } inspect<C>( key: string, overrides: IConfigurationOverrides, _workspace: Workspace | undefined, ): IConfigurationValue<C> { const defaultValue = overrides.overrideIdentifier ? this._defaultConfiguration .freeze() .override(overrides.overrideIdentifier) .getValue<C>(key) : this._defaultConfiguration.freeze().getValue<C>(key); return { defaultValue, default: defaultValue !== undefined ? { value: this._defaultConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._defaultConfiguration .freeze() .getOverrideValue(key, overrides.overrideIdentifier) : undefined, } : undefined, }; } keys(_workspace: Workspace | undefined): { default: string[]; user: string[]; workspace: string[]; workspaceFolder: string[]; } { return { default: this._defaultConfiguration.freeze().keys, user: this._defaultConfiguration.freeze().keys, workspace: this._defaultConfiguration.freeze().keys, workspaceFolder: this._defaultConfiguration.freeze().keys, }; } updateDefaultConfiguration(defaultConfiguration: ConfigurationModel): void { this._defaultConfiguration = defaultConfiguration; this._workspaceConsolidatedConfiguration = null; this._foldersConsolidatedConfigurations.clear(); } updateLocalUserConfiguration(localUserConfiguration: ConfigurationModel): void { this._localUserConfiguration = localUserConfiguration; this._userConfiguration = null; this._workspaceConsolidatedConfiguration = null; this._foldersConsolidatedConfigurations.clear(); } updateRemoteUserConfiguration(remoteUserConfiguration: ConfigurationModel): void { this._remoteUserConfiguration = remoteUserConfiguration; this._userConfiguration = null; this._workspaceConsolidatedConfiguration = null; this._foldersConsolidatedConfigurations.clear(); } updateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel): void { this._workspaceConfiguration = workspaceConfiguration; this._workspaceConsolidatedConfiguration = null; this._foldersConsolidatedConfigurations.clear(); } compareAndUpdateDefaultConfiguration( defaults: ConfigurationModel, keys: string[], ): IConfigurationChange { const overrides: [string, string[]][] = keys .filter((key) => OVERRIDE_PROPERTY_PATTERN.test(key)) .map((key) => { const overrideIdentifier = overrideIdentifierFromKey(key); const fromKeys = this._defaultConfiguration.getKeysForOverrideIdentifier(overrideIdentifier); const toKeys = defaults.getKeysForOverrideIdentifier(overrideIdentifier); const keys = [ ...toKeys.filter((key) => fromKeys.indexOf(key) === -1), ...fromKeys.filter((key) => toKeys.indexOf(key) === -1), ...fromKeys.filter( (key) => !objects.objectEquals( this._defaultConfiguration.override(overrideIdentifier).getValue(key), defaults.override(overrideIdentifier).getValue(key), ), ), ]; return [overrideIdentifier, keys]; }); this.updateDefaultConfiguration(defaults); return { keys, overrides }; } compareAndUpdateLocalUserConfiguration( user: ConfigurationModel, ): IConfigurationChange { const { added, updated, removed, overrides } = compare( this.localUserConfiguration, user, ); const keys = [...added, ...updated, ...removed]; if (keys.length) { this.updateLocalUserConfiguration(user); } return { keys, overrides }; } compareAndUpdateRemoteUserConfiguration( user: ConfigurationModel, ): IConfigurationChange { const { added, updated, removed, overrides } = compare( this.remoteUserConfiguration, user, ); const keys = [...added, ...updated, ...removed]; if (keys.length) { this.updateRemoteUserConfiguration(user); } return { keys, overrides }; } compareAndUpdateWorkspaceConfiguration( workspaceConfiguration: ConfigurationModel, ): IConfigurationChange { const { added, updated, removed, overrides } = compare( this.workspaceConfiguration, workspaceConfiguration, ); const keys = [...added, ...updated, ...removed]; if (keys.length) { this.updateWorkspaceConfiguration(workspaceConfiguration); } return { keys, overrides }; } compareAndUpdateFolderConfiguration( _resource: URI, _folderConfiguration: ConfigurationModel, ): IConfigurationChange { return { keys: [], overrides: [] }; } compareAndDeleteFolderConfiguration(_folder: URI): IConfigurationChange { return { keys: [], overrides: [] }; } get defaults(): ConfigurationModel { return this._defaultConfiguration; } 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; } get localUserConfiguration(): ConfigurationModel { return this._localUserConfiguration; } get remoteUserConfiguration(): ConfigurationModel { return this._remoteUserConfiguration; } get workspaceConfiguration(): ConfigurationModel { return this._workspaceConfiguration; } private getConsolidateConfigurationModel( overrides: IConfigurationOverrides, workspace: Workspace | undefined, ): ConfigurationModel { const configurationModel = this.getConsolidatedConfigurationModelForResource( overrides, workspace, ); return overrides.overrideIdentifier ? configurationModel.override(overrides.overrideIdentifier) : configurationModel; } private getConsolidatedConfigurationModelForResource( { resource: _resource }: IConfigurationOverrides, _workspace: Workspace | undefined, ): ConfigurationModel { const consolidateConfiguration = this.getWorkspaceConsolidatedConfiguration(); 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; } 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, }, }; } allKeys(): string[] { const keys: Set<string> = new Set<string>(); this._defaultConfiguration.freeze().keys.forEach((key) => keys.add(key)); this.userConfiguration.freeze().keys.forEach((key) => keys.add(key)); this._workspaceConfiguration.freeze().keys.forEach((key) => keys.add(key)); return [...keys.values()]; } protected getAllKeysForOverrideIdentifier(overrideIdentifier: string): string[] { const keys: Set<string> = new Set<string>(); this._defaultConfiguration .getKeysForOverrideIdentifier(overrideIdentifier) .forEach((key) => keys.add(key)); this.userConfiguration .getKeysForOverrideIdentifier(overrideIdentifier) .forEach((key) => keys.add(key)); this._workspaceConfiguration .getKeysForOverrideIdentifier(overrideIdentifier) .forEach((key) => keys.add(key)); return [...keys.values()]; } static parse(data: IConfigurationData): Configuration { const defaultConfiguration = this.parseConfigurationModel(data.defaults); const userConfiguration = this.parseConfigurationModel(data.user); const workspaceConfiguration = this.parseConfigurationModel(data.workspace); return new Configuration( defaultConfiguration, userConfiguration, new ConfigurationModel(), workspaceConfiguration, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), false, ); } private static parseConfigurationModel( model: IConfigurationModel, ): ConfigurationModel { return new ConfigurationModel(model.contents, model.keys, model.overrides).freeze(); } } export function mergeChanges(...changes: IConfigurationChange[]): IConfigurationChange { if (changes.length === 0) { return { keys: [], overrides: [] }; } if (changes.length === 1) { return changes[0]; } const keysSet = new Set<string>(); const overridesMap = new Map<string, Set<string>>(); for (const change of changes) { change.keys.forEach((key) => keysSet.add(key)); change.overrides.forEach(([identifier, keys]) => { const result = getOrSet(overridesMap, identifier, new Set<string>()); keys.forEach((key) => result.add(key)); }); } const overrides: [string, string[]][] = []; overridesMap.forEach((keys, identifier) => overrides.push([identifier, [...keys.values()]]), ); return { keys: [...keysSet.values()], overrides }; } export class ConfigurationChangeEvent implements IConfigurationChangeEvent { private readonly affectedKeysTree: any; readonly affectedKeys: string[]; source!: ConfigurationTarget; sourceConfig: any; readonly change: IConfigurationChange; private readonly previous: | { workspace?: Workspace; data: IConfigurationData } | undefined; private readonly currentConfiguraiton: Configuration; private readonly currentWorkspace?: Workspace | undefined; constructor( change: IConfigurationChange, previous: { workspace?: Workspace; data: IConfigurationData } | undefined, currentConfiguraiton: Configuration, currentWorkspace?: Workspace, ) { this.change = change; this.previous = previous; this.currentConfiguraiton = currentConfiguraiton; this.currentWorkspace = currentWorkspace; const keysSet = new Set<string>(); 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.objectEquals(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; } } export class AllKeysConfigurationChangeEvent extends ConfigurationChangeEvent { public override source: ConfigurationTarget; public override sourceConfig: any; constructor( configuration: Configuration, workspace: Workspace, source: ConfigurationTarget, sourceConfig: any, ) { super( { keys: configuration.allKeys(), overrides: [] }, undefined, configuration, workspace, ); this.source = source; this.sourceConfig = sourceConfig; } }