Skip to content

Commit

Permalink
preferences for multi-root workspace
Browse files Browse the repository at this point in the history
With changes in 543b119, URIs of root folders in a multi-root workspace are stored in a file with the extension of "theia-workspace", while the workspace preferences are in the ".theia/settings.json" under the first root.

In this pull request, workspace preferences are stored
- in the workspace file under the "settings" property, if a workspace file exists, or
- in the ".theia/settings.json" if the workspace data is not saved in a workspace file.

Also, this change supports
- having 4 levels of preferences (from highest priority to the lowest): FolderPreference, WorkspacePreference, UserPreference, and DefaultPreference, with
- an updated preference editor that supports viewing & editing the FolderPreferences, WorkspacePreferences, and UserPreferneces.

- Internally, vsCode uses an enum to define the preference scope. External extensions, however, make contributions to preference schemas by having schemas defined in the package.json, where the preference scope is a string enum of "window", "application", and "resource". This change updates the PreferenceSchema interface defined in theia, to allow contribution points to define scopes with strings.

- Added affects() function to PreferenceChange,  to help client decide whether or not the preference change matters.

Signed-off-by: elaihau <liang.huang@ericsson.com>
  • Loading branch information
elaihau authored and elaihau committed Feb 4, 2019
1 parent f54fc5c commit a2fa904
Show file tree
Hide file tree
Showing 35 changed files with 1,667 additions and 418 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Expand Up @@ -27,6 +27,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
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
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)) {
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()
});
}
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
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[] {
return [];
}

protected getScope() {
return PreferenceScope.Default;
}
}
21 changes: 16 additions & 5 deletions packages/core/src/browser/preferences/preference-proxy.ts
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

0 comments on commit a2fa904

Please sign in to comment.