@difizen/mana-core
Version:
618 lines (555 loc) • 20.7 kB
text/typescript
/* 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;
}