1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import * as Ajv from 'ajv';
|
18 | import { inject, injectable, interfaces, named, postConstruct } from 'inversify';
|
19 | import { ContributionProvider, bindContributionProvider, Emitter, Event, Disposable } from '../../common';
|
20 | import { PreferenceScope } from './preference-scope';
|
21 | import { PreferenceProvider, PreferenceProviderDataChange } from './preference-provider';
|
22 | import {
|
23 | PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty
|
24 | } from '../../common/preferences/preference-schema';
|
25 | import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider';
|
26 | import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
|
27 | import { bindPreferenceConfigurations, PreferenceConfigurations } from './preference-configurations';
|
28 | export { PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty };
|
29 | import { isObject, Mutable } from '../../common/types';
|
30 | import { PreferenceLanguageOverrideService } from './preference-language-override-service';
|
31 | import { JSONValue } from '@phosphor/coreutils';
|
32 |
|
33 |
|
34 |
|
35 | export const PreferenceContribution = Symbol('PreferenceContribution');
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | export interface PreferenceContribution {
|
62 | readonly schema: PreferenceSchema;
|
63 | }
|
64 |
|
65 | export function bindPreferenceSchemaProvider(bind: interfaces.Bind): void {
|
66 | bindPreferenceConfigurations(bind);
|
67 | bind(PreferenceSchemaProvider).toSelf().inSingletonScope();
|
68 | bind(PreferenceLanguageOverrideService).toSelf().inSingletonScope();
|
69 | bindContributionProvider(bind, PreferenceContribution);
|
70 | }
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | export interface FrontendApplicationPreferenceConfig extends FrontendApplicationConfig {
|
77 | preferences: {
|
78 | [preferenceName: string]: any
|
79 | }
|
80 | }
|
81 | export namespace FrontendApplicationPreferenceConfig {
|
82 | export function is(config: FrontendApplicationConfig): config is FrontendApplicationPreferenceConfig {
|
83 | return isObject(config.preferences);
|
84 | }
|
85 | }
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | @injectable()
|
93 | export class PreferenceSchemaProvider extends PreferenceProvider {
|
94 |
|
95 | protected readonly preferences: { [name: string]: any } = {};
|
96 | protected readonly combinedSchema: PreferenceDataSchema = { properties: {}, patternProperties: {}, allowComments: true, allowTrailingCommas: true, };
|
97 | protected readonly workspaceSchema: PreferenceDataSchema = { properties: {}, patternProperties: {}, allowComments: true, allowTrailingCommas: true, };
|
98 | protected readonly folderSchema: PreferenceDataSchema = { properties: {}, patternProperties: {}, allowComments: true, allowTrailingCommas: true, };
|
99 |
|
100 | @inject(ContributionProvider) @named(PreferenceContribution)
|
101 | protected readonly preferenceContributions: ContributionProvider<PreferenceContribution>;
|
102 |
|
103 | @inject(PreferenceConfigurations)
|
104 | protected readonly configurations: PreferenceConfigurations;
|
105 |
|
106 | protected readonly onDidPreferenceSchemaChangedEmitter = new Emitter<void>();
|
107 | readonly onDidPreferenceSchemaChanged: Event<void> = this.onDidPreferenceSchemaChangedEmitter.event;
|
108 | protected fireDidPreferenceSchemaChanged(): void {
|
109 | this.onDidPreferenceSchemaChangedEmitter.fire(undefined);
|
110 | }
|
111 |
|
112 | @postConstruct()
|
113 | protected init(): void {
|
114 | this.readConfiguredPreferences();
|
115 | this.preferenceContributions.getContributions().forEach(contrib => {
|
116 | this.doSetSchema(contrib.schema);
|
117 | });
|
118 | this.combinedSchema.additionalProperties = false;
|
119 | this._ready.resolve();
|
120 | }
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | registerOverrideIdentifier(overrideIdentifier: string): void {
|
131 | if (this.preferenceOverrideService.addOverrideIdentifier(overrideIdentifier)) {
|
132 | this.updateOverridePatternPropertiesKey();
|
133 | }
|
134 | }
|
135 |
|
136 | protected readonly overridePatternProperties: Required<Pick<PreferenceDataProperty, 'properties' | 'additionalProperties'>> & PreferenceDataProperty = {
|
137 | type: 'object',
|
138 | description: 'Configure editor settings to be overridden for a language.',
|
139 | errorMessage: 'Unknown Identifier. Use language identifiers',
|
140 | properties: {},
|
141 | additionalProperties: false
|
142 | };
|
143 | protected overridePatternPropertiesKey: string | undefined;
|
144 | protected updateOverridePatternPropertiesKey(): void {
|
145 | const oldKey = this.overridePatternPropertiesKey;
|
146 | const newKey = this.preferenceOverrideService.computeOverridePatternPropertiesKey();
|
147 | if (oldKey === newKey) {
|
148 | return;
|
149 | }
|
150 | if (oldKey) {
|
151 | delete this.combinedSchema.patternProperties[oldKey];
|
152 | }
|
153 | this.overridePatternPropertiesKey = newKey;
|
154 | if (newKey) {
|
155 | this.combinedSchema.patternProperties[newKey] = this.overridePatternProperties;
|
156 | }
|
157 | this.fireDidPreferenceSchemaChanged();
|
158 | }
|
159 |
|
160 | protected doUnsetSchema(changes: PreferenceProviderDataChange[]): PreferenceProviderDataChange[] {
|
161 | const inverseChanges: PreferenceProviderDataChange[] = [];
|
162 | for (const change of changes) {
|
163 | const preferenceName = change.preferenceName;
|
164 | const overridden = this.preferenceOverrideService.overriddenPreferenceName(preferenceName);
|
165 | if (overridden) {
|
166 | delete this.overridePatternProperties.properties[`[${overridden.overrideIdentifier}]`];
|
167 | this.removePropFromSchemas(`[${overridden.overrideIdentifier}]`);
|
168 | } else {
|
169 | this.removePropFromSchemas(preferenceName);
|
170 | }
|
171 | const newValue = change.oldValue;
|
172 | const oldValue = change.newValue;
|
173 | const { scope, domain } = change;
|
174 | const inverseChange: Mutable<PreferenceProviderDataChange> = { preferenceName, oldValue, scope, domain };
|
175 | if (typeof newValue === undefined) {
|
176 | delete this.preferences[preferenceName];
|
177 | } else {
|
178 | inverseChange.newValue = newValue;
|
179 | this.preferences[preferenceName] = newValue;
|
180 | }
|
181 | inverseChanges.push(inverseChange);
|
182 | }
|
183 | return inverseChanges;
|
184 | }
|
185 |
|
186 | protected validateSchema(schema: PreferenceSchema): void {
|
187 | const ajv = new Ajv();
|
188 | const valid = ajv.validateSchema(schema);
|
189 | if (!valid) {
|
190 | const errors = !!ajv.errors ? ajv.errorsText(ajv.errors) : 'unknown validation error';
|
191 | console.warn('A contributed preference schema has validation issues : ' + errors);
|
192 | }
|
193 | }
|
194 |
|
195 | protected doSetSchema(schema: PreferenceSchema): PreferenceProviderDataChange[] {
|
196 | if (FrontendApplicationConfigProvider.get().validatePreferencesSchema) {
|
197 | this.validateSchema(schema);
|
198 | }
|
199 | const scope = PreferenceScope.Default;
|
200 | const domain = this.getDomain();
|
201 | const changes: PreferenceProviderDataChange[] = [];
|
202 | const defaultScope = PreferenceSchema.getDefaultScope(schema);
|
203 | const overridable = schema.overridable || false;
|
204 | for (const [preferenceName, rawSchemaProps] of Object.entries(schema.properties)) {
|
205 | if (this.combinedSchema.properties[preferenceName]) {
|
206 | console.error('Preference name collision detected in the schema for property: ' + preferenceName);
|
207 | } else if (!rawSchemaProps.hasOwnProperty('included') || rawSchemaProps.included) {
|
208 | const schemaProps = PreferenceDataProperty.fromPreferenceSchemaProperty(rawSchemaProps, defaultScope);
|
209 | if (typeof schemaProps.overridable !== 'boolean' && overridable) {
|
210 | schemaProps.overridable = true;
|
211 | }
|
212 | if (schemaProps.overridable) {
|
213 | this.overridePatternProperties.properties[preferenceName] = schemaProps;
|
214 | }
|
215 | this.updateSchemaProps(preferenceName, schemaProps);
|
216 |
|
217 | const schemaDefault = this.getDefaultValue(schemaProps);
|
218 | const configuredDefault = this.getConfiguredDefault(preferenceName);
|
219 | if (this.preferenceOverrideService.testOverrideValue(preferenceName, schemaDefault)) {
|
220 | schemaProps.defaultValue = PreferenceSchemaProperties.is(configuredDefault)
|
221 | ? PreferenceProvider.merge(schemaDefault, configuredDefault)
|
222 | : schemaDefault;
|
223 | if (schemaProps.defaultValue && PreferenceSchemaProperties.is(schemaProps.defaultValue)) {
|
224 | for (const overriddenPreferenceName in schemaProps.defaultValue) {
|
225 | const overrideValue = schemaDefault[overriddenPreferenceName];
|
226 | const overridePreferenceName = `${preferenceName}.${overriddenPreferenceName}`;
|
227 | changes.push(this.doSetPreferenceValue(overridePreferenceName, overrideValue, { scope, domain }));
|
228 | }
|
229 | }
|
230 | } else {
|
231 | schemaProps.defaultValue = configuredDefault === undefined ? schemaDefault : configuredDefault;
|
232 | changes.push(this.doSetPreferenceValue(preferenceName, schemaProps.defaultValue, { scope, domain }));
|
233 | }
|
234 | }
|
235 | }
|
236 | return changes;
|
237 | }
|
238 |
|
239 | protected doSetPreferenceValue(preferenceName: string, newValue: any, { scope, domain }: {
|
240 | scope: PreferenceScope,
|
241 | domain?: string[]
|
242 | }): PreferenceProviderDataChange {
|
243 | const oldValue = this.preferences[preferenceName];
|
244 | this.preferences[preferenceName] = newValue;
|
245 | return { preferenceName, oldValue, newValue, scope, domain };
|
246 | }
|
247 |
|
248 | getDefaultValue(property: PreferenceItem): JSONValue {
|
249 | if (property.defaultValue !== undefined) {
|
250 | return property.defaultValue;
|
251 | }
|
252 | if (property.default !== undefined) {
|
253 | return property.default;
|
254 | }
|
255 | const type = Array.isArray(property.type) ? property.type[0] : property.type;
|
256 | switch (type) {
|
257 | case 'boolean':
|
258 | return false;
|
259 | case 'integer':
|
260 | case 'number':
|
261 | return 0;
|
262 | case 'string':
|
263 | return '';
|
264 | case 'array':
|
265 | return [];
|
266 | case 'object':
|
267 | return {};
|
268 | }
|
269 |
|
270 | return null;
|
271 | }
|
272 |
|
273 | protected getConfiguredDefault(preferenceName: string): any {
|
274 | const config = FrontendApplicationConfigProvider.get();
|
275 | if (preferenceName && FrontendApplicationPreferenceConfig.is(config) && preferenceName in config.preferences) {
|
276 | return config.preferences[preferenceName];
|
277 | }
|
278 | }
|
279 |
|
280 | getCombinedSchema(): PreferenceDataSchema {
|
281 | return this.combinedSchema;
|
282 | }
|
283 |
|
284 | getSchema(scope: PreferenceScope): PreferenceDataSchema {
|
285 | switch (scope) {
|
286 | case PreferenceScope.Default:
|
287 | case PreferenceScope.User:
|
288 | return this.combinedSchema;
|
289 | case PreferenceScope.Workspace:
|
290 | return this.workspaceSchema;
|
291 | case PreferenceScope.Folder:
|
292 | return this.folderSchema;
|
293 | }
|
294 | }
|
295 |
|
296 | setSchema(schema: PreferenceSchema): Disposable {
|
297 | const changes = this.doSetSchema(schema);
|
298 | if (!changes.length) {
|
299 | return Disposable.NULL;
|
300 | }
|
301 | this.fireDidPreferenceSchemaChanged();
|
302 | this.emitPreferencesChangedEvent(changes);
|
303 | return Disposable.create(() => {
|
304 | const inverseChanges = this.doUnsetSchema(changes);
|
305 | if (!inverseChanges.length) {
|
306 | return;
|
307 | }
|
308 | this.fireDidPreferenceSchemaChanged();
|
309 | this.emitPreferencesChangedEvent(inverseChanges);
|
310 | });
|
311 | }
|
312 |
|
313 | getPreferences(): { [name: string]: any } {
|
314 | return this.preferences;
|
315 | }
|
316 |
|
317 | async setPreference(): Promise<boolean> {
|
318 | return false;
|
319 | }
|
320 |
|
321 | isValidInScope(preferenceName: string, scope: PreferenceScope): boolean {
|
322 | let property;
|
323 | const overridden = this.preferenceOverrideService.overriddenPreferenceName(preferenceName);
|
324 | if (overridden) {
|
325 |
|
326 | property = this.overridePatternProperties[`[${overridden.overrideIdentifier}]`];
|
327 | property = property && property[overridden.preferenceName];
|
328 | if (!property) {
|
329 |
|
330 | property = this.overridePatternProperties[overridden.preferenceName];
|
331 | }
|
332 | if (!property) {
|
333 |
|
334 | property = this.combinedSchema.properties[overridden.preferenceName];
|
335 | }
|
336 | } else {
|
337 | property = this.combinedSchema.properties[preferenceName];
|
338 | }
|
339 | return property && property.scope! >= scope;
|
340 | }
|
341 |
|
342 | *getPreferenceNames(): IterableIterator<string> {
|
343 | for (const preferenceName in this.combinedSchema.properties) {
|
344 | yield preferenceName;
|
345 | for (const overridePreferenceName of this.getOverridePreferenceNames(preferenceName)) {
|
346 | yield overridePreferenceName;
|
347 | }
|
348 | }
|
349 | }
|
350 |
|
351 | getOverridePreferenceNames(preferenceName: string): IterableIterator<string> {
|
352 | const preference = this.combinedSchema.properties[preferenceName];
|
353 | if (preference && preference.overridable) {
|
354 | return this.preferenceOverrideService.getOverridePreferenceNames(preferenceName);
|
355 | }
|
356 | return [][Symbol.iterator]();
|
357 | }
|
358 |
|
359 | getSchemaProperty(key: string): PreferenceDataProperty | undefined {
|
360 | return this.combinedSchema.properties[key];
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 | updateSchemaProperty(key: string, property: PreferenceDataProperty): void {
|
368 | this.updateSchemaProps(key, property);
|
369 | this.fireDidPreferenceSchemaChanged();
|
370 | }
|
371 |
|
372 | protected updateSchemaProps(key: string, property: PreferenceDataProperty): void {
|
373 | this.combinedSchema.properties[key] = property;
|
374 |
|
375 | switch (property.scope) {
|
376 | case PreferenceScope.Folder:
|
377 | this.folderSchema.properties[key] = property;
|
378 |
|
379 |
|
380 | case PreferenceScope.Workspace:
|
381 | this.workspaceSchema.properties[key] = property;
|
382 | break;
|
383 | }
|
384 | }
|
385 |
|
386 | protected removePropFromSchemas(key: string): void {
|
387 |
|
388 | delete this.combinedSchema.properties[key];
|
389 | delete this.workspaceSchema.properties[key];
|
390 | delete this.folderSchema.properties[key];
|
391 | }
|
392 |
|
393 | protected readConfiguredPreferences(): void {
|
394 | const config = FrontendApplicationConfigProvider.get();
|
395 | if (FrontendApplicationPreferenceConfig.is(config)) {
|
396 | try {
|
397 | const configuredDefaults = config.preferences;
|
398 | const parsedDefaults = this.getParsedContent(configuredDefaults);
|
399 | Object.assign(this.preferences, parsedDefaults);
|
400 | const scope = PreferenceScope.Default;
|
401 | const domain = this.getDomain();
|
402 | const changes: PreferenceProviderDataChange[] = Object.keys(this.preferences)
|
403 | .map((key): PreferenceProviderDataChange => ({ preferenceName: key, oldValue: undefined, newValue: this.preferences[key], scope, domain }));
|
404 | this.emitPreferencesChangedEvent(changes);
|
405 | } catch (e) {
|
406 | console.error('Failed to load preferences from frontend configuration.', e);
|
407 | }
|
408 | }
|
409 | }
|
410 |
|
411 | }
|