Skip to content

Commit

Permalink
fix #1106, fix #3540: support language specific preferences
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov authored and svenefftinge committed Feb 11, 2019
1 parent d9385ed commit 508e33c
Show file tree
Hide file tree
Showing 18 changed files with 681 additions and 154 deletions.
15 changes: 13 additions & 2 deletions .theia/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"[json]": {
"editor.tabSize": 2
},
"[jsonc]": {
"editor.tabSize": 2
},
"typescript.tsdk": "node_modules/typescript/lib",
"files.insertFinalNewline": true
}
175 changes: 144 additions & 31 deletions packages/core/src/browser/preferences/preference-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -63,6 +70,7 @@ export interface PreferenceItem {
properties?: { [name: string]: PreferenceItem };
additionalProperties?: object;
[name: string]: any;
overridable?: boolean;
}

export interface PreferenceSchemaProperty extends PreferenceItem {
Expand Down Expand Up @@ -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<PreferenceContribution>;
protected validateFunction: Ajv.ValidateFunction;

protected readonly onDidPreferenceSchemaChangedEmitter = new Emitter<void>();
readonly onDidPreferenceSchemaChanged: Event<void> = this.onDidPreferenceSchemaChangedEmitter.event;
protected fireDidPreferenceSchemaChanged(): void {
this.onDidPreferenceSchemaChangedEmitter.fire(undefined);
}

@postConstruct()
protected init(): void {
Expand All @@ -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<string>();
registerOverrideIdentifier(overrideIdentifier: string): void {
if (this.overrideIdentifiers.has(overrideIdentifier)) {
return;
}
this.overrideIdentifiers.add(overrideIdentifier);
this.updateOverridePatternPropertiesKey();
}

protected readonly overridePatternProperties: Required<Pick<PreferenceDataProperty, 'properties'>> & 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;
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -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<string> {
for (const preferenceName in this.combinedSchema.properties) {
yield preferenceName;
for (const overridePreferenceName of this.getOverridePreferenceNames(preferenceName)) {
yield overridePreferenceName;
}
}
}

*getOverridePreferenceNames(preferenceName: string): IterableIterator<string> {
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);
}
}
38 changes: 30 additions & 8 deletions packages/core/src/browser/preferences/preference-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
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<T> {
Expand All @@ -33,24 +33,46 @@ export interface PreferenceEventEmitter<T> {
}

export interface PreferenceRetrieval<T> {
get<K extends keyof T>(preferenceName: K, defaultValue?: T[K], resourceUri?: string): T[K];
get<K extends keyof T>(preferenceName: K | {
preferenceName: K,
overrideIdentifier?: string
}, defaultValue?: T[K], resourceUri?: string): T[K];
}

export type PreferenceProxy<T> = Readonly<T> & Disposable & PreferenceEventEmitter<T> & PreferenceRetrieval<T>;

export function createPreferenceProxy<T>(preferences: PreferenceService, schema: PreferenceSchema): PreferenceProxy<T> {
const toDispose = new DisposableCollection();
const onPreferenceChangedEmitter = new Emitter<PreferenceChange>();
const onPreferenceChangedEmitter = new Emitter<PreferenceChangeEvent<T>>();
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<any>['get'] = (arg, defaultValue, resourceUri) => {
const preferenceName = typeof arg === 'object' && arg.overrideIdentifier ?
preferences.overridePreferenceName(<OverridePreferenceName>arg) :
<string>arg;
return preferences.get(preferenceName, defaultValue, resourceUri);
};
return new Proxy({}, {
get: (_, property: string) => {
if (schema.properties[property]) {
Expand All @@ -66,7 +88,7 @@ export function createPreferenceProxy<T>(preferences: PreferenceService, schema:
return preferences.ready;
}
if (property === 'get') {
return preferences.get.bind(preferences);
return getValue;
}
throw new Error(`unexpected property: ${property}`);
},
Expand Down
Loading

0 comments on commit 508e33c

Please sign in to comment.