UNPKG

@difizen/mana-core

Version:

618 lines (555 loc) 20.7 kB
/* eslint-disable @typescript-eslint/no-use-before-define */ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { Event, IStringDictionary } from '@difizen/mana-common'; import { Emitter, types } from '@difizen/mana-common'; import * as nls from '../../../nls'; import type { IJSONContributionRegistry } from '../jsonContributionRegistry'; import { Extensions as JSONExtensions } from '../jsonContributionRegistry'; import type { IJSONSchema } from '../jsonSchema'; import { Registry } from '../platform'; export const Extensions = { Configuration: 'base.contributions.configuration', }; export interface IConfigurationRegistry { /** * Register a configuration to the registry. */ registerConfiguration: (configuration: IConfigurationNode) => void; /** * Register multiple configurations to the registry. */ registerConfigurations: ( configurations: IConfigurationNode[], validate?: boolean, ) => void; /** * Deregister multiple configurations from the registry. */ deregisterConfigurations: (configurations: IConfigurationNode[]) => void; /** * Register multiple default configurations to the registry. */ registerDefaultConfigurations: ( defaultConfigurations: IStringDictionary<any>[], ) => void; /** * Deregister multiple default configurations from the registry. */ deregisterDefaultConfigurations: ( defaultConfigurations: IStringDictionary<any>[], ) => void; /** * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values. * Property or default value changes are not allowed. */ notifyConfigurationSchemaUpdated: (...configurations: IConfigurationNode[]) => void; /** * Event that fires whenver a configuration has been * registered. */ onDidSchemaChange: Event<void>; /** * Event that fires whenver a configuration has been * registered. */ onDidUpdateConfiguration: Event<string[]>; /** * Returns all configuration nodes contributed to this registry. */ getConfigurations: () => IConfigurationNode[]; /** * Returns all configurations settings of all configuration nodes contributed to this registry. */ getConfigurationProperties: () => Record<string, IConfigurationPropertySchema>; /** * Returns all excluded configurations settings of all configuration nodes contributed to this registry. */ getExcludedConfigurationProperties: () => Record< string, IConfigurationPropertySchema >; /** * Register the identifiers for editor configurations */ registerOverrideIdentifiers: (identifiers: string[]) => void; } export enum ConfigurationScope { /** * Application specific configuration, which can be configured only in local user settings. */ APPLICATION = 1, /** * Machine specific configuration, which can be configured only in local and remote user settings. */ MACHINE, /** * Window specific configuration, which can be configured in the user or workspace settings. */ WINDOW, /** * Resource specific configuration, which can be configured in the user, workspace or folder settings. */ RESOURCE, /** * Resource specific configuration that can be configured in language specific settings */ LANGUAGE_OVERRIDABLE, /** * Machine specific configuration that can also be configured in workspace or folder settings. */ MACHINE_OVERRIDABLE, } export interface IConfigurationPropertySchema extends IJSONSchema { scope?: ConfigurationScope | undefined; included?: boolean; tags?: string[]; /** * When enabled this setting is ignored during sync and user can override this. */ ignoreSync?: boolean; /** * When enabled this setting is ignored during sync and user cannot override this. */ disallowSyncIgnore?: boolean; enumItemLabels?: string[]; } export interface IConfigurationExtensionInfo { id: string; } export interface IConfigurationNode { id?: string; order?: number; type?: string | string[]; title?: string; description?: string; properties?: Record<string, IConfigurationPropertySchema>; allOf?: IConfigurationNode[]; scope?: ConfigurationScope; extensionInfo?: IConfigurationExtensionInfo; } type SettingProperties = Record<string, any>; export const allSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const applicationSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const machineSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const machineOverridableSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const windowSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const resourceSettings: { properties: SettingProperties; patternProperties: SettingProperties; } = { properties: {}, patternProperties: {} }; export const resourceLanguageSettingsSchemaId = 'vscode://schemas/settings/resourceLanguage'; const contributionRegistry = Registry.as<IJSONContributionRegistry>( JSONExtensions.JSONContribution, ); class ConfigurationRegistry implements IConfigurationRegistry { private readonly defaultValues: IStringDictionary<any>; private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: Record<string, IJSONSchema>; private readonly excludedConfigurationProperties: Record<string, IJSONSchema>; private readonly resourceLanguageSettingsSchema: IJSONSchema; private readonly overrideIdentifiers = new Set<string>(); private readonly _onDidSchemaChange = new Emitter<void>(); readonly onDidSchemaChange: Event<void> = this._onDidSchemaChange.event; private readonly _onDidUpdateConfiguration: Emitter<string[]> = new Emitter< string[] >(); readonly onDidUpdateConfiguration: Event<string[]> = this._onDidUpdateConfiguration.event; constructor() { this.defaultValues = {}; this.defaultLanguageConfigurationOverridesNode = { id: 'defaultOverrides', title: nls.localize( 'defaultLanguageConfigurationOverrides.title', 'Default Language Configuration Overrides', ), properties: {}, }; this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode]; this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true, }; this.configurationProperties = {}; this.excludedConfigurationProperties = {}; contributionRegistry.registerSchema( resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema, ); } public registerConfiguration( configuration: IConfigurationNode, validate = true, ): void { this.registerConfigurations([configuration], validate); } public registerConfigurations( configurations: IConfigurationNode[], validate = true, ): void { const properties: string[] = []; configurations.forEach((configuration) => { properties.push(...this.validateAndRegisterProperties(configuration, validate)); // fills in defaults this.configurationContributors.push(configuration); this.registerJSONConfiguration(configuration); }); contributionRegistry.registerSchema( resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema, ); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } public deregisterConfigurations(configurations: IConfigurationNode[]): void { const properties: string[] = []; const deregisterConfiguration = (configuration: IConfigurationNode) => { if (configuration.properties) { for (const key in configuration.properties) { properties.push(key); delete this.configurationProperties[key]; this.removeFromSchema(key, configuration.properties[key]); } } if (configuration.allOf) { configuration.allOf.forEach((node) => deregisterConfiguration(node)); } }; for (const configuration of configurations) { deregisterConfiguration(configuration); const index = this.configurationContributors.indexOf(configuration); if (index !== -1) { this.configurationContributors.splice(index, 1); } } contributionRegistry.registerSchema( resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema, ); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } public registerDefaultConfigurations( defaultConfigurations: IStringDictionary<any>[], ): void { const properties: string[] = []; const overrideIdentifiers: string[] = []; for (const defaultConfiguration of defaultConfigurations) { for (const key in defaultConfiguration) { properties.push(key); if (OVERRIDE_PROPERTY_PATTERN.test(key)) { this.defaultValues[key] = { ...(this.defaultValues[key] || {}), ...defaultConfiguration[key], }; const property: IConfigurationPropertySchema = { type: 'object', default: this.defaultValues[key], description: nls.localize( 'defaultLanguageConfiguration.description', 'Configure settings to be overridden for {0} language.', key, ), $ref: resourceLanguageSettingsSchemaId, }; overrideIdentifiers.push(overrideIdentifierFromKey(key)); this.configurationProperties[key] = property; this.defaultLanguageConfigurationOverridesNode.properties![key] = property; } else { this.defaultValues[key] = defaultConfiguration[key]; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } } } this.registerOverrideIdentifiers(overrideIdentifiers); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } public deregisterDefaultConfigurations( defaultConfigurations: IStringDictionary<any>[], ): void { const properties: string[] = []; for (const defaultConfiguration of defaultConfigurations) { for (const key in defaultConfiguration) { properties.push(key); delete this.defaultValues[key]; if (OVERRIDE_PROPERTY_PATTERN.test(key)) { delete this.configurationProperties[key]; delete this.defaultLanguageConfigurationOverridesNode.properties![key]; } else { const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); this.updateSchema(key, property); } } } } this.updateOverridePropertyPatternKey(); this._onDidSchemaChange.fire(); this._onDidUpdateConfiguration.fire(properties); } public notifyConfigurationSchemaUpdated(..._configurations: IConfigurationNode[]) { this._onDidSchemaChange.fire(); } public registerOverrideIdentifiers(overrideIdentifiers: string[]): void { for (const overrideIdentifier of overrideIdentifiers) { this.overrideIdentifiers.add(overrideIdentifier); } this.updateOverridePropertyPatternKey(); } private validateAndRegisterProperties( configuration: IConfigurationNode, validate = true, scope: ConfigurationScope = ConfigurationScope.WINDOW, ): string[] { scope = types.isUndefinedOrNull(configuration.scope) ? scope : configuration.scope; const propertyKeys: string[] = []; const { properties } = configuration; if (properties) { for (const key in properties) { if (validate && validateProperty(key)) { delete properties[key]; continue; } const property = properties[key]; // update default value this.updatePropertyDefaultValue(key, property); // update scope if (OVERRIDE_PROPERTY_PATTERN.test(key)) { property.scope = undefined; // No scope for overridable properties `[${identifier}]` } else { property.scope = types.isUndefinedOrNull(property.scope) ? scope : property.scope; } // Add to properties maps // Property is included by default if 'included' is unspecified if ( Object.prototype.hasOwnProperty.call(properties[key], 'included') && !properties[key].included ) { this.excludedConfigurationProperties[key] = properties[key]; delete properties[key]; continue; } else { this.configurationProperties[key] = properties[key]; } if ( !properties[key].deprecationMessage && properties[key].markdownDeprecationMessage ) { // If not set, default deprecationMessage to the markdown source properties[key].deprecationMessage = properties[key].markdownDeprecationMessage; } propertyKeys.push(key); } } const subNodes = configuration.allOf; if (subNodes) { for (const node of subNodes) { propertyKeys.push(...this.validateAndRegisterProperties(node, validate, scope)); } } return propertyKeys; } getConfigurations(): IConfigurationNode[] { return this.configurationContributors; } getConfigurationProperties(): Record<string, IConfigurationPropertySchema> { return this.configurationProperties; } getExcludedConfigurationProperties(): Record<string, IConfigurationPropertySchema> { return this.excludedConfigurationProperties; } private registerJSONConfiguration(configuration: IConfigurationNode) { const register = (configuration: IConfigurationNode) => { const { properties } = configuration; if (properties) { for (const key in properties) { this.updateSchema(key, properties[key]); } } const subNodes = configuration.allOf; if (subNodes) { subNodes.forEach(register); } }; register(configuration); } private updateSchema(key: string, property: IConfigurationPropertySchema): void { allSettings.properties[key] = property; switch (property.scope) { case ConfigurationScope.APPLICATION: applicationSettings.properties[key] = property; break; case ConfigurationScope.MACHINE: machineSettings.properties[key] = property; break; case ConfigurationScope.MACHINE_OVERRIDABLE: machineOverridableSettings.properties[key] = property; break; case ConfigurationScope.WINDOW: windowSettings.properties[key] = property; break; case ConfigurationScope.RESOURCE: resourceSettings.properties[key] = property; break; case ConfigurationScope.LANGUAGE_OVERRIDABLE: resourceSettings.properties[key] = property; this.resourceLanguageSettingsSchema.properties![key] = property; break; } } private removeFromSchema(key: string, property: IConfigurationPropertySchema): void { delete allSettings.properties[key]; switch (property.scope) { case ConfigurationScope.APPLICATION: delete applicationSettings.properties[key]; break; case ConfigurationScope.MACHINE: delete machineSettings.properties[key]; break; case ConfigurationScope.MACHINE_OVERRIDABLE: delete machineOverridableSettings.properties[key]; break; case ConfigurationScope.WINDOW: delete windowSettings.properties[key]; break; case ConfigurationScope.RESOURCE: case ConfigurationScope.LANGUAGE_OVERRIDABLE: delete resourceSettings.properties[key]; break; } } private updateOverridePropertyPatternKey(): void { for (const overrideIdentifier of this.overrideIdentifiers.values()) { const overrideIdentifierProperty = `[${overrideIdentifier}]`; const resourceLanguagePropertiesSchema: IJSONSchema = { type: 'object', description: nls.localize( 'overrideSettings.defaultDescription', 'Configure editor settings to be overridden for a language.', ), errorMessage: nls.localize( 'overrideSettings.errorMessage', 'This setting does not support per-language configuration.', ), $ref: resourceLanguageSettingsSchemaId, }; this.updatePropertyDefaultValue( overrideIdentifierProperty, resourceLanguagePropertiesSchema, ); allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; machineOverridableSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; windowSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; resourceSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; } this._onDidSchemaChange.fire(); } private updatePropertyDefaultValue( key: string, property: IConfigurationPropertySchema, ): void { let defaultValue = this.defaultValues[key]; if (types.isUndefined(defaultValue)) { defaultValue = property.default; } if (types.isUndefined(defaultValue)) { defaultValue = getDefaultValue(property.type); } property.default = defaultValue; } } const OVERRIDE_PROPERTY = '\\[.*\\]$'; export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); export function overrideIdentifierFromKey(key: string): string { return key.substring(1, key.length - 1); } export function getDefaultValue(type: string | string[] | undefined): any { const t = Array.isArray(type) ? (<string[]>type)[0] : <string>type; switch (t) { case 'boolean': return false; case 'integer': case 'number': return 0; case 'string': return ''; case 'array': return []; case 'object': return {}; default: return null; } } const configurationRegistry = new ConfigurationRegistry(); Registry.add(Extensions.Configuration, configurationRegistry); export function validateProperty(property: string): string | null { if (!property.trim()) { return nls.localize('config.property.empty', 'Cannot register an empty property'); } if (OVERRIDE_PROPERTY_PATTERN.test(property)) { return nls.localize( 'config.property.languageDefault', "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", property, ); } if (configurationRegistry.getConfigurationProperties()[property] !== undefined) { return nls.localize( 'config.property.duplicate', "Cannot register '{0}'. This property is already registered.", property, ); } return null; } export function getScopes(): [string, ConfigurationScope | undefined][] { const scopes: [string, ConfigurationScope | undefined][] = []; const configurationProperties = configurationRegistry.getConfigurationProperties(); for (const key of Object.keys(configurationProperties)) { scopes.push([key, configurationProperties[key].scope]); } scopes.push(['launch', ConfigurationScope.RESOURCE]); scopes.push(['task', ConfigurationScope.RESOURCE]); return scopes; }