diff --git a/.theia/settings.json b/.theia/settings.json index be725ff0a2b86..198dfbae0092e 100644 --- a/.theia/settings.json +++ b/.theia/settings.json @@ -1,3 +1,14 @@ { - "typescript.tsdk": "node_modules/typescript/lib" -} + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "[typescript]": { + "editor.tabSize": 4 + }, + "[json]": { + "editor.tabSize": 2 + }, + "[jsonc]": { + "editor.tabSize": 2 + }, + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3651a916538fc..1ab601bdfd054 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,6 +44,9 @@ "[json]": { "editor.tabSize": 2 }, + "[jsonc]": { + "editor.tabSize": 2 + }, "typescript.tsdk": "node_modules/typescript/lib", "files.insertFinalNewline": true } \ No newline at end of file diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 093980bc38825..657366103e295 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -16,11 +16,12 @@ import * as Ajv from 'ajv'; import { inject, injectable, interfaces, named, postConstruct } from 'inversify'; -import { ContributionProvider, bindContributionProvider } from '../../common'; +import { ContributionProvider, bindContributionProvider, escapeRegExpCharacters, Emitter, Event } from '../../common'; import { PreferenceScope } from './preference-service'; import { PreferenceProvider, PreferenceProviderPriority, PreferenceProviderDataChange } from './preference-provider'; // tslint:disable:no-any +// tslint:disable:forin export const PreferenceContribution = Symbol('PreferenceContribution'); export interface PreferenceContribution { @@ -30,9 +31,8 @@ export interface PreferenceContribution { export interface PreferenceSchema { [name: string]: any, scope?: 'application' | 'window' | 'resource' | PreferenceScope, - properties: { - [name: string]: PreferenceSchemaProperty - } + overridable?: boolean; + properties: PreferenceSchemaProperties } export namespace PreferenceSchema { export function getDefaultScope(schema: PreferenceSchema): PreferenceScope { @@ -46,12 +46,19 @@ export namespace PreferenceSchema { } } +export interface PreferenceSchemaProperties { + [name: string]: PreferenceSchemaProperty +} + export interface PreferenceDataSchema { [name: string]: any, scope?: PreferenceScope, properties: { [name: string]: PreferenceDataProperty } + patternProperties: { + [name: string]: PreferenceDataProperty + }; } export interface PreferenceItem { @@ -63,6 +70,7 @@ export interface PreferenceItem { properties?: { [name: string]: PreferenceItem }; additionalProperties?: object; [name: string]: any; + overridable?: boolean; } export interface PreferenceSchemaProperty extends PreferenceItem { @@ -92,15 +100,31 @@ export function bindPreferenceSchemaProvider(bind: interfaces.Bind): void { bindContributionProvider(bind, PreferenceContribution); } +export interface OverridePreferenceName { + preferenceName: string + overrideIdentifier: string +} + +const OVERRIDE_PROPERTY = '\\[(.*)\\]$'; +export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); + +const OVERRIDE_PATTERN_WITH_SUBSTITUTION = '\\[(${0})\\]$'; + @injectable() export class PreferenceSchemaProvider extends PreferenceProvider { protected readonly preferences: { [name: string]: any } = {}; - protected readonly combinedSchema: PreferenceDataSchema = { properties: {} }; - protected validateFunction: Ajv.ValidateFunction; + protected readonly combinedSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; @inject(ContributionProvider) @named(PreferenceContribution) protected readonly preferenceContributions: ContributionProvider; + protected validateFunction: Ajv.ValidateFunction; + + protected readonly onDidPreferenceSchemaChangedEmitter = new Emitter(); + readonly onDidPreferenceSchemaChanged: Event = this.onDidPreferenceSchemaChangedEmitter.event; + protected fireDidPreferenceSchemaChanged(): void { + this.onDidPreferenceSchemaChangedEmitter.fire(undefined); + } @postConstruct() protected init(): void { @@ -109,27 +133,79 @@ export class PreferenceSchemaProvider extends PreferenceProvider { }); this.combinedSchema.additionalProperties = false; this.updateValidate(); + this.onDidPreferencesChanged(() => this.updateValidate()); this._ready.resolve(); } - protected doSetSchema(schema: PreferenceSchema): void { + protected readonly overrideIdentifiers = new Set(); + registerOverrideIdentifier(overrideIdentifier: string): void { + if (this.overrideIdentifiers.has(overrideIdentifier)) { + return; + } + this.overrideIdentifiers.add(overrideIdentifier); + this.updateOverridePatternPropertiesKey(); + } + + protected readonly overridePatternProperties: Required> & PreferenceDataProperty = { + type: 'object', + description: 'Configure editor settings to be overridden for a language.', + errorMessage: 'Unknown Identifier. Use language identifiers', + properties: {} + }; + protected overridePatternPropertiesKey: string | undefined; + protected updateOverridePatternPropertiesKey(): void { + const oldKey = this.overridePatternPropertiesKey; + const newKey = this.computeOverridePatternPropertiesKey(); + if (oldKey === newKey) { + return; + } + if (oldKey) { + delete this.combinedSchema.patternProperties[oldKey]; + } + this.overridePatternPropertiesKey = newKey; + if (newKey) { + this.combinedSchema.patternProperties[newKey] = this.overridePatternProperties; + } + this.fireDidPreferenceSchemaChanged(); + } + protected computeOverridePatternPropertiesKey(): string | undefined { + let param: string = ''; + for (const overrideIdentifier of this.overrideIdentifiers.keys()) { + if (param.length) { + param += '|'; + } + param += new RegExp(escapeRegExpCharacters(overrideIdentifier)).source; + } + return param.length ? OVERRIDE_PATTERN_WITH_SUBSTITUTION.replace('${0}', param) : undefined; + } + + protected doSetSchema(schema: PreferenceSchema): PreferenceProviderDataChange[] { + const scope = this.getScope(); + const domain = this.getDomain(); + const changes: PreferenceProviderDataChange[] = []; const defaultScope = PreferenceSchema.getDefaultScope(schema); - const props: string[] = []; - for (const property of Object.keys(schema.properties)) { - const schemaProps = schema.properties[property]; - if (this.combinedSchema.properties[property]) { - console.error('Preference name collision detected in the schema for property: ' + property); + const overridable = schema.overridable || false; + for (const preferenceName of Object.keys(schema.properties)) { + if (this.combinedSchema.properties[preferenceName]) { + console.error('Preference name collision detected in the schema for property: ' + preferenceName); } else { - this.combinedSchema.properties[property] = PreferenceDataProperty.fromPreferenceSchemaProperty(schemaProps, defaultScope); - props.push(property); + const schemaProps = PreferenceDataProperty.fromPreferenceSchemaProperty(schema.properties[preferenceName], defaultScope); + if (typeof schemaProps.overridable !== 'boolean' && overridable) { + schemaProps.overridable = true; + } + if (schemaProps.overridable) { + this.overridePatternProperties.properties[preferenceName] = schemaProps; + } + const newValue = schemaProps.default = this.getDefaultValue(schemaProps); + this.combinedSchema.properties[preferenceName] = schemaProps; + this.preferences[preferenceName] = newValue; + changes.push({ preferenceName, newValue, scope, domain }); } } - for (const property of props) { - this.preferences[property] = this.getDefaultValue(this.combinedSchema.properties[property]); - } + return changes; } - protected getDefaultValue(property: PreferenceDataProperty): any { + protected getDefaultValue(property: PreferenceItem): any { if (property.default) { return property.default; } @@ -164,15 +240,8 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } setSchema(schema: PreferenceSchema): void { - this.doSetSchema(schema); - this.updateValidate(); - const changes: PreferenceProviderDataChange[] = []; - for (const property of Object.keys(schema.properties)) { - const schemaProps = schema.properties[property]; - changes.push({ - preferenceName: property, newValue: schemaProps.default, oldValue: undefined, scope: this.getScope(), domain: this.getDomain() - }); - } + const changes = this.doSetSchema(schema); + this.fireDidPreferenceSchemaChanged(); this.emitPreferencesChangedEvent(changes); } @@ -188,11 +257,55 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return { priority: PreferenceProviderPriority.Default, provider: this }; } - isValidInScope(prefName: string, scope: PreferenceScope): boolean { - const schemaProps = this.combinedSchema.properties[prefName]; - if (schemaProps) { - return schemaProps.scope! >= scope; + isValidInScope(preferenceName: string, scope: PreferenceScope): boolean { + const preference = this.getPreferenceProperty(preferenceName); + if (preference) { + return preference.scope! >= scope; } return false; } + + *getPreferenceNames(): IterableIterator { + for (const preferenceName in this.combinedSchema.properties) { + yield preferenceName; + for (const overridePreferenceName of this.getOverridePreferenceNames(preferenceName)) { + yield overridePreferenceName; + } + } + } + + *getOverridePreferenceNames(preferenceName: string): IterableIterator { + const preference = this.combinedSchema.properties[preferenceName]; + if (preference && preference.overridable) { + for (const overrideIdentifier of this.overrideIdentifiers) { + yield this.overridePreferenceName({ preferenceName, overrideIdentifier }); + } + } + } + + getPreferenceProperty(preferenceName: string): PreferenceItem | undefined { + const overriden = this.overridenPreferenceName(preferenceName); + return this.combinedSchema.properties[overriden ? overriden.preferenceName : preferenceName]; + } + + overridePreferenceName({ preferenceName, overrideIdentifier }: OverridePreferenceName): string { + return `[${overrideIdentifier}].${preferenceName}`; + } + overridenPreferenceName(name: string): OverridePreferenceName | undefined { + const index = name.indexOf('.'); + if (index === -1) { + return undefined; + } + const matches = name.substr(0, index).match(OVERRIDE_PROPERTY_PATTERN); + const overrideIdentifier = matches && matches[1]; + if (!overrideIdentifier || !this.overrideIdentifiers.has(overrideIdentifier)) { + return undefined; + } + const preferenceName = name.substr(index + 1); + return { preferenceName, overrideIdentifier }; + } + + testOverrideValue(name: string, value: any): boolean { + return typeof value === 'object' && OVERRIDE_PROPERTY_PATTERN.test(name); + } } diff --git a/packages/core/src/browser/preferences/preference-proxy.ts b/packages/core/src/browser/preferences/preference-proxy.ts index 89ffd5d2d14fe..9b7a8ae54f88d 100644 --- a/packages/core/src/browser/preferences/preference-proxy.ts +++ b/packages/core/src/browser/preferences/preference-proxy.ts @@ -17,14 +17,14 @@ // tslint:disable:no-any import { Disposable, DisposableCollection, Event, Emitter } from '../../common'; -import { PreferenceService, PreferenceChange } from './preference-service'; -import { PreferenceSchema } from './preference-contribution'; +import { PreferenceService } from './preference-service'; +import { PreferenceSchema, OverridePreferenceName } from './preference-contribution'; export interface PreferenceChangeEvent { readonly preferenceName: keyof T; readonly newValue?: T[keyof T]; readonly oldValue?: T[keyof T]; - affects(resourceUri?: string): boolean; + affects(resourceUri?: string, overrideIdentifier?: string): boolean; } export interface PreferenceEventEmitter { @@ -33,24 +33,46 @@ export interface PreferenceEventEmitter { } export interface PreferenceRetrieval { - get(preferenceName: K, defaultValue?: T[K], resourceUri?: string): T[K]; + get(preferenceName: K | { + preferenceName: K, + overrideIdentifier?: string + }, defaultValue?: T[K], resourceUri?: string): T[K]; } export type PreferenceProxy = Readonly & Disposable & PreferenceEventEmitter & PreferenceRetrieval; export function createPreferenceProxy(preferences: PreferenceService, schema: PreferenceSchema): PreferenceProxy { const toDispose = new DisposableCollection(); - const onPreferenceChangedEmitter = new Emitter(); + const onPreferenceChangedEmitter = new Emitter>(); toDispose.push(onPreferenceChangedEmitter); toDispose.push(preferences.onPreferenceChanged(e => { - if (schema.properties[e.preferenceName]) { - onPreferenceChangedEmitter.fire(e); + const overriden = preferences.overridenPreferenceName(e.preferenceName); + const preferenceName: any = overriden ? overriden.preferenceName : e.preferenceName; + if (schema.properties[preferenceName]) { + const { newValue, oldValue } = e; + onPreferenceChangedEmitter.fire({ + newValue, oldValue, preferenceName, + affects: (resourceUri, overrideIdentifier) => { + if (overrideIdentifier !== undefined) { + if (overriden && overriden.overrideIdentifier !== overrideIdentifier) { + return false; + } + } + return e.affects(resourceUri); + } + }); } })); const unsupportedOperation = (_: any, __: string) => { throw new Error('Unsupported operation'); }; + const getValue: PreferenceRetrieval['get'] = (arg, defaultValue, resourceUri) => { + const preferenceName = typeof arg === 'object' && arg.overrideIdentifier ? + preferences.overridePreferenceName(arg) : + arg; + return preferences.get(preferenceName, defaultValue, resourceUri); + }; return new Proxy({}, { get: (_, property: string) => { if (schema.properties[property]) { @@ -66,7 +88,7 @@ export function createPreferenceProxy(preferences: PreferenceService, schema: return preferences.ready; } if (property === 'get') { - return preferences.get.bind(preferences); + return getValue; } throw new Error(`unexpected property: ${property}`); }, diff --git a/packages/core/src/browser/preferences/preference-service.ts b/packages/core/src/browser/preferences/preference-service.ts index d34d3101ad1ba..eb3d93a637de8 100644 --- a/packages/core/src/browser/preferences/preference-service.ts +++ b/packages/core/src/browser/preferences/preference-service.ts @@ -22,7 +22,7 @@ import { FrontendApplicationContribution } from '../../browser'; import { Event, Emitter, DisposableCollection, Disposable, deepFreeze } from '../../common'; import { Deferred } from '../../common/promise-util'; import { PreferenceProvider, PreferenceProviderDataChange, PreferenceProviderDataChanges } from './preference-provider'; -import { PreferenceSchemaProvider } from './preference-contribution'; +import { PreferenceSchemaProvider, OverridePreferenceName } from './preference-contribution'; import URI from '../../common/uri'; export enum PreferenceScope { @@ -134,6 +134,9 @@ export interface PreferenceService extends Disposable { get(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined; set(preferenceName: string, value: any, scope?: PreferenceScope, resourceUri?: string): Promise; onPreferenceChanged: Event; + + overridePreferenceName(options: OverridePreferenceName): string; + overridenPreferenceName(preferenceName: string): OverridePreferenceName | undefined; } /** @@ -217,25 +220,35 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica protected reconcilePreferences(changes?: PreferenceProviderDataChanges): void { const changesToEmit: PreferenceChanges = {}; + const acceptChange = (change: PreferenceProviderDataChange) => + this.getAffectedPreferenceNames(change, preferenceName => + changesToEmit[preferenceName] = new PreferenceChangeImpl({ ...change, preferenceName }, this.providersMap) + ); if (changes) { - for (const prefName of Object.keys(changes)) { - const change = changes[prefName]; - if (this.schema.isValidInScope(prefName, PreferenceScope.Folder)) { - const toEmit = new PreferenceChangeImpl(change, this.providersMap); - changesToEmit[prefName] = toEmit; + for (const preferenceName of Object.keys(changes)) { + let change = changes[preferenceName]; + if (!change.newValue) { + const overriden = this.overridenPreferenceName(change.preferenceName); + if (overriden) { + change = { + ...change, newValue: this.doGet(overriden.preferenceName) + }; + } + } + if (this.schema.isValidInScope(preferenceName, PreferenceScope.Folder)) { + acceptChange(change); continue; } - for (const s of PreferenceScope.getReversedScopes()) { - if (this.schema.isValidInScope(prefName, s)) { - const p = this.providersMap.get(s); - if (p) { - const value = p.get(prefName); - if (s > change.scope && value !== undefined && value !== null) { + for (const scope of PreferenceScope.getReversedScopes()) { + if (this.schema.isValidInScope(preferenceName, scope)) { + const provider = this.getProvider(scope); + if (provider) { + const value = provider.get(preferenceName); + if (scope > change.scope && value !== undefined && value !== null) { // preference defined in a more specific scope break; - } else if (s === change.scope) { - const toEmit = new PreferenceChangeImpl(change, this.providersMap); - changesToEmit[prefName] = toEmit; + } else if (scope === change.scope) { + acceptChange(change); } } } @@ -250,10 +263,7 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica if (newValue === undefined && oldValue !== newValue || oldValue === undefined && newValue !== oldValue // JSONExt.deepEqual() does not support handling `undefined` || !JSONExt.deepEqual(oldValue, newValue)) { - const toEmit = new PreferenceChangeImpl({ - newValue, oldValue, preferenceName, scope: PreferenceScope.Workspace, domain: [] - }, this.providersMap); - changesToEmit[preferenceName] = toEmit; + acceptChange({ newValue, oldValue, preferenceName, scope: PreferenceScope.Workspace, domain: [] }); } } this.preferences = newPrefs; @@ -266,6 +276,14 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica } changedPreferenceNames.forEach(preferenceName => this.onPreferenceChangedEmitter.fire(changesToEmit[preferenceName])); } + protected getAffectedPreferenceNames(change: PreferenceProviderDataChange, accept: (affectedPreferenceName: string) => void): void { + accept(change.preferenceName); + for (const overridePreferenceName of this.schema.getOverridePreferenceNames(change.preferenceName)) { + if (!this.doHas(overridePreferenceName)) { + accept(overridePreferenceName); + } + } + } protected doCreateProvider(scope: PreferenceScope): PreferenceProvider | undefined { if (!this.providersMap.has(scope)) { @@ -282,36 +300,46 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica this.toDispose.push(provider); } + protected getProvider(scope: PreferenceScope, preferenceName?: string, resourceUri?: string): PreferenceProvider | undefined { + const provider = this.providersMap.get(scope); + if (provider && (!preferenceName || provider.canProvide(preferenceName, resourceUri).priority >= 0)) { + return provider; + } + return undefined; + } + getPreferences(resourceUri?: string): { [key: string]: any } { - const prefs: { [key: string]: any } = {}; - Object.keys(this.schema.getCombinedSchema().properties).forEach(p => { - prefs[p] = resourceUri ? this.get(p, undefined, resourceUri) : this.get(p, undefined); - }); - return prefs; + const preferences: { [key: string]: any } = {}; + for (const preferenceName of this.schema.getPreferenceNames()) { + preferences[preferenceName] = this.get(preferenceName, undefined, resourceUri); + } + return preferences; } has(preferenceName: string, resourceUri?: string): boolean { - return resourceUri ? this.get(preferenceName, undefined, resourceUri) !== undefined : this.get(preferenceName, undefined) !== undefined; + return this.get(preferenceName, undefined, resourceUri) !== undefined; } get(preferenceName: string): T | undefined; get(preferenceName: string, defaultValue: T): T; get(preferenceName: string, defaultValue: T, resourceUri: string): T; + get(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined; get(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined { - for (const s of PreferenceScope.getReversedScopes()) { - if (this.schema.isValidInScope(preferenceName, s)) { - const p = this.providersMap.get(s); - if (p && p.canProvide(preferenceName, resourceUri).priority >= 0) { - const value = p.get(preferenceName, resourceUri); - const ret = value !== null && value !== undefined ? value : defaultValue; - return deepFreeze(ret); - } + const value = this.doGet(preferenceName, defaultValue, resourceUri); + if (value === null || value === undefined) { + const overriden = this.overridenPreferenceName(preferenceName); + if (overriden) { + return this.doGet(overriden.preferenceName, defaultValue, resourceUri); } } + return value; } - set(preferenceName: string, value: any, scope: PreferenceScope = PreferenceScope.User, resourceUri?: string): Promise { - return this.providerProvider(scope).setPreference(preferenceName, value, resourceUri); + async set(preferenceName: string, value: any, scope: PreferenceScope = PreferenceScope.User, resourceUri?: string): Promise { + const provider = this.getProvider(scope); + if (provider) { + await provider.setPreference(preferenceName, value, resourceUri); + } } getBoolean(preferenceName: string): boolean | undefined; @@ -347,22 +375,6 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica return Number(value); } - protected inpsectInScope(preferenceName: string, scope: PreferenceScope, resourceUri?: string): T | undefined { - const val = this.inspect(preferenceName, resourceUri); - if (val) { - switch (scope) { - case PreferenceScope.Default: - return val.defaultValue; - case PreferenceScope.User: - return val.globalValue; - case PreferenceScope.Workspace: - return val.workspaceValue; - case PreferenceScope.Folder: - return val.workspaceFolderValue; - } - } - } - inspect(preferenceName: string, resourceUri?: string): { preferenceName: string, defaultValue: T | undefined, @@ -370,22 +382,52 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica workspaceValue: T | undefined, // Workspace Preference workspaceFolderValue: T | undefined // Folder Preference } | undefined { - const schemaProps = this.schema.getCombinedSchema().properties[preferenceName]; + const schemaProps = this.schema.getPreferenceProperty(preferenceName); if (schemaProps) { const defaultValue = schemaProps.default; - const userProvider = this.providersMap.get(PreferenceScope.User); - const globalValue = userProvider && userProvider.canProvide(preferenceName, resourceUri).priority >= 0 - ? userProvider.get(preferenceName, resourceUri) : undefined; + const globalValue = this.inspectInScope(preferenceName, PreferenceScope.User, resourceUri); + const workspaceValue = this.inspectInScope(preferenceName, PreferenceScope.Workspace, resourceUri); + const workspaceFolderValue = this.inspectInScope(preferenceName, PreferenceScope.Folder, resourceUri); - const workspaceProvider = this.providersMap.get(PreferenceScope.Workspace); - const workspaceValue = workspaceProvider && workspaceProvider.canProvide(preferenceName, resourceUri).priority >= 0 - ? workspaceProvider.get(preferenceName, resourceUri) : undefined; + return { preferenceName, defaultValue, globalValue, workspaceValue, workspaceFolderValue }; + } + } + protected inspectInScope(preferenceName: string, scope: PreferenceScope, resourceUri?: string): T | undefined { + const value = this.doInspectInScope(preferenceName, scope, resourceUri); + if (value === null || value === undefined) { + const overriden = this.overridenPreferenceName(preferenceName); + if (overriden) { + return this.doInspectInScope(overriden.preferenceName, scope, resourceUri); + } + } + return value; + } - const folderProvider = this.providersMap.get(PreferenceScope.Folder); - const workspaceFolderValue = folderProvider && folderProvider.canProvide(preferenceName, resourceUri).priority >= 0 - ? folderProvider.get(preferenceName, resourceUri) : undefined; + overridePreferenceName(options: OverridePreferenceName): string { + return this.schema.overridePreferenceName(options); + } + overridenPreferenceName(preferenceName: string): OverridePreferenceName | undefined { + return this.schema.overridenPreferenceName(preferenceName); + } - return { preferenceName, defaultValue, globalValue, workspaceValue, workspaceFolderValue }; + protected doHas(preferenceName: string, resourceUri?: string): boolean { + return this.doGet(preferenceName, undefined, resourceUri) !== undefined; + } + protected doInspectInScope(preferenceName: string, scope: PreferenceScope, resourceUri?: string): T | undefined { + const provider = this.getProvider(scope, preferenceName, resourceUri); + return provider && provider.get(preferenceName, resourceUri); + } + protected doGet(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined { + for (const scope of PreferenceScope.getReversedScopes()) { + if (this.schema.isValidInScope(preferenceName, scope)) { + const provider = this.getProvider(scope, preferenceName, resourceUri); + if (provider) { + const value = provider.get(preferenceName, resourceUri); + const result = value !== null && value !== undefined ? value : defaultValue; + return deepFreeze(result); + } + } } } + } diff --git a/packages/core/src/browser/preferences/test/mock-preference-provider.ts b/packages/core/src/browser/preferences/test/mock-preference-provider.ts index 0913c3518c61f..9c6dd021f0b0e 100644 --- a/packages/core/src/browser/preferences/test/mock-preference-provider.ts +++ b/packages/core/src/browser/preferences/test/mock-preference-provider.ts @@ -16,6 +16,7 @@ import { injectable } from 'inversify'; import { PreferenceProvider, PreferenceProviderPriority } from '../'; +import { PreferenceScope } from '../preference-service'; @injectable() export class MockPreferenceProvider extends PreferenceProvider { @@ -26,8 +27,10 @@ export class MockPreferenceProvider extends PreferenceProvider { return this.prefs; } // tslint:disable-next-line:no-any - setPreference(key: string, value: any, resourceUri?: string): Promise { - return Promise.resolve(); + async setPreference(preferenceName: string, newValue: any, resourceUri?: string): Promise { + const oldValue = this.prefs[preferenceName]; + this.prefs[preferenceName] = newValue; + this.emitPreferencesChangedEvent([{ preferenceName, oldValue, newValue, scope: PreferenceScope.User, domain: [] }]); } canProvide(preferenceName: string, resourceUri?: string): { priority: number, provider: PreferenceProvider } { if (this.prefs[preferenceName] === undefined) { diff --git a/packages/core/src/browser/preferences/test/mock-preference-service.ts b/packages/core/src/browser/preferences/test/mock-preference-service.ts index 4b3f46ac528c0..4fe2e5c53b804 100644 --- a/packages/core/src/browser/preferences/test/mock-preference-service.ts +++ b/packages/core/src/browser/preferences/test/mock-preference-service.ts @@ -17,6 +17,7 @@ import { injectable } from 'inversify'; import { PreferenceService, PreferenceChange } from '../'; import { Emitter, Event } from '../../../common'; +import { OverridePreferenceName } from '../preference-contribution'; @injectable() export class MockPreferenceService implements PreferenceService { @@ -32,4 +33,10 @@ export class MockPreferenceService implements PreferenceService { set(preferenceName: string, value: any): Promise { return Promise.resolve(); } ready: Promise = Promise.resolve(); readonly onPreferenceChanged: Event = new Emitter().event; + overridePreferenceName(options: OverridePreferenceName): string { + return options.preferenceName; + } + overridenPreferenceName(preferenceName: string): OverridePreferenceName | undefined { + return undefined; + } } diff --git a/packages/core/src/common/strings.ts b/packages/core/src/common/strings.ts index 7714171c88d24..5f3db25ea6275 100644 --- a/packages/core/src/common/strings.ts +++ b/packages/core/src/common/strings.ts @@ -27,10 +27,14 @@ export function* split(s: string, splitter: string): IterableIterator { } } -export function escapeInvisibleChars(value: string ): string { +export function escapeInvisibleChars(value: string): string { return value.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); } export function unescapeInvisibleChars(value: string): string { return value.replace(/\\n/g, '\n').replace(/\\r/g, '\r'); } + +export function escapeRegExpCharacters(value: string): string { + return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&'); +} diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index c81035f138f0e..efb1fab549720 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -28,6 +28,7 @@ import { isOSX } from '@theia/core/lib/common/os'; export const editorPreferenceSchema: PreferenceSchema = { 'type': 'object', 'scope': 'resource', + 'overridable': true, 'properties': { 'editor.tabSize': { 'type': 'number', @@ -65,12 +66,14 @@ export const editorPreferenceSchema: PreferenceSchema = { 'off' ], 'default': 'on', - 'description': 'Configure whether the editor should be auto saved.' + 'description': 'Configure whether the editor should be auto saved.', + overridable: false }, 'editor.autoSaveDelay': { 'type': 'number', 'default': 500, - 'description': 'Configure the auto save delay in milliseconds.' + 'description': 'Configure the auto save delay in milliseconds.', + overridable: false }, 'editor.rulers': { 'type': 'array', diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 3fe7c93e304d7..ee9cb0033031f 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -144,10 +144,11 @@ export class MonacoEditorProvider { const options = this.createMonacoEditorOptions(model); const editor = new MonacoEditor(uri, model, document.createElement('div'), this.m2p, this.p2m, options, override); toDispose.push(this.editorPreferences.onPreferenceChanged(event => { - if (event.affects(uri.toString())) { + if (event.affects(uri.toString(), model.languageId)) { this.updateMonacoEditorOptions(editor, event); } })); + toDispose.push(editor.onLanguageChanged(() => this.updateMonacoEditorOptions(editor))); editor.document.onWillSaveModel(event => { event.waitUntil(new Promise(async resolve => { if (event.reason === TextDocumentSaveReason.Manual && this.editorPreferences['editor.formatOnSave']) { @@ -159,15 +160,22 @@ export class MonacoEditorProvider { return editor; } protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions { - const options = this.createOptions(this.preferencePrefixes, model.uri); + const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId); options.model = model.textEditorModel; options.readOnly = model.readOnly; return options; } - protected updateMonacoEditorOptions(editor: MonacoEditor, event: EditorPreferenceChange): void { - const preferenceName = event.preferenceName; - const newValue = this.editorPreferences.get(preferenceName, undefined, editor.uri.toString()); - editor.getControl().updateOptions(this.setOption(preferenceName, newValue, this.preferencePrefixes)); + protected updateMonacoEditorOptions(editor: MonacoEditor, event?: EditorPreferenceChange): void { + if (event) { + const preferenceName = event.preferenceName; + const overrideIdentifier = editor.document.languageId; + const newValue = this.editorPreferences.get({ preferenceName, overrideIdentifier }, undefined, editor.uri.toString()); + editor.getControl().updateOptions(this.setOption(preferenceName, newValue, this.preferencePrefixes)); + } else { + const options = this.createMonacoEditorOptions(editor.document); + delete options.model; + editor.getControl().updateOptions(options); + } } protected get diffPreferencePrefixes(): string[] { @@ -189,27 +197,37 @@ export class MonacoEditorProvider { override); toDispose.push(this.editorPreferences.onPreferenceChanged(event => { const originalFileUri = original.withoutQuery().withScheme('file').toString(); - if (event.affects(originalFileUri)) { + if (event.affects(originalFileUri, editor.document.languageId)) { this.updateMonacoDiffEditorOptions(editor, event, originalFileUri); } })); + toDispose.push(editor.onLanguageChanged(() => this.updateMonacoDiffEditorOptions(editor))); return editor; } protected createMonacoDiffEditorOptions(original: MonacoEditorModel, modified: MonacoEditorModel): MonacoDiffEditor.IOptions { - const options = this.createOptions(this.diffPreferencePrefixes, modified.uri); + const options = this.createOptions(this.diffPreferencePrefixes, modified.uri, modified.languageId); options.originalEditable = !original.readOnly; options.readOnly = modified.readOnly; return options; } - protected updateMonacoDiffEditorOptions(editor: MonacoDiffEditor, event: EditorPreferenceChange, resourceUri?: string): void { - const preferenceName = event.preferenceName; - const newValue = this.editorPreferences.get(preferenceName, undefined, resourceUri); - editor.diffEditor.updateOptions(this.setOption(preferenceName, newValue, this.diffPreferencePrefixes)); + protected updateMonacoDiffEditorOptions(editor: MonacoDiffEditor, event?: EditorPreferenceChange, resourceUri?: string): void { + if (event) { + const preferenceName = event.preferenceName; + const overrideIdentifier = editor.document.languageId; + const newValue = this.editorPreferences.get({ preferenceName, overrideIdentifier }, undefined, resourceUri); + editor.diffEditor.updateOptions(this.setOption(preferenceName, newValue, this.diffPreferencePrefixes)); + } else { + const options = this.createMonacoDiffEditorOptions(editor.originalModel, editor.modifiedModel); + editor.diffEditor.updateOptions(options); + } } - protected createOptions(prefixes: string[], uri: string): { [name: string]: any } { + /** @deprecated always pass a language as an overrideIdentifier */ + protected createOptions(prefixes: string[], uri: string): { [name: string]: any }; + protected createOptions(prefixes: string[], uri: string, overrideIdentifier: string): { [name: string]: any }; + protected createOptions(prefixes: string[], uri: string, overrideIdentifier?: string): { [name: string]: any } { return Object.keys(this.editorPreferences).reduce((options, preferenceName) => { - const value = (this.editorPreferences).get(preferenceName, undefined, uri); + const value = (this.editorPreferences).get({ preferenceName, overrideIdentifier }, undefined, uri); return this.setOption(preferenceName, value, prefixes, options); }, {}); } diff --git a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts index 419294f9d5fb6..2e36d16d03f86 100644 --- a/packages/monaco/src/browser/monaco-frontend-application-contribution.ts +++ b/packages/monaco/src/browser/monaco-frontend-application-contribution.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, PreferenceSchemaProvider } from '@theia/core/lib/browser'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider'; @@ -28,12 +28,24 @@ export class MonacoFrontendApplicationContribution implements FrontendApplicatio @inject(MonacoSnippetSuggestProvider) protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider; + @inject(PreferenceSchemaProvider) + protected readonly preferenceSchema: PreferenceSchemaProvider; + async initialize() { const currentTheme = this.themeService.getCurrentTheme(); this.changeTheme(currentTheme.editorTheme); this.themeService.onThemeChange(event => this.changeTheme(event.newTheme.editorTheme)); monaco.suggest.setSnippetSuggestSupport(this.snippetSuggestProvider); + + for (const language of monaco.languages.getLanguages()) { + this.preferenceSchema.registerOverrideIdentifier(language.id); + } + const registerLanguage = monaco.languages.register.bind(monaco.languages); + monaco.languages.register = language => { + registerLanguage(language); + this.preferenceSchema.registerOverrideIdentifier(language.id); + }; } protected changeTheme(editorTheme: string | undefined) { diff --git a/packages/monaco/src/browser/monaco-text-model-service.ts b/packages/monaco/src/browser/monaco-text-model-service.ts index c69e06036c006..e4dcef0e0b0dd 100644 --- a/packages/monaco/src/browser/monaco-text-model-service.ts +++ b/packages/monaco/src/browser/monaco-text-model-service.ts @@ -57,13 +57,11 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { } protected async loadModel(uri: URI): Promise { - const uriStr = uri.toString(); await this.editorPreferences.ready; const resource = await this.resourceProvider(uri); const model = await (new MonacoEditorModel(resource, this.m2p, this.p2m).load()); - model.autoSave = this.editorPreferences.get('editor.autoSave', undefined, uriStr); - model.autoSaveDelay = this.editorPreferences.get('editor.autoSaveDelay', undefined, uriStr); - model.textEditorModel.updateOptions(this.getModelOptions(uriStr)); + this.updateModel(model); + model.textEditorModel.onDidChangeLanguage(() => this.updateModel(model)); const disposable = this.editorPreferences.onPreferenceChanged(change => this.updateModel(model, change)); model.onDispose(() => disposable.dispose()); return model; @@ -76,26 +74,40 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { 'editor.insertSpaces': 'insertSpaces' }; - protected updateModel(model: MonacoEditorModel, change: EditorPreferenceChange): void { - if (change.preferenceName === 'editor.autoSave') { + protected updateModel(model: MonacoEditorModel, change?: EditorPreferenceChange): void { + if (change) { + if (!change.affects(model.uri, model.languageId)) { + return; + } + if (change.preferenceName === 'editor.autoSave') { + model.autoSave = this.editorPreferences.get('editor.autoSave', undefined, model.uri); + } + if (change.preferenceName === 'editor.autoSaveDelay') { + model.autoSaveDelay = this.editorPreferences.get('editor.autoSaveDelay', undefined, model.uri); + } + const modelOption = this.modelOptions[change.preferenceName]; + if (modelOption) { + const options: monaco.editor.ITextModelUpdateOptions = {}; + // tslint:disable-next-line:no-any + options[modelOption] = change.newValue as any; + model.textEditorModel.updateOptions(options); + } + } else { model.autoSave = this.editorPreferences.get('editor.autoSave', undefined, model.uri); - } - if (change.preferenceName === 'editor.autoSaveDelay') { model.autoSaveDelay = this.editorPreferences.get('editor.autoSaveDelay', undefined, model.uri); - } - const modelOption = this.modelOptions[change.preferenceName]; - if (modelOption) { - const options: monaco.editor.ITextModelUpdateOptions = {}; - // tslint:disable-next-line:no-any - options[modelOption] = change.newValue as any; - model.textEditorModel.updateOptions(options); + model.textEditorModel.updateOptions(this.getModelOptions(model)); } } - protected getModelOptions(uri: string): monaco.editor.ITextModelUpdateOptions { + /** @deprecated pass MonacoEditorModel instead */ + protected getModelOptions(uri: string): monaco.editor.ITextModelUpdateOptions; + protected getModelOptions(model: MonacoEditorModel): monaco.editor.ITextModelUpdateOptions; + protected getModelOptions(arg: string | MonacoEditorModel): monaco.editor.ITextModelUpdateOptions { + const uri = typeof arg === 'string' ? arg : arg.uri; + const overrideIdentifier = typeof arg === 'string' ? undefined : arg.languageId; return { - tabSize: this.editorPreferences.get('editor.tabSize', undefined, uri), - insertSpaces: this.editorPreferences.get('editor.insertSpaces', undefined, uri) + tabSize: this.editorPreferences.get({ preferenceName: 'editor.tabSize', overrideIdentifier }, undefined, uri), + insertSpaces: this.editorPreferences.get({ preferenceName: 'editor.insertSpaces', overrideIdentifier }, undefined, uri) }; } diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index a30e2902852ab..95a38c1f28954 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -18,7 +18,8 @@ import { RPCProtocol } from '../api/rpc-protocol'; import { Disposable } from '@theia/core/lib/common/disposable'; import { LogPart, KeysToAnyValues, KeysToKeysToAnyValue } from './types'; import { CharacterPair, CommentRule, PluginAPIFactory, Plugin } from '../api/plugin-api'; -import { PreferenceSchema } from '@theia/core/lib/browser/preferences'; +// FIXME get rid of browser code in backend +import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; @@ -55,6 +56,7 @@ export interface PluginPackage { */ export interface PluginPackageContribution { configuration?: PreferenceSchema; + configurationDefaults?: PreferenceSchemaProperties; languages?: PluginPackageLanguageContribution[]; grammars?: PluginPackageGrammarsContribution[]; viewsContainers?: { [location: string]: PluginPackageViewContainer[] }; @@ -347,6 +349,7 @@ export interface PluginModel { */ export interface PluginContribution { configuration?: PreferenceSchema; + configurationDefaults?: PreferenceSchemaProperties; languages?: LanguageContribution[]; grammars?: GrammarsContribution[]; viewsContainers?: { [location: string]: ViewContainer[] }; diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 9d0dc1b8831d7..6331b304a1030 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -119,6 +119,7 @@ export class TheiaPluginScanner implements PluginScanner { const config = this.readConfiguration(rawPlugin.contributes.configuration!, rawPlugin.packagePath); contributions.configuration = config; } + contributions.configurationDefaults = rawPlugin.contributes.configurationDefaults; if (rawPlugin.contributes!.languages) { const languages = this.readLanguages(rawPlugin.contributes.languages!, rawPlugin.packagePath); diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 7cad8d6d98f8e..1639da7024871 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -21,7 +21,7 @@ import { MenusContributionPointHandler } from './menus/menus-contribution-handle import { ViewRegistry } from './view/view-registry'; import { PluginContribution, IndentationRules, FoldingRules, ScopeMap } from '../../common'; import { PreferenceSchemaProvider } from '@theia/core/lib/browser'; -import { PreferenceSchema } from '@theia/core/lib/browser/preferences'; +import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences'; import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler'; import { MonacoSnippetSuggestProvider } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider'; import { PluginSharedStyle } from './plugin-shared-style'; @@ -64,6 +64,9 @@ export class PluginContributionHandler { if (contributions.configuration) { this.updateConfigurationSchema(contributions.configuration); } + if (contributions.configurationDefaults) { + this.updateDefaultOverridesSchema(contributions.configurationDefaults); + } if (contributions.languages) { for (const lang of contributions.languages) { @@ -185,6 +188,28 @@ export class PluginContributionHandler { this.preferenceSchemaProvider.setSchema(schema); } + protected updateDefaultOverridesSchema(configurationDefaults: PreferenceSchemaProperties): void { + const defaultOverrides: PreferenceSchema = { + id: 'defaultOverrides', + title: 'Default Configuration Overrides', + properties: {} + }; + // tslint:disable-next-line:forin + for (const key in configurationDefaults) { + const defaultValue = configurationDefaults[key]; + if (this.preferenceSchemaProvider.testOverrideValue(key, defaultValue)) { + defaultOverrides.properties[key] = { + type: 'object', + default: defaultValue, + description: `Configure editor settings to be overridden for ${key} language.` + }; + } + } + if (Object.keys(defaultOverrides.properties).length) { + this.preferenceSchemaProvider.setSchema(defaultOverrides); + } + } + private createRegex(value: string | undefined): RegExp | undefined { if (typeof value === 'string') { return new RegExp(value, ''); diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index 543709cb080a8..05de310a9835b 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -119,8 +119,31 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi // tslint:disable-next-line:no-any protected async getParsedContent(content: string): Promise<{ [key: string]: any }> { const strippedContent = jsoncparser.stripComments(content); - const newPrefs = jsoncparser.parse(strippedContent) || {}; - return newPrefs; + const jsonData = jsoncparser.parse(strippedContent); + // tslint:disable-next-line:no-any + const preferences: { [key: string]: any } = {}; + if (typeof jsonData !== 'object') { + return preferences; + } + const uri = (await this.resource).uri.toString(); + // tslint:disable-next-line:forin + for (const preferenceName in jsonData) { + const preferenceValue = jsonData[preferenceName]; + if (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)) { + console.warn(`Preference ${preferenceName} in ${uri} is invalid.`); + continue; + } + if (this.schemaProvider.testOverrideValue(preferenceName, preferenceValue)) { + // tslint:disable-next-line:forin + for (const overriddenPreferenceName in preferenceValue) { + const overriddeValue = preferenceValue[overriddenPreferenceName]; + preferences[`${preferenceName}.${overriddenPreferenceName}`] = overriddeValue; + } + } else { + preferences[preferenceName] = preferenceValue; + } + } + return preferences; } // tslint:disable-next-line:no-any @@ -129,24 +152,19 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi this.preferences = newPrefs; const prefNames = new Set([...Object.keys(oldPrefs), ...Object.keys(newPrefs)]); const prefChanges: PreferenceProviderDataChange[] = []; + const uri = (await this.resource).uri.toString(); for (const prefName of prefNames.values()) { const oldValue = oldPrefs[prefName]; const newValue = newPrefs[prefName]; - const prefNameAndFile = `Preference ${prefName} in ${(await this.resource).uri.toString()}`; - if (!this.schemaProvider.validate(prefName, newValue) && newValue !== undefined) { // do not emit the change event if pref is not defined in schema - console.warn(`${prefNameAndFile} is invalid.`); - continue; - } const schemaProperties = this.schemaProvider.getCombinedSchema().properties[prefName]; if (schemaProperties) { const scope = schemaProperties.scope; // do not emit the change event if the change is made out of the defined preference scope if (!this.schemaProvider.isValidInScope(prefName, this.getScope())) { - console.warn(`${prefNameAndFile} can only be defined in scopes: ${PreferenceScope.getScopeNames(scope).join(', ')}.`); + console.warn(`Preference ${prefName} in ${uri} can only be defined in scopes: ${PreferenceScope.getScopeNames(scope).join(', ')}.`); continue; } } - if (newValue === undefined && oldValue !== newValue || oldValue === undefined && newValue !== oldValue // JSONExt.deepEqual() does not support handling `undefined` || !JSONExt.deepEqual(oldValue, newValue)) { diff --git a/packages/preferences/src/browser/preference-service.spec.ts b/packages/preferences/src/browser/preference-service.spec.ts index 9ed7308665d0f..eb6dde85c0a11 100644 --- a/packages/preferences/src/browser/preference-service.spec.ts +++ b/packages/preferences/src/browser/preference-service.spec.ts @@ -22,13 +22,14 @@ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; let disableJSDOM = enableJSDOM(); import { Container } from 'inversify'; +import * as assert from 'assert'; import * as chai from 'chai'; import * as fs from 'fs-extra'; import * as temp from 'temp'; import { Emitter } from '@theia/core/lib/common'; import { PreferenceService, PreferenceScope, PreferenceProviderDataChanges, - PreferenceSchemaProvider, PreferenceProviderProvider, PreferenceServiceImpl, bindPreferenceSchemaProvider + PreferenceSchemaProvider, PreferenceProviderProvider, PreferenceServiceImpl, bindPreferenceSchemaProvider, PreferenceChange } from '@theia/core/lib/browser/preferences'; import { FileSystem, FileShouldOverwrite, FileStat } from '@theia/filesystem/lib/common/'; import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; @@ -418,4 +419,233 @@ describe('Preference Service', () => { stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); expect(service.get('mypref')).to.equal(5); }); + + describe('overridden preferences', () => { + + it('getPreferences', () => { + const { preferences, schema } = prepareServices(); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + assert.deepEqual({ + 'editor.tabSize': 4 + }, preferences.getPreferences()); + + schema.registerOverrideIdentifier('json'); + + assert.deepEqual({ + 'editor.tabSize': 4, + '[json].editor.tabSize': 2 + }, preferences.getPreferences()); + }); + + it('get #0', () => { + const { preferences, schema } = prepareServices(); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + assert.equal(4, preferences.get('editor.tabSize')); + assert.equal(undefined, preferences.get('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.equal(4, preferences.get('editor.tabSize')); + assert.equal(2, preferences.get('[json].editor.tabSize')); + }); + + it('get #1', () => { + const { preferences, schema } = prepareServices(); + schema.registerOverrideIdentifier('json'); + + assert.equal(4, preferences.get('editor.tabSize')); + assert.equal(4, preferences.get('[json].editor.tabSize')); + + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + assert.equal(4, preferences.get('editor.tabSize')); + assert.equal(2, preferences.get('[json].editor.tabSize')); + }); + + it('get #2', () => { + const { preferences, schema } = prepareServices(); + schema.registerOverrideIdentifier('json'); + + assert.equal(4, preferences.get('editor.tabSize')); + assert.equal(4, preferences.get('[json].editor.tabSize')); + + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + assert.equal(2, preferences.get('editor.tabSize')); + assert.equal(2, preferences.get('[json].editor.tabSize')); + }); + + it('has', () => { + const { preferences, schema } = prepareServices(); + + assert.ok(preferences.has('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.ok(preferences.has('editor.tabSize')); + assert.ok(preferences.has('[json].editor.tabSize')); + }); + + it('inspect #0', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepEqual({ + ...expected, + preferenceName: '[json].editor.tabSize' + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('inspect #1', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: 2, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepEqual({ + ...expected, + preferenceName: '[json].editor.tabSize' + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('inspect #2', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + assert.deepEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepEqual({ + ...expected, + preferenceName: '[json].editor.tabSize', + globalValue: 2 + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('onPreferenceChanged #0', () => { + const { preferences, schema } = prepareServices(); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + preferences.set('editor.tabSize', 3, PreferenceScope.User); + + assert.deepEqual([{ + preferenceName: '[json].editor.tabSize', + newValue: 2 + }, { + preferenceName: 'editor.tabSize', + newValue: 3 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + it('onPreferenceChanged #1', () => { + const { preferences, schema } = prepareServices(); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + schema.registerOverrideIdentifier('json'); + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + assert.deepEqual([{ + preferenceName: 'editor.tabSize', + newValue: 2 + }, { + preferenceName: '[json].editor.tabSize', + newValue: 2 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + it('onPreferenceChanged #3', () => { + const { preferences, schema } = prepareServices(); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + preferences.set('editor.tabSize', 3, PreferenceScope.User); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + preferences.set('[json].editor.tabSize', undefined, PreferenceScope.User); + + assert.deepEqual([{ + preferenceName: '[json].editor.tabSize', + newValue: 3 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + function prepareServices() { + const container = new Container(); + bindPreferenceSchemaProvider(container.bind.bind(container)); + container.bind(PreferenceProviderProvider).toFactory(() => () => new MockPreferenceProvider()); + container.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); + + const schema = container.get(PreferenceSchemaProvider); + schema.setSchema({ + properties: { + 'editor.tabSize': { + type: 'number', + description: '', + overridable: true, + default: 4 + } + } + }); + + const preferences = container.get(PreferenceServiceImpl); + preferences.initialize(); + return { preferences, schema }; + } + + }); + }); diff --git a/packages/preferences/src/browser/preferences-frontend-application-contribution.ts b/packages/preferences/src/browser/preferences-frontend-application-contribution.ts index 91da9b3e14835..e1bd32331200d 100644 --- a/packages/preferences/src/browser/preferences-frontend-application-contribution.ts +++ b/packages/preferences/src/browser/preferences-frontend-application-contribution.ts @@ -36,7 +36,7 @@ export class PreferencesFrontendApplicationContribution implements FrontendAppli fileMatch: ['.theia/settings.json', USER_PREFERENCE_URI.toString()], url: uri.toString() }); - this.schemaProvider.onDidPreferencesChanged(() => + this.schemaProvider.onDidPreferenceSchemaChanged(() => this.inmemoryResources.update(uri, serializeSchema()) ); }