Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/configurationExtensionPoint.ts
5239 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../nls.js';
7
import * as objects from '../../../base/common/objects.js';
8
import { Registry } from '../../../platform/registry/common/platform.js';
9
import { IJSONSchema } from '../../../base/common/jsonSchema.js';
10
import { ExtensionsRegistry, IExtensionPointUser } from '../../services/extensions/common/extensionsRegistry.js';
11
import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue, getAllConfigurationProperties, parseScope, EXTENSION_UNIFICATION_EXTENSION_IDS, overrideIdentifiersFromKey } from '../../../platform/configuration/common/configurationRegistry.js';
12
import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../platform/jsonschemas/common/jsonContributionRegistry.js';
13
import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId, mcpSchemaId } from '../../services/configuration/common/configuration.js';
14
import { isObject, isUndefined } from '../../../base/common/types.js';
15
import { ExtensionIdentifierMap, IExtensionManifest } from '../../../platform/extensions/common/extensions.js';
16
import { IStringDictionary } from '../../../base/common/collections.js';
17
import { Extensions as ExtensionFeaturesExtensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from '../../services/extensionManagement/common/extensionFeatures.js';
18
import { Disposable } from '../../../base/common/lifecycle.js';
19
import { SyncDescriptor } from '../../../platform/instantiation/common/descriptors.js';
20
import { MarkdownString } from '../../../base/common/htmlContent.js';
21
import product from '../../../platform/product/common/product.js';
22
23
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
24
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
25
26
const configurationEntrySchema: IJSONSchema = {
27
type: 'object',
28
defaultSnippets: [{ body: { title: '', properties: {} } }],
29
properties: {
30
title: {
31
description: nls.localize('vscode.extension.contributes.configuration.title', 'A title for the current category of settings. This label will be rendered in the Settings editor as a subheading. If the title is the same as the extension display name, then the category will be grouped under the main extension heading.'),
32
type: 'string'
33
},
34
order: {
35
description: nls.localize('vscode.extension.contributes.configuration.order', 'When specified, gives the order of this category of settings relative to other categories.'),
36
type: 'integer'
37
},
38
properties: {
39
description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),
40
type: 'object',
41
propertyNames: {
42
pattern: '\\S+',
43
patternErrorMessage: nls.localize('vscode.extension.contributes.configuration.property.empty', 'Property should not be empty.'),
44
},
45
additionalProperties: {
46
anyOf: [
47
{
48
title: nls.localize('vscode.extension.contributes.configuration.properties.schema', 'Schema of the configuration property.'),
49
$ref: 'http://json-schema.org/draft-07/schema#'
50
},
51
{
52
type: 'object',
53
properties: {
54
scope: {
55
type: 'string',
56
enum: ['application', 'machine', 'window', 'resource', 'language-overridable', 'machine-overridable'],
57
default: 'window',
58
enumDescriptions: [
59
nls.localize('scope.application.description', "Configuration that can be configured only in the user settings."),
60
nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings or only in the remote settings."),
61
nls.localize('scope.window.description', "Configuration that can be configured in the user, remote or workspace settings."),
62
nls.localize('scope.resource.description', "Configuration that can be configured in the user, remote, workspace or folder settings."),
63
nls.localize('scope.language-overridable.description', "Resource configuration that can be configured in language specific settings."),
64
nls.localize('scope.machine-overridable.description', "Machine configuration that can be configured also in workspace or folder settings.")
65
],
66
markdownDescription: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `application`, `machine`, `window`, `resource`, and `machine-overridable`.")
67
},
68
enumDescriptions: {
69
type: 'array',
70
items: {
71
type: 'string',
72
},
73
description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values')
74
},
75
markdownEnumDescriptions: {
76
type: 'array',
77
items: {
78
type: 'string',
79
},
80
description: nls.localize('scope.markdownEnumDescriptions', 'Descriptions for enum values in the markdown format.')
81
},
82
enumItemLabels: {
83
type: 'array',
84
items: {
85
type: 'string'
86
},
87
markdownDescription: nls.localize('scope.enumItemLabels', 'Labels for enum values to be displayed in the Settings editor. When specified, the {0} values still show after the labels, but less prominently.', '`enum`')
88
},
89
markdownDescription: {
90
type: 'string',
91
description: nls.localize('scope.markdownDescription', 'The description in the markdown format.')
92
},
93
deprecationMessage: {
94
type: 'string',
95
description: nls.localize('scope.deprecationMessage', 'If set, the property is marked as deprecated and the given message is shown as an explanation.')
96
},
97
markdownDeprecationMessage: {
98
type: 'string',
99
description: nls.localize('scope.markdownDeprecationMessage', 'If set, the property is marked as deprecated and the given message is shown as an explanation in the markdown format.')
100
},
101
editPresentation: {
102
type: 'string',
103
enum: ['singlelineText', 'multilineText'],
104
enumDescriptions: [
105
nls.localize('scope.singlelineText.description', 'The value will be shown in an inputbox.'),
106
nls.localize('scope.multilineText.description', 'The value will be shown in a textarea.')
107
],
108
default: 'singlelineText',
109
description: nls.localize('scope.editPresentation', 'When specified, controls the presentation format of the string setting.')
110
},
111
order: {
112
type: 'integer',
113
description: nls.localize('scope.order', 'When specified, gives the order of this setting relative to other settings within the same category. Settings with an order property will be placed before settings without this property set.')
114
},
115
ignoreSync: {
116
type: 'boolean',
117
description: nls.localize('scope.ignoreSync', 'When enabled, Settings Sync will not sync the user value of this configuration by default.')
118
},
119
tags: {
120
type: 'array',
121
items: {
122
type: 'string',
123
enum: [
124
'accessibility',
125
'advanced',
126
'experimental',
127
'telemetry',
128
'usesOnlineServices',
129
],
130
enumDescriptions: [
131
nls.localize('accessibility', 'Accessibility settings'),
132
nls.localize('advanced', 'Advanced settings are hidden by default in the Settings editor unless the user chooses to show advanced settings.'),
133
nls.localize('experimental', 'Experimental settings are subject to change and may be removed in future releases.'),
134
nls.localize('preview', 'Preview settings can be used to try out new features before they are finalized.'),
135
nls.localize('telemetry', 'Telemetry settings'),
136
nls.localize('usesOnlineServices', 'Settings that use online services')
137
],
138
},
139
additionalItems: true,
140
markdownDescription: nls.localize('scope.tags', 'A list of tags under which to place the setting. The tag can then be searched up in the Settings editor. For example, specifying the `experimental` tag allows one to find the setting by searching `@tag:experimental`.'),
141
}
142
}
143
}
144
]
145
}
146
}
147
}
148
};
149
150
// build up a delta across two ext points and only apply it once
151
let _configDelta: IConfigurationDelta | undefined;
152
153
154
// BEGIN VSCode extension point `configurationDefaults`
155
const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IStringDictionary<IStringDictionary<unknown>>>({
156
extensionPoint: 'configurationDefaults',
157
jsonSchema: {
158
$ref: configurationDefaultsSchemaId,
159
},
160
canHandleResolver: true
161
});
162
defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {
163
164
if (_configDelta) {
165
// HIGHLY unlikely, but just in case
166
configurationRegistry.deltaConfiguration(_configDelta);
167
}
168
169
const configNow = _configDelta = {};
170
// schedule a HIGHLY unlikely task in case only the default configurations EXT point changes
171
queueMicrotask(() => {
172
if (_configDelta === configNow) {
173
configurationRegistry.deltaConfiguration(_configDelta);
174
_configDelta = undefined;
175
}
176
});
177
178
if (removed.length) {
179
const removedDefaultConfigurations = removed.map<IConfigurationDefaults>(extension => ({ overrides: objects.deepClone(extension.value), source: { id: extension.description.identifier.value, displayName: extension.description.displayName } }));
180
_configDelta.removedDefaults = removedDefaultConfigurations;
181
}
182
if (added.length) {
183
const registeredProperties = configurationRegistry.getConfigurationProperties();
184
const allowedScopes = [ConfigurationScope.MACHINE_OVERRIDABLE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];
185
const addedDefaultConfigurations = added.map<IConfigurationDefaults>(extension => {
186
const overrides = objects.deepClone(extension.value);
187
for (const key of Object.keys(overrides)) {
188
const registeredPropertyScheme = registeredProperties[key];
189
if (registeredPropertyScheme?.disallowConfigurationDefault) {
190
extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key));
191
delete overrides[key];
192
continue;
193
}
194
if (!OVERRIDE_PROPERTY_REGEX.test(key)) {
195
if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) {
196
extension.collector.warn(nls.localize('config.property.defaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. Only defaults for machine-overridable, window, resource and language overridable scoped settings are supported.", key));
197
delete overrides[key];
198
continue;
199
}
200
}
201
}
202
return { overrides, source: { id: extension.description.identifier.value, displayName: extension.description.displayName } };
203
});
204
_configDelta.addedDefaults = addedDefaultConfigurations;
205
}
206
});
207
// END VSCode extension point `configurationDefaults`
208
209
210
// BEGIN VSCode extension point `configuration`
211
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({
212
extensionPoint: 'configuration',
213
deps: [defaultConfigurationExtPoint],
214
jsonSchema: {
215
description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
216
oneOf: [
217
configurationEntrySchema,
218
{
219
type: 'array',
220
items: configurationEntrySchema
221
}
222
]
223
},
224
canHandleResolver: true
225
});
226
227
const extensionConfigurations: ExtensionIdentifierMap<IConfigurationNode[]> = new ExtensionIdentifierMap<IConfigurationNode[]>();
228
229
configurationExtPoint.setHandler((extensions, { added, removed }) => {
230
231
// HIGHLY unlikely (only configuration but not defaultConfiguration EXT point changes)
232
_configDelta ??= {};
233
234
if (removed.length) {
235
const removedConfigurations: IConfigurationNode[] = [];
236
for (const extension of removed) {
237
removedConfigurations.push(...(extensionConfigurations.get(extension.description.identifier) || []));
238
extensionConfigurations.delete(extension.description.identifier);
239
}
240
_configDelta.removedConfigurations = removedConfigurations;
241
}
242
243
const seenProperties = new Set<string>();
244
245
function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<unknown>): IConfigurationNode {
246
const configuration = objects.deepClone(node);
247
248
if (configuration.title && (typeof configuration.title !== 'string')) {
249
extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));
250
}
251
252
validateProperties(configuration, extension);
253
254
configuration.id = node.id || extension.description.identifier.value;
255
configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName };
256
configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined;
257
configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value;
258
return configuration;
259
}
260
261
function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<unknown>): void {
262
const properties = configuration.properties;
263
const extensionConfigurationPolicy = product.extensionConfigurationPolicy;
264
if (properties) {
265
if (typeof properties !== 'object') {
266
extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));
267
configuration.properties = {};
268
}
269
for (const key in properties) {
270
const propertyConfiguration = properties[key];
271
const message = validateProperty(key, propertyConfiguration, extension.description.identifier.value);
272
if (message) {
273
delete properties[key];
274
extension.collector.warn(message);
275
continue;
276
}
277
if (seenProperties.has(key) && !EXTENSION_UNIFICATION_EXTENSION_IDS.has(extension.description.identifier.value.toLowerCase())) {
278
delete properties[key];
279
extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key));
280
continue;
281
}
282
if (!isObject(propertyConfiguration)) {
283
delete properties[key];
284
extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key));
285
continue;
286
}
287
if (extensionConfigurationPolicy?.[key]) {
288
propertyConfiguration.policy = extensionConfigurationPolicy?.[key];
289
}
290
if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) {
291
propertyConfiguration.experiment = {
292
mode: 'startup'
293
};
294
}
295
seenProperties.add(key);
296
propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW;
297
}
298
}
299
const subNodes = configuration.allOf;
300
if (subNodes) {
301
extension.collector.error(nls.localize('invalid.allOf', "'configuration.allOf' is deprecated and should no longer be used. Instead, pass multiple configuration sections as an array to the 'configuration' contribution point."));
302
for (const node of subNodes) {
303
validateProperties(node, extension);
304
}
305
}
306
}
307
308
if (added.length) {
309
const addedConfigurations: IConfigurationNode[] = [];
310
for (const extension of added) {
311
const configurations: IConfigurationNode[] = [];
312
const value = <IConfigurationNode | IConfigurationNode[]>extension.value;
313
if (Array.isArray(value)) {
314
value.forEach(v => configurations.push(handleConfiguration(v, extension)));
315
} else {
316
configurations.push(handleConfiguration(value, extension));
317
}
318
extensionConfigurations.set(extension.description.identifier, configurations);
319
addedConfigurations.push(...configurations);
320
}
321
322
_configDelta.addedConfigurations = addedConfigurations;
323
}
324
325
configurationRegistry.deltaConfiguration(_configDelta);
326
_configDelta = undefined;
327
});
328
// END VSCode extension point `configuration`
329
330
jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', {
331
allowComments: true,
332
allowTrailingCommas: true,
333
default: {
334
folders: [
335
{
336
path: ''
337
}
338
],
339
settings: {
340
}
341
},
342
required: ['folders'],
343
properties: {
344
'folders': {
345
minItems: 0,
346
uniqueItems: true,
347
description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."),
348
items: {
349
type: 'object',
350
defaultSnippets: [{ body: { path: '$1' } }],
351
oneOf: [{
352
properties: {
353
path: {
354
type: 'string',
355
description: nls.localize('workspaceConfig.path.description', "A file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file.")
356
},
357
name: {
358
type: 'string',
359
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
360
}
361
},
362
required: ['path']
363
}, {
364
properties: {
365
uri: {
366
type: 'string',
367
description: nls.localize('workspaceConfig.uri.description', "URI of the folder")
368
},
369
name: {
370
type: 'string',
371
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
372
}
373
},
374
required: ['uri']
375
}]
376
}
377
},
378
'settings': {
379
type: 'object',
380
default: {},
381
description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
382
$ref: workspaceSettingsSchemaId
383
},
384
'launch': {
385
type: 'object',
386
default: { configurations: [], compounds: [] },
387
description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"),
388
$ref: launchSchemaId
389
},
390
'tasks': {
391
type: 'object',
392
default: { version: '2.0.0', tasks: [] },
393
description: nls.localize('workspaceConfig.tasks.description', "Workspace task configurations"),
394
$ref: tasksSchemaId
395
},
396
'mcp': {
397
type: 'object',
398
default: {
399
inputs: [],
400
servers: {
401
'mcp-server-time': {
402
command: 'uvx',
403
args: ['mcp_server_time', '--local-timezone=America/Los_Angeles']
404
}
405
}
406
},
407
description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),
408
$ref: mcpSchemaId
409
},
410
'extensions': {
411
type: 'object',
412
default: {},
413
description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),
414
$ref: 'vscode://schemas/extensions'
415
},
416
'remoteAuthority': {
417
type: 'string',
418
doNotSuggest: true,
419
description: nls.localize('workspaceConfig.remoteAuthority', "The remote server where the workspace is located."),
420
},
421
'transient': {
422
type: 'boolean',
423
doNotSuggest: true,
424
description: nls.localize('workspaceConfig.transient', "A transient workspace will disappear when restarting or reloading."),
425
}
426
},
427
errorMessage: nls.localize('unknownWorkspaceProperty', "Unknown workspace configuration property")
428
});
429
430
431
class SettingsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {
432
433
readonly type = 'table';
434
435
shouldRender(manifest: IExtensionManifest): boolean {
436
return !!manifest.contributes?.configuration;
437
}
438
439
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
440
const configuration: IConfigurationNode[] = manifest.contributes?.configuration
441
? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]
442
: [];
443
444
const properties = getAllConfigurationProperties(configuration);
445
446
const contrib = properties ? Object.keys(properties) : [];
447
const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")];
448
const rows: IRowData[][] = contrib.sort((a, b) => a.localeCompare(b))
449
.map(key => {
450
return [
451
new MarkdownString().appendMarkdown(`\`${key}\``),
452
properties[key].markdownDescription ? new MarkdownString(properties[key].markdownDescription, false) : properties[key].description ?? '',
453
new MarkdownString().appendCodeblock('json', JSON.stringify(isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default, null, 2)),
454
];
455
});
456
457
return {
458
data: {
459
headers,
460
rows
461
},
462
dispose: () => { }
463
};
464
}
465
}
466
467
Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({
468
id: 'configuration',
469
label: nls.localize('settings', "Settings"),
470
access: {
471
canToggle: false
472
},
473
renderer: new SyncDescriptor(SettingsTableRenderer),
474
});
475
476
class ConfigurationDefaultsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {
477
478
readonly type = 'table';
479
480
shouldRender(manifest: IExtensionManifest): boolean {
481
return !!manifest.contributes?.configurationDefaults;
482
}
483
484
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
485
const configurationDefaults = manifest.contributes?.configurationDefaults ?? {};
486
487
const headers = [nls.localize('language', "Languages"), nls.localize('setting', "Setting"), nls.localize('default override value', "Override Value")];
488
const rows: IRowData[][] = [];
489
490
for (const key of Object.keys(configurationDefaults).sort((a, b) => a.localeCompare(b))) {
491
const value = configurationDefaults[key];
492
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
493
const languages = overrideIdentifiersFromKey(key);
494
const languageMarkdown = new MarkdownString().appendMarkdown(`${languages.join(', ')}`);
495
for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
496
const row: IRowData[] = [];
497
row.push(languageMarkdown);
498
row.push(new MarkdownString().appendMarkdown(`\`${key}\``));
499
row.push(new MarkdownString().appendCodeblock('json', JSON.stringify(value[key], null, 2)));
500
rows.push(row);
501
}
502
} else {
503
const row: IRowData[] = [];
504
row.push('');
505
row.push(new MarkdownString().appendMarkdown(`\`${key}\``));
506
row.push(new MarkdownString().appendCodeblock('json', JSON.stringify(value, null, 2)));
507
rows.push(row);
508
}
509
}
510
511
return {
512
data: {
513
headers,
514
rows
515
},
516
dispose: () => { }
517
};
518
}
519
}
520
521
Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({
522
id: 'configurationDefaults',
523
label: nls.localize('settings default overrides', "Settings Default Overrides"),
524
access: {
525
canToggle: false
526
},
527
renderer: new SyncDescriptor(ConfigurationDefaultsTableRenderer),
528
});
529
530