1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { inject, injectable, postConstruct } from 'inversify';
|
18 | import { PreferenceSchema } from '../../common/preferences/preference-schema';
|
19 | import { Disposable, DisposableCollection, Emitter, Event, isObject, MaybePromise } from '../../common';
|
20 | import { PreferenceChangeEvent, PreferenceEventEmitter, PreferenceProxy, PreferenceProxyOptions, PreferenceRetrieval } from './preference-proxy';
|
21 | import { PreferenceChange, PreferenceChangeImpl, PreferenceChanges, PreferenceScope, PreferenceService } from './preference-service';
|
22 | import { JSONValue } from '@phosphor/coreutils';
|
23 | import { PreferenceProviderDataChange } from './preference-provider';
|
24 | import { OverridePreferenceName } from './preference-language-override-service';
|
25 |
|
26 | export const PreferenceProxySchema = Symbol('PreferenceProxySchema');
|
27 | export interface PreferenceProxyFactory {
|
28 | <T>(schema: MaybePromise<PreferenceSchema>, options?: PreferenceProxyOptions): PreferenceProxy<T>;
|
29 | }
|
30 | export const PreferenceProxyFactory = Symbol('PreferenceProxyFactory');
|
31 |
|
32 | export 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()
|
46 | export 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;
|
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;
|
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 |
|
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 | }
|