1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | import { injectable, inject, postConstruct } from 'inversify';
|
20 | import { Event, Emitter, DisposableCollection, Disposable, deepFreeze, unreachable } from '../../common';
|
21 | import { Deferred } from '../../common/promise-util';
|
22 | import { PreferenceProvider, PreferenceProviderDataChange, PreferenceProviderDataChanges, PreferenceResolveResult } from './preference-provider';
|
23 | import { PreferenceSchemaProvider } from './preference-contribution';
|
24 | import URI from '../../common/uri';
|
25 | import { PreferenceScope } from './preference-scope';
|
26 | import { PreferenceConfigurations } from './preference-configurations';
|
27 | import { JSONExt, JSONValue } from '@phosphor/coreutils/lib/json';
|
28 | import { OverridePreferenceName, PreferenceLanguageOverrideService } from './preference-language-override-service';
|
29 |
|
30 | export { PreferenceScope };
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | export interface PreferenceChange extends PreferenceProviderDataChange {
|
37 | |
38 |
|
39 |
|
40 |
|
41 | affects(resourceUri?: string): boolean;
|
42 | }
|
43 |
|
44 | export class PreferenceChangeImpl implements PreferenceChange {
|
45 | protected readonly change: PreferenceProviderDataChange;
|
46 | constructor(change: PreferenceProviderDataChange) {
|
47 | this.change = deepFreeze(change);
|
48 | }
|
49 |
|
50 | get preferenceName(): string {
|
51 | return this.change.preferenceName;
|
52 | }
|
53 | get newValue(): string {
|
54 | return this.change.newValue;
|
55 | }
|
56 | get oldValue(): string {
|
57 | return this.change.oldValue;
|
58 | }
|
59 | get scope(): PreferenceScope {
|
60 | return this.change.scope;
|
61 | }
|
62 | get domain(): string[] | undefined {
|
63 | return this.change.domain;
|
64 | }
|
65 |
|
66 |
|
67 | affects(resourceUri?: string): boolean {
|
68 | const resourcePath = resourceUri && new URI(resourceUri).path;
|
69 | const domain = this.change.domain;
|
70 | return !resourcePath || !domain || domain.some(uri => new URI(uri).path.relativity(resourcePath) >= 0);
|
71 | }
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 | export interface PreferenceChanges {
|
77 | [preferenceName: string]: PreferenceChange
|
78 | }
|
79 |
|
80 | export const PreferenceService = Symbol('PreferenceService');
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | export interface PreferenceService extends Disposable {
|
90 | |
91 |
|
92 |
|
93 | readonly ready: Promise<void>;
|
94 | |
95 |
|
96 |
|
97 | readonly isReady: boolean;
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | get<T>(preferenceName: string): T | undefined;
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | get<T>(preferenceName: string, defaultValue: T): T;
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 | get<T>(preferenceName: string, defaultValue: T, resourceUri?: string): T;
|
127 | |
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | get<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined;
|
139 | |
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | set(preferenceName: string, value: any, scope?: PreferenceScope, resourceUri?: string): Promise<void>;
|
153 |
|
154 | |
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | updateValue(preferenceName: string, value: any, resourceUri?: string): Promise<void>
|
164 |
|
165 | |
166 |
|
167 |
|
168 | onPreferenceChanged: Event<PreferenceChange>;
|
169 | |
170 |
|
171 |
|
172 | onPreferencesChanged: Event<PreferenceChanges>;
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | inspect<T extends JSONValue>(preferenceName: string, resourceUri?: string, forceLanguageOverride?: boolean): PreferenceInspection<T> | undefined;
|
184 | |
185 |
|
186 |
|
187 |
|
188 |
|
189 | inspectInScope<T extends JSONValue>(preferenceName: string, scope: PreferenceScope, resourceUri?: string, forceLanguageOverride?: boolean): T | undefined
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | overridePreferenceName(options: OverridePreferenceName): string;
|
198 | |
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | overriddenPreferenceName(preferenceName: string): OverridePreferenceName | undefined;
|
212 | |
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | resolve<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): PreferenceResolveResult<T>;
|
225 | |
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 | getConfigUri(scope: PreferenceScope, resourceUri?: string, sectionName?: string): URI | undefined;
|
236 | }
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | export interface PreferenceInspection<T = JSONValue> {
|
242 | |
243 |
|
244 |
|
245 | preferenceName: string,
|
246 | |
247 |
|
248 |
|
249 | defaultValue: T | undefined,
|
250 | |
251 |
|
252 |
|
253 | globalValue: T | undefined,
|
254 | |
255 |
|
256 |
|
257 | workspaceValue: T | undefined,
|
258 | |
259 |
|
260 |
|
261 | workspaceFolderValue: T | undefined,
|
262 | |
263 |
|
264 |
|
265 | value: T | undefined;
|
266 | }
|
267 |
|
268 | export type PreferenceInspectionScope = keyof Omit<PreferenceInspection<unknown>, 'preferenceName'>;
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | export const PreferenceProviderProvider = Symbol('PreferenceProviderProvider');
|
275 | export type PreferenceProviderProvider = (scope: PreferenceScope, uri?: URI) => PreferenceProvider;
|
276 |
|
277 | @injectable()
|
278 | export class PreferenceServiceImpl implements PreferenceService {
|
279 |
|
280 | protected readonly onPreferenceChangedEmitter = new Emitter<PreferenceChange>();
|
281 | readonly onPreferenceChanged = this.onPreferenceChangedEmitter.event;
|
282 |
|
283 | protected readonly onPreferencesChangedEmitter = new Emitter<PreferenceChanges>();
|
284 | readonly onPreferencesChanged = this.onPreferencesChangedEmitter.event;
|
285 |
|
286 | protected readonly toDispose = new DisposableCollection(this.onPreferenceChangedEmitter, this.onPreferencesChangedEmitter);
|
287 |
|
288 | @inject(PreferenceSchemaProvider)
|
289 | protected readonly schema: PreferenceSchemaProvider;
|
290 |
|
291 | @inject(PreferenceProviderProvider)
|
292 | protected readonly providerProvider: PreferenceProviderProvider;
|
293 |
|
294 | @inject(PreferenceConfigurations)
|
295 | protected readonly configurations: PreferenceConfigurations;
|
296 |
|
297 | @inject(PreferenceLanguageOverrideService)
|
298 | protected readonly preferenceOverrideService: PreferenceLanguageOverrideService;
|
299 |
|
300 | protected readonly preferenceProviders = new Map<PreferenceScope, PreferenceProvider>();
|
301 |
|
302 | protected async initializeProviders(): Promise<void> {
|
303 | try {
|
304 | for (const scope of PreferenceScope.getScopes()) {
|
305 | const provider = this.providerProvider(scope);
|
306 | this.preferenceProviders.set(scope, provider);
|
307 | this.toDispose.push(provider.onDidPreferencesChanged(changes =>
|
308 | this.reconcilePreferences(changes)
|
309 | ));
|
310 | await provider.ready;
|
311 | }
|
312 | this._ready.resolve();
|
313 | this._isReady = true;
|
314 | } catch (e) {
|
315 | this._ready.reject(e);
|
316 | }
|
317 | }
|
318 |
|
319 | @postConstruct()
|
320 | protected init(): void {
|
321 | this.toDispose.push(Disposable.create(() => this._ready.reject(new Error('preference service is disposed'))));
|
322 | this.initializeProviders();
|
323 | }
|
324 |
|
325 | dispose(): void {
|
326 | this.toDispose.dispose();
|
327 | }
|
328 |
|
329 | protected readonly _ready = new Deferred<void>();
|
330 | get ready(): Promise<void> {
|
331 | return this._ready.promise;
|
332 | }
|
333 |
|
334 | protected _isReady = false;
|
335 | get isReady(): boolean {
|
336 | return this._isReady;
|
337 | }
|
338 |
|
339 | protected reconcilePreferences(changes: PreferenceProviderDataChanges): void {
|
340 | const changesToEmit: PreferenceChanges = {};
|
341 | const acceptChange = (change: PreferenceProviderDataChange) =>
|
342 | this.getAffectedPreferenceNames(change, preferenceName =>
|
343 | changesToEmit[preferenceName] = new PreferenceChangeImpl({ ...change, preferenceName })
|
344 | );
|
345 |
|
346 | for (const preferenceName of Object.keys(changes)) {
|
347 | let change = changes[preferenceName];
|
348 | if (change.newValue === undefined) {
|
349 | const overridden = this.overriddenPreferenceName(change.preferenceName);
|
350 | if (overridden) {
|
351 | change = {
|
352 | ...change, newValue: this.doGet(overridden.preferenceName)
|
353 | };
|
354 | }
|
355 | }
|
356 | if (this.schema.isValidInScope(preferenceName, PreferenceScope.Folder)) {
|
357 | acceptChange(change);
|
358 | continue;
|
359 | }
|
360 | for (const scope of PreferenceScope.getReversedScopes()) {
|
361 | if (this.schema.isValidInScope(preferenceName, scope)) {
|
362 | const provider = this.getProvider(scope);
|
363 | if (provider) {
|
364 | const value = provider.get(preferenceName);
|
365 | if (scope > change.scope && value !== undefined) {
|
366 |
|
367 | break;
|
368 | } else if (scope === change.scope && change.newValue !== undefined) {
|
369 |
|
370 | acceptChange(change);
|
371 | } else if (scope < change.scope && change.newValue === undefined && value !== undefined) {
|
372 |
|
373 | change = {
|
374 | ...change,
|
375 | newValue: value,
|
376 | scope
|
377 | };
|
378 | acceptChange(change);
|
379 | }
|
380 | }
|
381 | } else if (change.newValue === undefined && change.scope === PreferenceScope.Default) {
|
382 |
|
383 | acceptChange(change);
|
384 | break;
|
385 | }
|
386 | }
|
387 | }
|
388 |
|
389 |
|
390 | const changedPreferenceNames = Object.keys(changesToEmit);
|
391 | if (changedPreferenceNames.length > 0) {
|
392 | this.onPreferencesChangedEmitter.fire(changesToEmit);
|
393 | }
|
394 | changedPreferenceNames.forEach(preferenceName => this.onPreferenceChangedEmitter.fire(changesToEmit[preferenceName]));
|
395 | }
|
396 | protected getAffectedPreferenceNames(change: PreferenceProviderDataChange, accept: (affectedPreferenceName: string) => void): void {
|
397 | accept(change.preferenceName);
|
398 | for (const overridePreferenceName of this.schema.getOverridePreferenceNames(change.preferenceName)) {
|
399 | if (!this.doHas(overridePreferenceName)) {
|
400 | accept(overridePreferenceName);
|
401 | }
|
402 | }
|
403 | }
|
404 |
|
405 | protected getProvider(scope: PreferenceScope): PreferenceProvider | undefined {
|
406 | return this.preferenceProviders.get(scope);
|
407 | }
|
408 |
|
409 | has(preferenceName: string, resourceUri?: string): boolean {
|
410 | return this.get(preferenceName, undefined, resourceUri) !== undefined;
|
411 | }
|
412 |
|
413 | get<T>(preferenceName: string): T | undefined;
|
414 | get<T>(preferenceName: string, defaultValue: T): T;
|
415 | get<T>(preferenceName: string, defaultValue: T, resourceUri: string): T;
|
416 | get<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined;
|
417 | get<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined {
|
418 | return this.resolve<T>(preferenceName, defaultValue, resourceUri).value;
|
419 | }
|
420 |
|
421 | resolve<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): PreferenceResolveResult<T> {
|
422 | const { value, configUri } = this.doResolve(preferenceName, defaultValue, resourceUri);
|
423 | if (value === undefined) {
|
424 | const overridden = this.overriddenPreferenceName(preferenceName);
|
425 | if (overridden) {
|
426 | return this.doResolve(overridden.preferenceName, defaultValue, resourceUri);
|
427 | }
|
428 | }
|
429 | return { value, configUri };
|
430 | }
|
431 |
|
432 | async set(preferenceName: string, value: any, scope: PreferenceScope | undefined, resourceUri?: string): Promise<void> {
|
433 | const resolvedScope = scope ?? (!resourceUri ? PreferenceScope.Workspace : PreferenceScope.Folder);
|
434 | if (resolvedScope === PreferenceScope.Folder && !resourceUri) {
|
435 | throw new Error('Unable to write to Folder Settings because no resource is provided.');
|
436 | }
|
437 | const provider = this.getProvider(resolvedScope);
|
438 | if (provider && await provider.setPreference(preferenceName, value, resourceUri)) {
|
439 | return;
|
440 | }
|
441 | throw new Error(`Unable to write to ${PreferenceScope[resolvedScope]} Settings.`);
|
442 | }
|
443 |
|
444 | getBoolean(preferenceName: string): boolean | undefined;
|
445 | getBoolean(preferenceName: string, defaultValue: boolean): boolean;
|
446 | getBoolean(preferenceName: string, defaultValue: boolean, resourceUri: string): boolean;
|
447 | getBoolean(preferenceName: string, defaultValue?: boolean, resourceUri?: string): boolean | undefined {
|
448 | const value = resourceUri ? this.get(preferenceName, defaultValue, resourceUri) : this.get(preferenceName, defaultValue);
|
449 |
|
450 | return value !== null && value !== undefined ? !!value : defaultValue;
|
451 | }
|
452 |
|
453 | getString(preferenceName: string): string | undefined;
|
454 | getString(preferenceName: string, defaultValue: string): string;
|
455 | getString(preferenceName: string, defaultValue: string, resourceUri: string): string;
|
456 | getString(preferenceName: string, defaultValue?: string, resourceUri?: string): string | undefined {
|
457 | const value = resourceUri ? this.get(preferenceName, defaultValue, resourceUri) : this.get(preferenceName, defaultValue);
|
458 |
|
459 | if (value === null || value === undefined) {
|
460 | return defaultValue;
|
461 | }
|
462 | return value.toString();
|
463 | }
|
464 |
|
465 | getNumber(preferenceName: string): number | undefined;
|
466 | getNumber(preferenceName: string, defaultValue: number): number;
|
467 | getNumber(preferenceName: string, defaultValue: number, resourceUri: string): number;
|
468 | getNumber(preferenceName: string, defaultValue?: number, resourceUri?: string): number | undefined {
|
469 | const value = resourceUri ? this.get(preferenceName, defaultValue, resourceUri) : this.get(preferenceName, defaultValue);
|
470 |
|
471 | if (value === null || value === undefined) {
|
472 | return defaultValue;
|
473 | }
|
474 | if (typeof value === 'number') {
|
475 | return value;
|
476 | }
|
477 | return Number(value);
|
478 | }
|
479 |
|
480 | inspect<T extends JSONValue>(preferenceName: string, resourceUri?: string, forceLanguageOverride?: boolean): PreferenceInspection<T> | undefined {
|
481 | const defaultValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Default, resourceUri, forceLanguageOverride);
|
482 | const globalValue = this.inspectInScope<T>(preferenceName, PreferenceScope.User, resourceUri, forceLanguageOverride);
|
483 | const workspaceValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Workspace, resourceUri, forceLanguageOverride);
|
484 | const workspaceFolderValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Folder, resourceUri, forceLanguageOverride);
|
485 |
|
486 | const valueApplied = workspaceFolderValue ?? workspaceValue ?? globalValue ?? defaultValue;
|
487 |
|
488 | return { preferenceName, defaultValue, globalValue, workspaceValue, workspaceFolderValue, value: valueApplied };
|
489 | }
|
490 |
|
491 | inspectInScope<T extends JSONValue>(preferenceName: string, scope: PreferenceScope, resourceUri?: string, forceLanguageOverride?: boolean): T | undefined {
|
492 | const value = this.doInspectInScope<T>(preferenceName, scope, resourceUri);
|
493 | if (value === undefined && !forceLanguageOverride) {
|
494 | const overridden = this.overriddenPreferenceName(preferenceName);
|
495 | if (overridden) {
|
496 | return this.doInspectInScope(overridden.preferenceName, scope, resourceUri);
|
497 | }
|
498 | }
|
499 | return value;
|
500 | }
|
501 |
|
502 | protected getScopedValueFromInspection<T>(inspection: PreferenceInspection<T>, scope: PreferenceScope): T | undefined {
|
503 | switch (scope) {
|
504 | case PreferenceScope.Default:
|
505 | return inspection.defaultValue;
|
506 | case PreferenceScope.User:
|
507 | return inspection.globalValue;
|
508 | case PreferenceScope.Workspace:
|
509 | return inspection.workspaceValue;
|
510 | case PreferenceScope.Folder:
|
511 | return inspection.workspaceFolderValue;
|
512 | }
|
513 | unreachable(scope, 'Not all PreferenceScope enum variants handled.');
|
514 | }
|
515 |
|
516 | async updateValue(preferenceName: string, value: any, resourceUri?: string): Promise<void> {
|
517 | const inspection = this.inspect<any>(preferenceName, resourceUri);
|
518 | if (inspection) {
|
519 | const scopesToChange = this.getScopesToChange(inspection, value);
|
520 | const isDeletion = value === undefined
|
521 | || (scopesToChange.length === 1 && scopesToChange[0] === PreferenceScope.User && JSONExt.deepEqual(value, inspection.defaultValue));
|
522 | const effectiveValue = isDeletion ? undefined : value;
|
523 | await Promise.all(scopesToChange.map(scope => this.set(preferenceName, effectiveValue, scope, resourceUri)));
|
524 | }
|
525 | }
|
526 |
|
527 | protected getScopesToChange(inspection: PreferenceInspection<any>, intendedValue: any): PreferenceScope[] {
|
528 | if (JSONExt.deepEqual(inspection.value, intendedValue)) {
|
529 | return [];
|
530 | }
|
531 |
|
532 |
|
533 | const allScopes = PreferenceScope.getReversedScopes();
|
534 |
|
535 | allScopes.pop();
|
536 |
|
537 | const isScopeDefined = (scope: PreferenceScope) => this.getScopedValueFromInspection(inspection, scope) !== undefined;
|
538 |
|
539 | if (intendedValue === undefined) {
|
540 | return allScopes.filter(isScopeDefined);
|
541 | }
|
542 |
|
543 | return [allScopes.find(isScopeDefined) ?? PreferenceScope.User];
|
544 | }
|
545 |
|
546 | overridePreferenceName(options: OverridePreferenceName): string {
|
547 | return this.preferenceOverrideService.overridePreferenceName(options);
|
548 | }
|
549 | overriddenPreferenceName(preferenceName: string): OverridePreferenceName | undefined {
|
550 | return this.preferenceOverrideService.overriddenPreferenceName(preferenceName);
|
551 | }
|
552 |
|
553 | protected doHas(preferenceName: string, resourceUri?: string): boolean {
|
554 | return this.doGet(preferenceName, undefined, resourceUri) !== undefined;
|
555 | }
|
556 | protected doInspectInScope<T>(preferenceName: string, scope: PreferenceScope, resourceUri?: string): T | undefined {
|
557 | const provider = this.getProvider(scope);
|
558 | return provider && provider.get<T>(preferenceName, resourceUri);
|
559 | }
|
560 | protected doGet<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined {
|
561 | return this.doResolve(preferenceName, defaultValue, resourceUri).value;
|
562 | }
|
563 | protected doResolve<T>(preferenceName: string, defaultValue?: T, resourceUri?: string): PreferenceResolveResult<T> {
|
564 | const result: PreferenceResolveResult<T> = {};
|
565 | for (const scope of PreferenceScope.getScopes()) {
|
566 | if (this.schema.isValidInScope(preferenceName, scope)) {
|
567 | const provider = this.getProvider(scope);
|
568 | if (provider?.canHandleScope(scope)) {
|
569 | const { configUri, value } = provider.resolve<T>(preferenceName, resourceUri);
|
570 | if (value !== undefined) {
|
571 | result.configUri = configUri;
|
572 | result.value = PreferenceProvider.merge(result.value as any, value as any) as any;
|
573 | }
|
574 | }
|
575 | }
|
576 | }
|
577 | return {
|
578 | configUri: result.configUri,
|
579 | value: result.value !== undefined ? deepFreeze(result.value) : defaultValue
|
580 | };
|
581 | }
|
582 |
|
583 | getConfigUri(scope: PreferenceScope, resourceUri?: string, sectionName: string = this.configurations.getConfigName()): URI | undefined {
|
584 | const provider = this.getProvider(scope);
|
585 | if (!provider || !this.configurations.isAnyConfig(sectionName)) {
|
586 | return undefined;
|
587 | }
|
588 | const configUri = provider.getConfigUri(resourceUri, sectionName);
|
589 | if (configUri) {
|
590 | return configUri;
|
591 | }
|
592 | return provider.getContainingConfigUri && provider.getContainingConfigUri(resourceUri, sectionName);
|
593 | }
|
594 | }
|