UNPKG

12.3 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2022 Ericsson and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { inject, injectable, postConstruct } from 'inversify';
18import { PreferenceSchema } from '../../common/preferences/preference-schema';
19import { Disposable, DisposableCollection, Emitter, Event, isObject, MaybePromise } from '../../common';
20import { PreferenceChangeEvent, PreferenceEventEmitter, PreferenceProxy, PreferenceProxyOptions, PreferenceRetrieval } from './preference-proxy';
21import { PreferenceChange, PreferenceChangeImpl, PreferenceChanges, PreferenceScope, PreferenceService } from './preference-service';
22import { JSONValue } from '@phosphor/coreutils';
23import { PreferenceProviderDataChange } from './preference-provider';
24import { OverridePreferenceName } from './preference-language-override-service';
25
26export const PreferenceProxySchema = Symbol('PreferenceProxySchema');
27export interface PreferenceProxyFactory {
28 <T>(schema: MaybePromise<PreferenceSchema>, options?: PreferenceProxyOptions): PreferenceProxy<T>;
29}
30export const PreferenceProxyFactory = Symbol('PreferenceProxyFactory');
31
32export class PreferenceProxyChange extends PreferenceChangeImpl {
33 constructor(change: PreferenceProviderDataChange, protected readonly overrideIdentifier?: string) {
34 super(change);
35 }
36
37 override affects(resourceUri?: string, overrideIdentifier?: string): boolean {
38 if (overrideIdentifier !== this.overrideIdentifier) {
39 return false;
40 }
41 return super.affects(resourceUri);
42 }
43}
44
45@injectable()
46export class InjectablePreferenceProxy<T extends Record<string, JSONValue>> implements
47 ProxyHandler<T>, ProxyHandler<Disposable>, ProxyHandler<PreferenceEventEmitter<T>>, ProxyHandler<PreferenceRetrieval<T>> {
48
49 @inject(PreferenceProxyOptions) protected readonly options: PreferenceProxyOptions;
50 @inject(PreferenceService) protected readonly preferences: PreferenceService;
51 @inject(PreferenceProxySchema) protected readonly promisedSchema: PreferenceSchema | Promise<PreferenceSchema>;
52 @inject(PreferenceProxyFactory) protected readonly factory: PreferenceProxyFactory;
53 protected toDispose = new DisposableCollection();
54 protected _onPreferenceChangedEmitter: Emitter<PreferenceChangeEvent<T>> | undefined;
55 protected schema: PreferenceSchema | undefined;
56
57 protected get prefix(): string {
58 return this.options.prefix ?? '';
59 }
60
61 protected get style(): Required<PreferenceProxyOptions>['style'] {
62 return this.options.style ?? 'flat';
63 }
64
65 protected get resourceUri(): PreferenceProxyOptions['resourceUri'] {
66 return this.options.resourceUri;
67 }
68
69 protected get overrideIdentifier(): PreferenceProxyOptions['overrideIdentifier'] {
70 return this.options.overrideIdentifier;
71 }
72
73 protected get isDeep(): boolean {
74 const { style } = this;
75 return style === 'deep' || style === 'both';
76 }
77
78 protected get isFlat(): boolean {
79 const { style } = this;
80 return style === 'flat' || style === 'both';
81 }
82
83 protected get onPreferenceChangedEmitter(): Emitter<PreferenceChangeEvent<T>> {
84 if (!this._onPreferenceChangedEmitter) {
85 this._onPreferenceChangedEmitter = new Emitter();
86 this.subscribeToChangeEvents();
87 this.toDispose.push(this._onPreferenceChangedEmitter);
88 }
89 return this._onPreferenceChangedEmitter;
90 }
91
92 get onPreferenceChanged(): Event<PreferenceChangeEvent<T>> {
93 return this.onPreferenceChangedEmitter.event;
94 }
95
96 @postConstruct()
97 protected init(): void {
98 if (this.promisedSchema instanceof Promise) {
99 this.promisedSchema.then(schema => this.schema = schema);
100 } else {
101 this.schema = this.promisedSchema;
102 }
103 }
104
105 get(target: unknown, property: string, receiver: unknown): unknown {
106 if (typeof property !== 'string') {
107 throw new Error(`Unexpected property: ${String(property)}`);
108 }
109 const preferenceName = this.prefix + property;
110 if (this.schema && (this.isFlat || !property.includes('.')) && this.schema.properties[preferenceName]) {
111 const { overrideIdentifier } = this;
112 const toGet = overrideIdentifier ? this.preferences.overridePreferenceName({ overrideIdentifier, preferenceName }) : preferenceName;
113 return this.getValue(toGet as keyof T & string, undefined!);
114 }
115 switch (property) {
116 case 'onPreferenceChanged':
117 return this.onPreferenceChanged;
118 case 'dispose':
119 return this.dispose.bind(this);
120 case 'ready':
121 return Promise.all([this.preferences.ready, this.promisedSchema]).then(() => undefined);
122 case 'get':
123 return this.getValue.bind(this);
124 case 'toJSON':
125 return this.toJSON.bind(this);
126 case 'ownKeys':
127 return this.ownKeys.bind(this);
128 }
129 if (this.schema && this.isDeep) {
130 const prefix = `${preferenceName}.`;
131 if (Object.keys(this.schema.properties).some(key => key.startsWith(prefix))) {
132 const { style, resourceUri, overrideIdentifier } = this;
133 return this.factory(this.schema, { prefix, resourceUri, style, overrideIdentifier });
134 }
135 let value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
136 let parentSegment = preferenceName;
137 const segments = [];
138 do {
139 const index = parentSegment.lastIndexOf('.');
140 segments.push(parentSegment.substring(index + 1));
141 parentSegment = parentSegment.substring(0, index);
142 if (parentSegment in this.schema.properties) {
143 value = this.get(target, parentSegment, receiver);
144 }
145 } while (parentSegment && value === undefined);
146
147 let segment;
148 while (isObject(value) && (segment = segments.pop())) {
149 value = value[segment];
150 }
151 return segments.length ? undefined : value;
152 }
153 }
154
155 set(target: unknown, property: string, value: unknown, receiver: unknown): boolean {
156 if (typeof property !== 'string') {
157 throw new Error(`Unexpected property: ${String(property)}`);
158 }
159 const { style, schema, prefix, resourceUri, overrideIdentifier } = this;
160 if (style === 'deep' && property.indexOf('.') !== -1) {
161 return false;
162 }
163 if (schema) {
164 const fullProperty = prefix ? prefix + property : property;
165 if (schema.properties[fullProperty]) {
166 this.preferences.set(fullProperty, value, PreferenceScope.Default);
167 return true;
168 }
169 const newPrefix = fullProperty + '.';
170 for (const p of Object.keys(schema.properties)) {
171 if (p.startsWith(newPrefix)) {
172 const subProxy = this.factory<T>(schema, {
173 prefix: newPrefix,
174 resourceUri,
175 overrideIdentifier,
176 style
177 }) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
178 const valueAsContainer = value as T;
179 for (const k of Object.keys(valueAsContainer)) {
180 subProxy[k as keyof T] = valueAsContainer[k as keyof T];
181 }
182 }
183 }
184 }
185 return false;
186 }
187
188 ownKeys(): string[] {
189 const properties = [];
190 if (this.schema) {
191 const { isDeep, isFlat, prefix } = this;
192 for (const property of Object.keys(this.schema.properties)) {
193 if (property.startsWith(prefix)) {
194 const idx = property.indexOf('.', prefix.length);
195 if (idx !== -1 && isDeep) {
196 const pre = property.substring(prefix.length, idx);
197 if (properties.indexOf(pre) === -1) {
198 properties.push(pre);
199 }
200 }
201 const prop = property.substring(prefix.length);
202 if (isFlat || prop.indexOf('.') === -1) {
203 properties.push(prop);
204 }
205 }
206 }
207 }
208 return properties;
209 }
210
211 getOwnPropertyDescriptor(target: unknown, property: string): PropertyDescriptor {
212 if (this.ownKeys().includes(property)) {
213 return {
214 enumerable: true,
215 configurable: true
216 };
217 }
218 return {};
219 }
220
221 deleteProperty(): never {
222 throw new Error('Unsupported operation');
223 }
224
225 defineProperty(): never {
226 throw new Error('Unsupported operation');
227 }
228
229 toJSON(): JSONValue {
230 const result: JSONValue = {};
231 for (const key of this.ownKeys()) {
232 result[key] = this.get(undefined, key, undefined) as JSONValue;
233 }
234 return result;
235 };
236
237 protected subscribeToChangeEvents(): void {
238 this.toDispose.push(this.preferences.onPreferencesChanged(changes => this.handlePreferenceChanges(changes)));
239 }
240
241 protected handlePreferenceChanges(changes: PreferenceChanges): void {
242 if (this.schema) {
243 for (const change of Object.values(changes)) {
244 const overrideInfo = this.preferences.overriddenPreferenceName(change.preferenceName);
245 if (this.isRelevantChange(change, overrideInfo)) {
246 this.fireChangeEvent(this.buildNewChangeEvent(change, overrideInfo));
247 }
248 }
249 }
250 }
251
252 protected isRelevantChange(change: PreferenceChange, overrideInfo?: OverridePreferenceName): boolean {
253 const preferenceName = overrideInfo?.preferenceName ?? change.preferenceName;
254 return preferenceName.startsWith(this.prefix)
255 && (!this.overrideIdentifier || overrideInfo?.overrideIdentifier === this.overrideIdentifier)
256 && Boolean(this.schema?.properties[preferenceName]);
257 }
258
259 protected fireChangeEvent(change: PreferenceChangeEvent<T>): void {
260 this.onPreferenceChangedEmitter.fire(change);
261 }
262
263 protected buildNewChangeEvent(change: PreferenceProviderDataChange, overrideInfo?: OverridePreferenceName): PreferenceChangeEvent<T> {
264 const preferenceName = (overrideInfo?.preferenceName ?? change.preferenceName) as keyof T & string;
265 const { newValue, oldValue, scope, domain } = change;
266 // eslint-disable-next-line @typescript-eslint/no-explicit-any
267 return new PreferenceProxyChange({ newValue, oldValue, preferenceName, scope, domain }, overrideInfo?.overrideIdentifier) as any;
268 }
269
270 protected getValue<K extends keyof T & string>(
271 preferenceIdentifier: K | OverridePreferenceName & { preferenceName: K }, defaultValue: T[K], resourceUri = this.resourceUri
272 ): T[K] {
273 const preferenceName = OverridePreferenceName.is(preferenceIdentifier) ? this.preferences.overridePreferenceName(preferenceIdentifier) : preferenceIdentifier as string;
274 return this.preferences.get(preferenceName, defaultValue, resourceUri);
275 }
276
277 dispose(): void {
278 if (this.options.isDisposable) {
279 this.toDispose.dispose();
280 }
281 }
282}