@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
368 lines (350 loc) • 13.9 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Disposable, Event, isObject, MaybePromise } from '../../common';
import { PreferenceService } from './preference-service';
import { PreferenceSchema } from './preference-contribution';
import { PreferenceScope } from './preference-scope';
import { OverridePreferenceName } from './preference-language-override-service';
/**
* It is worth explaining the type for `PreferenceChangeEvent`:
*
* // Given T:
* type T = { a: string, b: number }
*
* // We construct a new type such as:
* type U = {
* a: {
* preferenceName: 'a'
* newValue: string
* oldValue?: string
* }
* b: {
* preferenceName: 'b'
* newValue: number
* oldValue?: number
* }
* }
*
* // Then we get the union of all values of U by selecting by `keyof T`:
* type V = U[keyof T]
*
* // Implementation:
* type PreferenceChangeEvent<T> = {
* // Create a mapping where each key is a key from T,
* // -? normalizes optional typings to avoid getting
* // `undefined` as part of the final union:
* [K in keyof T]-?: {
* // In this object, K will take the value of each
* // independent key from T:
* preferenceName: K
* newValue: T[K]
* oldValue?: T[K]
* // Finally we create the union by doing so:
* }[keyof T]
* }
*/
/**
* Union of all possible key/value pairs for a type `T`
*/
export type PreferenceChangeEvent<T> = {
affects(resourceUri?: string, overrideIdentifier?: string): boolean;
} & {
[K in keyof T]-?: {
readonly preferenceName: K;
readonly newValue: T[K];
/**
* Undefined if the preference is set for the first time.
*/
// TODO: Use the default value instead of undefined?
readonly oldValue?: T[K];
}
}[keyof T];
export interface PreferenceEventEmitter<T> {
readonly onPreferenceChanged: Event<PreferenceChangeEvent<T>>;
readonly ready: Promise<void>;
}
/**
* Generic interface to declare a typesafe get function based on the given
* configuration type.
*
* ### Illustration
*
* ```ts
* interface PreferenceConfiguration {
* 'myext.enabled': boolean,
* }
* const enabled : boolean = prefs.get('myext.enabled'); // valid
* const debug : string = prefs.get('myext.enabled'); // invalid
* prefs.get('foobar'); // invalid
* ```
*/
export interface PreferenceRetrieval<T> {
get<K extends keyof T>(preferenceName: K | {
preferenceName: K,
overrideIdentifier?: string
}, defaultValue?: T[K], resourceUri?: string): T[K];
}
/**
* Typesafe schema-based preferences utility based on the {@link PreferenceService}.
* Can be used to get preferences as well as listen to preference changes.
*
* See {@link createPreferenceProxy} on how to instantiate preference proxies.
*
* ### Example usage
*
* ```ts
* preferences.onPreferenceChanged(({ preferenceName, newValue }) => { ... });
* const enabled = preferences['myext.enabled'];
* ```
*/
export type PreferenceProxy<T> = Readonly<T> & Disposable & PreferenceEventEmitter<T> & PreferenceRetrieval<T>;
export const PreferenceProxyOptions = Symbol('PreferenceProxyOptions');
/**
* Proxy configuration parameters.
*/
export interface PreferenceProxyOptions {
/**
* Prefix which is transparently added to all preference identifiers.
*/
prefix?: string;
/**
* The default resourceUri to use if none was specified when calling "set" or "get".
*/
resourceUri?: string;
/**
* The overrideIdentifier to use with the underlying preferenceService.
* Useful to potentially override existing values while keeping both values in store.
*
* For example to store different editor settings, e.g. "[markdown].editor.autoIndent",
* "[json].editor.autoIndent" and "editor.autoIndent"
*/
overrideIdentifier?: string;
/**
* Indicates whether '.' in schema properties shall be interpreted as regular names (flat),
* as declaring nested objects (deep) or both. Default is flat.
*
* When 'deep' or 'both' is given, nested preference proxies can be retrieved.
*/
style?: 'flat' | 'deep' | 'both';
/**
* Indicates whether the proxy should be disposable. Proxies that are shared between multiple callers should not be disposable.
*/
isDisposable?: boolean;
}
/**
* Creates a preference proxy for typesafe preference handling.
*
* @param preferences the underlying preference service to use for preference handling.
* @param promisedSchema the JSON Schema which describes which preferences are available including types and descriptions. Can be a promise.
* @param options configuration options.
*
* @returns the created preference proxy.
*
* ### Usage
*
* 1. Create JSON Schema specifying your preferences
* 2. Create Configuration type based on the JSON Schema
* 3. Bind the return value of `createPreferenceProxy` to make your preferences available wherever needed.
*
* See {@link CorePreferences} for an example.
*
* Note that if `schema` is a Promise, most actions will be no-ops until the promise is resolved.
*
* @deprecated @since 1.23.0 use `PreferenceProxyFactory` instead.
*/
export function createPreferenceProxy<T>(preferences: PreferenceService, promisedSchema: MaybePromise<PreferenceSchema>, options?: PreferenceProxyOptions): PreferenceProxy<T> {
const opts = options || {};
const prefix = opts.prefix || '';
const style = opts.style || 'flat';
const isDeep = style === 'deep' || style === 'both';
const isFlat = style === 'both' || style === 'flat';
let schema: PreferenceSchema | undefined;
if (PreferenceSchema.is(promisedSchema)) {
schema = promisedSchema;
} else {
promisedSchema.then(s => schema = s);
}
const onPreferenceChanged = (listener: (e: PreferenceChangeEvent<T>) => any, thisArgs?: any, disposables?: Disposable[]) => preferences.onPreferencesChanged(changes => {
if (schema) {
for (const key of Object.keys(changes)) {
const e = changes[key];
const overridden = preferences.overriddenPreferenceName(e.preferenceName);
const preferenceName: any = overridden ? overridden.preferenceName : e.preferenceName;
if (preferenceName.startsWith(prefix) && (!opts.overrideIdentifier || overridden?.overrideIdentifier === opts.overrideIdentifier)) {
if (schema.properties[preferenceName]) {
const { newValue, oldValue } = e;
listener({
newValue, oldValue, preferenceName,
affects: (resourceUri, overrideIdentifier) => {
if (overrideIdentifier !== overridden?.overrideIdentifier) {
return false;
}
return e.affects(resourceUri);
}
});
}
}
}
}
}, thisArgs, disposables);
const unsupportedOperation = (_: any, __: string) => {
throw new Error('Unsupported operation');
};
const getValue: PreferenceRetrieval<any>['get'] = (arg, defaultValue, resourceUri) => {
const preferenceName = OverridePreferenceName.is(arg) ?
preferences.overridePreferenceName(arg) :
<string>arg;
return preferences.get(preferenceName, defaultValue, resourceUri || opts.resourceUri);
};
const ownKeys: () => string[] = () => {
const properties = [];
if (schema) {
for (const p of Object.keys(schema.properties)) {
if (p.startsWith(prefix)) {
const idx = p.indexOf('.', prefix.length);
if (idx !== -1 && isDeep) {
const pre = p.substring(prefix.length, idx);
if (properties.indexOf(pre) === -1) {
properties.push(pre);
}
}
const prop = p.substring(prefix.length);
if (isFlat || prop.indexOf('.') === -1) {
properties.push(prop);
}
}
}
}
return properties;
};
const set: (target: any, prop: string, value: any, receiver: any) => boolean = (_, property: string | symbol | number, value: any) => {
if (typeof property !== 'string') {
throw new Error(`unexpected property: ${String(property)}`);
}
if (style === 'deep' && property.indexOf('.') !== -1) {
return false;
}
if (schema) {
const fullProperty = prefix ? prefix + property : property;
if (schema.properties[fullProperty]) {
preferences.set(fullProperty, value, PreferenceScope.Default);
return true;
}
const newPrefix = fullProperty + '.';
for (const p of Object.keys(schema.properties)) {
if (p.startsWith(newPrefix)) {
const subProxy: { [k: string]: any } = createPreferenceProxy(preferences, schema, {
prefix: newPrefix,
resourceUri: opts.resourceUri,
overrideIdentifier: opts.overrideIdentifier,
style
});
for (const k of Object.keys(value)) {
subProxy[k] = value[k];
}
}
}
}
return false;
};
const get: (target: any, prop: string) => any = (_, property: string | symbol | number) => {
if (typeof property !== 'string') {
throw new Error(`unexpected property: ${String(property)}`);
}
const fullProperty = prefix ? prefix + property : property;
if (schema) {
if (isFlat || property.indexOf('.') === -1) {
if (schema.properties[fullProperty]) {
let value;
if (opts.overrideIdentifier) {
value = preferences.get(preferences.overridePreferenceName({
overrideIdentifier: opts.overrideIdentifier,
preferenceName: fullProperty
}), undefined, opts.resourceUri);
}
if (value === undefined) {
value = preferences.get(fullProperty, undefined, opts.resourceUri);
}
return value;
}
}
}
if (property === 'onPreferenceChanged') {
return onPreferenceChanged;
}
if (property === 'dispose') {
return () => { /* do nothing */ };
}
if (property === 'ready') {
return preferences.ready;
}
if (property === 'get') {
return getValue;
}
if (property === 'toJSON') {
return toJSON();
}
if (schema && isDeep) {
const newPrefix = fullProperty + '.';
for (const p of Object.keys(schema.properties)) {
if (p.startsWith(newPrefix)) {
return createPreferenceProxy(preferences, schema, { prefix: newPrefix, resourceUri: opts.resourceUri, overrideIdentifier: opts.overrideIdentifier, style });
}
}
let value;
let parentSegment = fullProperty;
const segments = [];
do {
const index = parentSegment.lastIndexOf('.');
segments.push(parentSegment.substring(index + 1));
parentSegment = parentSegment.substring(0, index);
if (parentSegment in schema.properties) {
value = get(_, parentSegment);
}
} while (parentSegment && value === undefined);
let segment;
while (isObject(value) && (segment = segments.pop())) {
value = value[segment];
}
return segments.length ? undefined : value;
}
return undefined;
};
const toJSON = () => {
const result: any = {};
for (const k of ownKeys()) {
result[k] = get(undefined, k);
}
return result;
};
return new Proxy({}, {
get,
ownKeys,
getOwnPropertyDescriptor: (_, property: string) => {
if (ownKeys().indexOf(property) !== -1) {
return {
enumerable: true,
configurable: true
};
}
return {};
},
set,
deleteProperty: unsupportedOperation,
defineProperty: unsupportedOperation
});
}