Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

preferences for multi-root workspace #3247

Merged
merged 1 commit into from
Feb 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ Breaking changes:
- `debug.thread.next` renamed to `workbench.action.debug.stepOver`
- `debug.stop` renamed to `workbench.action.debug.stop`
- `debug.editor.showHover` renamed to `editor.debug.action.showDebugHover`
- multi-root workspace support for preferences [#3247](https://github.com/theia-ide/theia/pull/3247)
- `PreferenceProvider`
- is changed from a regular class to an abstract class.
- the `fireOnDidPreferencesChanged` function is deprecated. `emitPreferencesChangedEvent` function should be used instead. `fireOnDidPreferencesChanged` will be removed with the next major release.
- `PreferenceServiceImpl`
- `preferences` is deprecated. `getPreferences` function should be used instead. `preferences` will be removed with the next major release.
- having `properties` property defined in the `PreferenceSchema` object is now mandatory.
- `PreferenceProperty` is renamed to `PreferenceDataProperty`.
- `PreferenceSchemaProvider`
- the type of `combinedSchema` property is changed from `PreferenceSchema` to `PreferenceDataSchema`.
- the return type of `getCombinedSchema` function is changed from `PreferenceSchema` to `PreferenceDataSchema`.
- `affects` function is added to `PreferenceChangeEvent` and `PreferenceChange` interface.


## v0.3.19
- [core] added `hostname` alias
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo

bind(PreferenceProvider).toSelf().inSingletonScope().whenTargetNamed(PreferenceScope.User);
bind(PreferenceProvider).toSelf().inSingletonScope().whenTargetNamed(PreferenceScope.Workspace);
bind(PreferenceProvider).toSelf().inSingletonScope().whenTargetNamed(PreferenceScope.Folder);
bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => {
if (scope === PreferenceScope.Default) {
return ctx.container.get(PreferenceSchemaProvider);
Expand Down
91 changes: 72 additions & 19 deletions packages/core/src/browser/preferences/preference-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
********************************************************************************/

import * as Ajv from 'ajv';
import { inject, injectable, named, interfaces, postConstruct } from 'inversify';
import { inject, injectable, interfaces, named, postConstruct } from 'inversify';
import { ContributionProvider, bindContributionProvider } from '../../common';
import { PreferenceProvider } from './preference-provider';
import { PreferenceScope } from './preference-service';
import { PreferenceProvider, PreferenceProviderPriority, PreferenceProviderDataChange } from './preference-provider';

// tslint:disable:no-any

Expand All @@ -26,30 +27,62 @@ export interface PreferenceContribution {
readonly schema: PreferenceSchema;
}

export const PreferenceSchema = Symbol('PreferenceSchema');

export interface PreferenceSchema {
[name: string]: Object,
[name: string]: any,
scope?: 'application' | 'window' | 'resource' | PreferenceScope,
properties: {
[name: string]: PreferenceSchemaProperty
}
}
export namespace PreferenceSchema {
export function getDefaultScope(schema: PreferenceSchema): PreferenceScope {
let defaultScope: PreferenceScope = PreferenceScope.Workspace;
if (!PreferenceScope.is(schema.scope)) {
defaultScope = PreferenceScope.fromString(<string>schema.scope) || PreferenceScope.Workspace;
} else {
defaultScope = schema.scope;
}
return defaultScope;
}
}

export interface PreferenceDataSchema {
[name: string]: any,
scope?: PreferenceScope,
properties: {
[name: string]: PreferenceProperty
[name: string]: PreferenceDataProperty
}
}

export interface PreferenceItem {
type?: JsonType | JsonType[];
minimum?: number;
// tslint:disable-next-line:no-any
default?: any;
enum?: string[];
items?: PreferenceItem;
properties?: { [name: string]: PreferenceItem };
additionalProperties?: object;
// tslint:disable-next-line:no-any
[name: string]: any;
}

export interface PreferenceProperty extends PreferenceItem {
export interface PreferenceSchemaProperty extends PreferenceItem {
description: string;
scope?: 'application' | 'window' | 'resource' | PreferenceScope;
}

export interface PreferenceDataProperty extends PreferenceItem {
description: string;
scope?: PreferenceScope;
}
export namespace PreferenceDataProperty {
export function fromPreferenceSchemaProperty(schemaProps: PreferenceSchemaProperty, defaultScope: PreferenceScope = PreferenceScope.Workspace): PreferenceDataProperty {
if (!schemaProps.scope) {
schemaProps.scope = defaultScope;
} else if (typeof schemaProps.scope === 'string') {
return Object.assign(schemaProps, { scope: PreferenceScope.fromString(schemaProps.scope) || defaultScope });
}
return <PreferenceDataProperty>schemaProps;
}
}

export type JsonType = 'string' | 'array' | 'number' | 'integer' | 'object' | 'boolean' | 'null';
Expand All @@ -62,8 +95,8 @@ export function bindPreferenceSchemaProvider(bind: interfaces.Bind): void {
@injectable()
export class PreferenceSchemaProvider extends PreferenceProvider {

protected readonly combinedSchema: PreferenceSchema = { properties: {} };
protected readonly preferences: { [name: string]: any } = {};
protected readonly combinedSchema: PreferenceDataSchema = { properties: {} };
protected validateFunction: Ajv.ValidateFunction;

@inject(ContributionProvider) @named(PreferenceContribution)
Expand All @@ -74,21 +107,23 @@ export class PreferenceSchemaProvider extends PreferenceProvider {
this.preferenceContributions.getContributions().forEach(contrib => {
this.doSetSchema(contrib.schema);
});
this.combinedSchema.additionalProperties = false;
this.updateValidate();
this._ready.resolve();
}

protected doSetSchema(schema: PreferenceSchema): void {
const defaultScope = PreferenceSchema.getDefaultScope(schema);
const props: string[] = [];
for (const property in schema.properties) {
for (const property of Object.keys(schema.properties)) {
elaihau marked this conversation as resolved.
Show resolved Hide resolved
const schemaProps = schema.properties[property];
if (this.combinedSchema.properties[property]) {
console.error('Preference name collision detected in the schema for property: ' + property);
} else {
this.combinedSchema.properties[property] = schema.properties[property];
this.combinedSchema.properties[property] = PreferenceDataProperty.fromPreferenceSchemaProperty(schemaProps, defaultScope);
props.push(property);
}
}
// tslint:disable-next-line:forin
for (const property of props) {
this.preferences[property] = this.combinedSchema.properties[property].default;
}
Expand All @@ -102,22 +137,40 @@ export class PreferenceSchemaProvider extends PreferenceProvider {
return this.validateFunction({ [name]: value }) as boolean;
}

getCombinedSchema(): PreferenceSchema {
getCombinedSchema(): PreferenceDataSchema {
return this.combinedSchema;
}

getPreferences(): { [name: string]: any } {
return this.preferences;
}

setSchema(schema: PreferenceSchema): void {
this.doSetSchema(schema);
this.updateValidate();
this.fireOnDidPreferencesChanged();
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()
});
}
elaihau marked this conversation as resolved.
Show resolved Hide resolved
this.emitPreferencesChangedEvent(changes);
}

getPreferences(): { [name: string]: any } {
return this.preferences;
}

async setPreference(): Promise<void> {
throw new Error('Unsupported');
}

canProvide(preferenceName: string, resourceUri?: string): { priority: number, provider: 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;
}
return false;
}
}
75 changes: 67 additions & 8 deletions packages/core/src/browser/preferences/preference-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,33 @@
import { injectable } from 'inversify';
import { Disposable, DisposableCollection, Emitter, Event } from '../../common';
import { Deferred } from '../../common/promise-util';
import { PreferenceScope } from './preference-service';

export namespace PreferenceProviderPriority {
export const NA = -1;
export const Default = 0;
export const User = 1;
export const Workspace = 2;
export const Folder = 3;
}

export interface PreferenceProviderDataChange {
readonly preferenceName: string;
readonly newValue?: any;
readonly oldValue?: any;
readonly scope: PreferenceScope;
readonly domain: string[];
}

export interface PreferenceProviderDataChanges {
[preferenceName: string]: PreferenceProviderDataChange
}

@injectable()
export class PreferenceProvider implements Disposable {
protected readonly onDidPreferencesChangedEmitter = new Emitter<void>();
readonly onDidPreferencesChanged: Event<void> = this.onDidPreferencesChangedEmitter.event;
export abstract class PreferenceProvider implements Disposable {

protected readonly onDidPreferencesChangedEmitter = new Emitter<PreferenceProviderDataChanges | undefined>();
readonly onDidPreferencesChanged: Event<PreferenceProviderDataChanges | undefined> = this.onDidPreferencesChangedEmitter.event;

protected readonly toDispose = new DisposableCollection();

Expand All @@ -41,20 +63,57 @@ export class PreferenceProvider implements Disposable {
this.toDispose.dispose();
}

/**
* Informs the listeners that one or more preferences of this provider are changed.
* The listeners are able to find what was changed from the emitted event.
*/
protected emitPreferencesChangedEvent(changes: PreferenceProviderDataChanges | PreferenceProviderDataChange[]): void {
if (Array.isArray(changes)) {
const prefChanges: PreferenceProviderDataChanges = {};
for (const change of changes) {
prefChanges[change.preferenceName] = change;
}
this.onDidPreferencesChangedEmitter.fire(prefChanges);
} else {
this.onDidPreferencesChangedEmitter.fire(changes);
}
}

/**
* Informs the listeners that one or more preferences of this provider are changed.
* @deprecated Use emitPreferencesChangedEvent instead.
*/
protected fireOnDidPreferencesChanged(): void {
this.onDidPreferencesChangedEmitter.fire(undefined);
}

getPreferences(): { [p: string]: any } {
return [];
get<T>(preferenceName: string, resourceUri?: string): T | undefined {
const value = this.getPreferences(resourceUri)[preferenceName];
if (value !== undefined && value !== null) {
return value;
}
}

setPreference(key: string, value: any): Promise<void> {
return Promise.resolve();
}
// tslint:disable-next-line:no-any
abstract getPreferences(resourceUri?: string): { [p: string]: any };

// tslint:disable-next-line:no-any
abstract setPreference(key: string, value: any, resourceUri?: string): Promise<void>;

/** See `_ready`. */
get ready() {
return this._ready.promise;
}

canProvide(preferenceName: string, resourceUri?: string): { priority: number, provider: PreferenceProvider } {
return { priority: PreferenceProviderPriority.NA, provider: this };
}

getDomain(): string[] {
elaihau marked this conversation as resolved.
Show resolved Hide resolved
return [];
}

protected getScope() {
return PreferenceScope.Default;
}
}
21 changes: 16 additions & 5 deletions packages/core/src/browser/preferences/preference-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ import { PreferenceService, PreferenceChange } from './preference-service';
import { PreferenceSchema } from './preference-contribution';

export interface PreferenceChangeEvent<T> {
readonly preferenceName: keyof T
readonly newValue?: T[keyof T]
readonly oldValue?: T[keyof T]
readonly preferenceName: keyof T;
readonly newValue?: T[keyof T];
readonly oldValue?: T[keyof T];
affects(resourceUri?: string): boolean;
}

export interface PreferenceEventEmitter<T> {
readonly onPreferenceChanged: Event<PreferenceChangeEvent<T>>;
readonly ready: Promise<void>;
}

export type PreferenceProxy<T> = Readonly<T> & Disposable & PreferenceEventEmitter<T>;
export interface PreferenceRetrieval<T> {
get<K extends keyof T>(preferenceName: K, 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>();
Expand All @@ -40,6 +47,7 @@ export function createPreferenceProxy<T>(preferences: PreferenceService, schema:
onPreferenceChangedEmitter.fire(e);
}
}));

const unsupportedOperation = (_: any, __: string) => {
throw new Error('Unsupported operation');
};
Expand All @@ -57,7 +65,10 @@ export function createPreferenceProxy<T>(preferences: PreferenceService, schema:
if (property === 'ready') {
return preferences.ready;
}
throw new Error('unexpected property: ' + property);
if (property === 'get') {
return preferences.get.bind(preferences);
}
throw new Error(`unexpected property: ${property}`);
},
ownKeys: () => Object.keys(schema.properties),
getOwnPropertyDescriptor: (_, property: string) => {
Expand Down
Loading