From a3f9be07cc4654aac2fe13c6ea0588e7848e9575 Mon Sep 17 00:00:00 2001 From: Nigel Westbury Date: Wed, 14 Oct 2020 11:03:57 +0100 Subject: [PATCH] Allow extenders to modify preferences Signed-off-by: Nigel Westbury --- CHANGELOG.md | 4 + .../preferences/preference-contribution.ts | 95 +++++++++++++++++-- .../browser/preferences/preference-service.ts | 23 +++-- .../common/preferences/preference-schema.ts | 17 ++++ .../abstract-resource-preference-provider.ts | 2 +- 5 files changed, 125 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d68cdb8695e..8265d2c93c221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.11.0 + +- [preferences] preferences schemas can now be modified or completely removed from the UI by downstream packages [#8626](https://github.com/eclipse-theia/theia/pull/8626) + ## v1.10.0 - 1/28/2021 - [api-samples] added example on how to contribute toggleable toolbar items [#8968](https://github.com/eclipse-theia/theia/pull/8968) diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 566247a0a320c..33142ae42dcd5 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -20,12 +20,16 @@ import { ContributionProvider, bindContributionProvider, escapeRegExpCharacters, import { PreferenceScope } from './preference-scope'; import { PreferenceProvider, PreferenceProviderDataChange } from './preference-provider'; import { - PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType + PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType, + PreferenceSchemaModification, PreferenceSchemaPropertyModifications, PreferenceSchemaPropertyModification } from '../../common/preferences/preference-schema'; import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { bindPreferenceConfigurations, PreferenceConfigurations } from './preference-configurations'; -export { PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType }; +export { + PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType, + PreferenceSchemaModification +}; import { Mutable } from '../../common/types'; /* eslint-disable guard-for-in, @typescript-eslint/no-explicit-any */ @@ -60,10 +64,16 @@ export interface PreferenceContribution { readonly schema: PreferenceSchema; } +export const PreferenceModificationContribution = Symbol('PreferenceModificationContribution'); +export interface PreferenceModificationContribution { + readonly schemaModification: PreferenceSchemaModification; +} + export function bindPreferenceSchemaProvider(bind: interfaces.Bind): void { bindPreferenceConfigurations(bind); bind(PreferenceSchemaProvider).toSelf().inSingletonScope(); bindContributionProvider(bind, PreferenceContribution); + bindContributionProvider(bind, PreferenceModificationContribution); } export interface OverridePreferenceName { @@ -108,10 +118,14 @@ export class PreferenceSchemaProvider extends PreferenceProvider { protected readonly combinedSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; protected readonly workspaceSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; protected readonly folderSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; + protected readonly modifications: PreferenceSchemaPropertyModifications = {}; @inject(ContributionProvider) @named(PreferenceContribution) protected readonly preferenceContributions: ContributionProvider; + @inject(ContributionProvider) @named(PreferenceModificationContribution) + protected readonly preferenceModificationContributions: ContributionProvider; + @inject(PreferenceConfigurations) protected readonly configurations: PreferenceConfigurations; @@ -123,6 +137,9 @@ export class PreferenceSchemaProvider extends PreferenceProvider { @postConstruct() protected init(): void { + this.preferenceModificationContributions.getContributions().forEach(contrib => { + this.doSetSchemaModification(contrib.schemaModification); + }); this.preferenceContributions.getContributions().forEach(contrib => { this.doSetSchema(contrib.schema); }); @@ -233,7 +250,8 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } this.updateSchemaProps(preferenceName, schemaProps); - const value = schemaProps.defaultValue = this.getDefaultValue(schemaProps, preferenceName); + const modifiedProperties = this.schema(preferenceName, schemaProps); + const value = schemaProps.defaultValue = this.getDefaultValue(modifiedProperties, preferenceName); if (this.testOverrideValue(preferenceName, value)) { for (const overriddenPreferenceName in value) { const overrideValue = value[overriddenPreferenceName]; @@ -289,8 +307,66 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return null; } + protected doSetSchemaModification(schemaModification: PreferenceSchemaModification): void { + for (const preferenceName of Object.keys(schemaModification.properties)) { + const modifiableProperties = this.combinedSchema.properties[preferenceName]; + const previousModifications = this.modifications[preferenceName] ?? {}; + const newModifications: PreferenceSchemaPropertyModification = schemaModification.properties[preferenceName]; + this.modifications[preferenceName] = { ...previousModifications, ...newModifications }; + + // Validate that the modifications only constrain the schema, cannot allow extra values + const modifiedEnum = newModifications.enum; + if (modifiedEnum !== undefined) { + if (modifiableProperties) { + if (modifiableProperties.type !== 'string') { + console.warn(`Override of preference ${preferenceName} cannot constrain to enum values because the property is not string type.`); + continue; + } + if (modifiableProperties.enum && modifiableProperties.enum.some(v => !modifiedEnum.includes(v))) { + console.warn(`Override of preference enum ${preferenceName} cannot add enum values, it can only constrain the set of values.`); + continue; + } + } + } + const modifiedMinimum = newModifications.minimum; + if (modifiedMinimum !== undefined) { + if (modifiableProperties) { + if (modifiableProperties.minimum && modifiedMinimum < modifiableProperties.minimum) { + console.warn(`Override of preference minimum ${preferenceName} cannot reduce the minimum, it can only increase it.`); + continue; + } + } + } + } + } + + protected schema(preferenceName: string, propertySchema: PreferenceDataProperty): PreferenceDataProperty { + const modifications = this.modifications[preferenceName]; + return modifications ? { ...propertySchema, ...modifications } : propertySchema; + } + getCombinedSchema(): PreferenceDataSchema { - return this.combinedSchema; + const properties: { [key: string]: PreferenceDataProperty; } = {}; + for (const preferenceName of Object.keys(this.combinedSchema.properties)) { + const value = this.combinedSchema.properties[preferenceName]; + const modifications = this.modifications[preferenceName]; + if (modifications) { + if (!modifications.hidden) { + const modifiedValue = this.schema(preferenceName, value); + properties[preferenceName] = modifiedValue; + } + } else { + properties[preferenceName] = value; + } + } + return { ...this.combinedSchema, properties }; + } + + getPropertySchema(preferenceName: string): PreferenceDataProperty | undefined { + const property = this.combinedSchema.properties[preferenceName]; + if (property) { + return this.schema(preferenceName, property); + } } getSchema(scope: PreferenceScope): PreferenceDataSchema { @@ -343,10 +419,10 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } if (!property) { // try from overridden value - property = this.combinedSchema.properties[overridden.preferenceName]; + property = this.getPropertySchema(overridden.preferenceName); } } else { - property = this.combinedSchema.properties[preferenceName]; + property = this.getPropertySchema(preferenceName); } return property && property.scope! >= scope; } @@ -361,7 +437,7 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } *getOverridePreferenceNames(preferenceName: string): IterableIterator { - const preference = this.combinedSchema.properties[preferenceName]; + const preference = this.getPropertySchema(preferenceName); if (preference && preference.overridable) { for (const overrideIdentifier of this.overrideIdentifiers) { yield this.overridePreferenceName({ preferenceName, overrideIdentifier }); @@ -416,4 +492,9 @@ export class PreferenceSchemaProvider extends PreferenceProvider { break; } } + + isPropertyHidden(preferenceName: string): boolean { + const modifications = this.modifications[preferenceName]; + return modifications && !!modifications.hidden; + } } diff --git a/packages/core/src/browser/preferences/preference-service.ts b/packages/core/src/browser/preferences/preference-service.ts index edd7f98d5562b..1ed2c302e0ce2 100644 --- a/packages/core/src/browser/preferences/preference-service.ts +++ b/packages/core/src/browser/preferences/preference-service.ts @@ -494,14 +494,21 @@ export class PreferenceServiceImpl implements PreferenceService { } protected doResolve(preferenceName: string, defaultValue?: T, resourceUri?: string): PreferenceResolveResult { const result: PreferenceResolveResult = {}; - for (const scope of PreferenceScope.getScopes()) { - if (this.schema.isValidInScope(preferenceName, scope)) { - const provider = this.getProvider(scope); - if (provider) { - const { configUri, value } = provider.resolve(preferenceName, resourceUri); - if (value !== undefined) { - result.configUri = configUri; - result.value = PreferenceProvider.merge(result.value as any, value as any) as any; + const propertyIsHidden = this.schema.isPropertyHidden(preferenceName); + if (propertyIsHidden) { + const { configUri, value } = this.schema.resolve(preferenceName, resourceUri); + result.configUri = configUri; + result.value = value; + } else { + for (const scope of PreferenceScope.getScopes()) { + if (this.schema.isValidInScope(preferenceName, scope)) { + const provider = this.getProvider(scope); + if (provider) { + const { configUri, value } = provider.resolve(preferenceName, resourceUri); + if (value !== undefined) { + result.configUri = configUri; + result.value = PreferenceProvider.merge(result.value as any, value as any) as any; + } } } } diff --git a/packages/core/src/common/preferences/preference-schema.ts b/packages/core/src/common/preferences/preference-schema.ts index 0737a5a6e3230..ecd91046add30 100644 --- a/packages/core/src/common/preferences/preference-schema.ts +++ b/packages/core/src/common/preferences/preference-schema.ts @@ -76,6 +76,7 @@ export interface PreferenceItem { additionalProperties?: object | boolean; [name: string]: any; overridable?: boolean; + hidden?: boolean; } export interface PreferenceSchemaProperty extends PreferenceItem { @@ -100,4 +101,20 @@ export namespace PreferenceDataProperty { } } +export interface PreferenceSchemaModification { + properties: PreferenceSchemaPropertyModifications +} + +export interface PreferenceSchemaPropertyModifications { + [name: string]: PreferenceSchemaPropertyModification +} + +export interface PreferenceSchemaPropertyModification { + minimum?: number; + default?: any; + enum?: string[]; + description?: string; + hidden?: boolean; +} + export type JsonType = 'string' | 'array' | 'number' | 'integer' | 'object' | 'boolean' | 'null'; diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index 976f124514dcd..ffdac49cf36c4 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -229,7 +229,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi for (const prefName of prefNames.values()) { const oldValue = oldPrefs[prefName]; const newValue = newPrefs[prefName]; - const schemaProperties = this.schemaProvider.getCombinedSchema().properties[prefName]; + const schemaProperties = this.schemaProvider.getPropertySchema(prefName); if (schemaProperties) { const scope = schemaProperties.scope; // do not emit the change event if the change is made out of the defined preference scope