UNPKG

11 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2018 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
17/* eslint-disable @typescript-eslint/no-explicit-any */
18
19import debounce = require('p-debounce');
20import { injectable, inject } from 'inversify';
21import { JSONExt, JSONValue } from '@phosphor/coreutils';
22import URI from '../../common/uri';
23import { Disposable, DisposableCollection, Emitter, Event, isObject } from '../../common';
24import { Deferred } from '../../common/promise-util';
25import { PreferenceScope } from './preference-scope';
26import { PreferenceLanguageOverrideService } from './preference-language-override-service';
27
28export interface PreferenceProviderDataChange {
29 /**
30 * The name of the changed preference.
31 */
32 readonly preferenceName: string;
33 /**
34 * The new value of the changed preference.
35 */
36 readonly newValue?: any;
37 /**
38 * The old value of the changed preference.
39 */
40 readonly oldValue?: any;
41 /**
42 * The {@link PreferenceScope} of the changed preference.
43 */
44 readonly scope: PreferenceScope;
45 /**
46 * URIs of the scopes in which this change applies.
47 */
48 readonly domain?: string[];
49}
50
51export namespace PreferenceProviderDataChange {
52 export function affects(change: PreferenceProviderDataChange, resourceUri?: string): boolean {
53 const resourcePath = resourceUri && new URI(resourceUri).path;
54 const domain = change.domain;
55 return !resourcePath || !domain || domain.some(uri => new URI(uri).path.relativity(resourcePath) >= 0);
56 }
57}
58
59export interface PreferenceProviderDataChanges {
60 [preferenceName: string]: PreferenceProviderDataChange;
61}
62
63export interface PreferenceResolveResult<T> {
64 configUri?: URI
65 value?: T
66}
67/**
68 * The {@link PreferenceProvider} is used to store and retrieve preference values. A {@link PreferenceProvider} does not operate in a global scope but is
69 * configured for one or more {@link PreferenceScope}s. The (default implementation for the) {@link PreferenceService} aggregates all {@link PreferenceProvider}s and
70 * serves as a common facade for manipulating preference values.
71 */
72@injectable()
73export abstract class PreferenceProvider implements Disposable {
74
75 @inject(PreferenceLanguageOverrideService) protected readonly preferenceOverrideService: PreferenceLanguageOverrideService;
76
77 protected readonly onDidPreferencesChangedEmitter = new Emitter<PreferenceProviderDataChanges>();
78 readonly onDidPreferencesChanged: Event<PreferenceProviderDataChanges> = this.onDidPreferencesChangedEmitter.event;
79
80 protected readonly toDispose = new DisposableCollection();
81
82 protected readonly _ready = new Deferred<void>();
83
84 constructor() {
85 this.toDispose.push(this.onDidPreferencesChangedEmitter);
86 }
87
88 dispose(): void {
89 this.toDispose.dispose();
90 }
91
92 protected deferredChanges: PreferenceProviderDataChanges | undefined;
93
94 /**
95 * Informs the listeners that one or more preferences of this provider are changed.
96 * The listeners are able to find what was changed from the emitted event.
97 */
98 protected emitPreferencesChangedEvent(changes: PreferenceProviderDataChanges | PreferenceProviderDataChange[]): Promise<boolean> {
99 if (Array.isArray(changes)) {
100 for (const change of changes) {
101 this.mergePreferenceProviderDataChange(change);
102 }
103 } else {
104 for (const preferenceName of Object.keys(changes)) {
105 this.mergePreferenceProviderDataChange(changes[preferenceName]);
106 }
107 }
108 return this.fireDidPreferencesChanged();
109 }
110
111 protected mergePreferenceProviderDataChange(change: PreferenceProviderDataChange): void {
112 if (!this.deferredChanges) {
113 this.deferredChanges = {};
114 }
115 const current = this.deferredChanges[change.preferenceName];
116 const { newValue, scope, domain } = change;
117 if (!current) {
118 // new
119 this.deferredChanges[change.preferenceName] = change;
120 } else if (current.oldValue === newValue) {
121 // delete
122 delete this.deferredChanges[change.preferenceName];
123 } else {
124 // update
125 Object.assign(current, { newValue, scope, domain });
126 }
127 }
128
129 protected fireDidPreferencesChanged = debounce(() => {
130 const changes = this.deferredChanges;
131 this.deferredChanges = undefined;
132 if (changes && Object.keys(changes).length) {
133 this.onDidPreferencesChangedEmitter.fire(changes);
134 return true;
135 }
136 return false;
137 }, 0);
138
139 /**
140 * Retrieve the stored value for the given preference and resource URI.
141 *
142 * @param preferenceName the preference identifier.
143 * @param resourceUri the uri of the resource for which the preference is stored. This is used to retrieve
144 * a potentially different value for the same preference for different resources, for example `files.encoding`.
145 *
146 * @returns the value stored for the given preference and resourceUri if it exists, otherwise `undefined`.
147 */
148 get<T>(preferenceName: string, resourceUri?: string): T | undefined {
149 return this.resolve<T>(preferenceName, resourceUri).value;
150 }
151
152 /**
153 * Resolve the value for the given preference and resource URI.
154 *
155 * @param preferenceName the preference identifier.
156 * @param resourceUri the URI of the resource for which this provider should resolve the preference. This is used to retrieve
157 * a potentially different value for the same preference for different resources, for example `files.encoding`.
158 *
159 * @returns an object containing the value stored for the given preference and resourceUri if it exists,
160 * otherwise `undefined`.
161 */
162 resolve<T>(preferenceName: string, resourceUri?: string): PreferenceResolveResult<T> {
163 const value = this.getPreferences(resourceUri)[preferenceName];
164 if (value !== undefined) {
165 return {
166 value,
167 configUri: this.getConfigUri(resourceUri)
168 };
169 }
170 return {};
171 }
172
173 abstract getPreferences(resourceUri?: string): { [p: string]: any };
174
175 /**
176 * Stores a new value for the given preference key in the provider.
177 * @param key the preference key (typically the name).
178 * @param value the new preference value.
179 * @param resourceUri the URI of the resource for which the preference is stored.
180 *
181 * @returns a promise that only resolves if all changes were delivered.
182 * If changes were made then implementation must either
183 * await on `this.emitPreferencesChangedEvent(...)` or
184 * `this.pendingChanges` if changes are fired indirectly.
185 */
186 abstract setPreference(key: string, value: any, resourceUri?: string): Promise<boolean>;
187
188 /**
189 * Resolved when the preference provider is ready to provide preferences
190 * It should be resolved by subclasses.
191 */
192 get ready(): Promise<void> {
193 return this._ready.promise;
194 }
195
196 /**
197 * Retrieve the domain for this provider.
198 *
199 * @returns the domain or `undefined` if this provider is suitable for all domains.
200 */
201 getDomain(): string[] | undefined {
202 return undefined;
203 }
204
205 /**
206 * Retrieve the configuration URI for the given resource URI.
207 * @param resourceUri the uri of the resource or `undefined`.
208 * @param sectionName the section to return the URI for, e.g. `tasks` or `launch`. Defaults to settings.
209 *
210 * @returns the corresponding resource URI or `undefined` if there is no valid URI.
211 */
212 getConfigUri(resourceUri?: string, sectionName?: string): URI | undefined {
213 return undefined;
214 }
215
216 /**
217 * Retrieves the first valid configuration URI contained by the given resource.
218 * @param resourceUri the uri of the container resource or `undefined`.
219 *
220 * @returns the first valid configuration URI contained by the given resource `undefined`
221 * if there is no valid configuration URI at all.
222 */
223 getContainingConfigUri?(resourceUri?: string, sectionName?: string): URI | undefined;
224
225 static merge(source: JSONValue | undefined, target: JSONValue): JSONValue {
226 if (source === undefined || !JSONExt.isObject(source)) {
227 return JSONExt.deepCopy(target);
228 }
229 if (JSONExt.isPrimitive(target)) {
230 return {};
231 }
232 for (const key of Object.keys(target)) {
233 const value = (target as any)[key];
234 if (key in source) {
235 if (JSONExt.isObject(source[key]) && JSONExt.isObject(value)) {
236 this.merge(source[key], value);
237 continue;
238 } else if (JSONExt.isArray(source[key]) && JSONExt.isArray(value)) {
239 source[key] = [...JSONExt.deepCopy(source[key] as any), ...JSONExt.deepCopy(value)];
240 continue;
241 }
242 }
243 source[key] = JSONExt.deepCopy(value);
244 }
245 return source;
246 }
247
248 /**
249 * Handles deep equality with the possibility of `undefined`
250 */
251 static deepEqual(a: JSONValue | undefined, b: JSONValue | undefined): boolean {
252 if (a === b) { return true; }
253 if (a === undefined || b === undefined) { return false; }
254 return JSONExt.deepEqual(a, b);
255 }
256
257 protected getParsedContent(jsonData: any): { [key: string]: any } {
258 const preferences: { [key: string]: any } = {};
259 if (!isObject(jsonData)) {
260 return preferences;
261 }
262 for (const [preferenceName, preferenceValue] of Object.entries(jsonData)) {
263 if (this.preferenceOverrideService.testOverrideValue(preferenceName, preferenceValue)) {
264 for (const [overriddenPreferenceName, overriddenValue] of Object.entries(preferenceValue)) {
265 preferences[`${preferenceName}.${overriddenPreferenceName}`] = overriddenValue;
266 }
267 } else {
268 preferences[preferenceName] = preferenceValue;
269 }
270 }
271 return preferences;
272 }
273
274 canHandleScope(scope: PreferenceScope): boolean {
275 return true;
276 }
277}