Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/configurationExtensionPoint.ts
3296 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 } 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
},
124
markdownDescription: nls.localize('scope.tags', 'A list of categories under which to place the setting. The category 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`.'),
125
}
126
}
127
}
128
]
129
}
130
}
131
}
132
};
133
134
// build up a delta across two ext points and only apply it once
135
let _configDelta: IConfigurationDelta | undefined;
136
137
138
// BEGIN VSCode extension point `configurationDefaults`
139
const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({
140
extensionPoint: 'configurationDefaults',
141
jsonSchema: {
142
$ref: configurationDefaultsSchemaId,
143
},
144
canHandleResolver: true
145
});
146
defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {
147
148
if (_configDelta) {
149
// HIGHLY unlikely, but just in case
150
configurationRegistry.deltaConfiguration(_configDelta);
151
}
152
153
const configNow = _configDelta = {};
154
// schedule a HIGHLY unlikely task in case only the default configurations EXT point changes
155
queueMicrotask(() => {
156
if (_configDelta === configNow) {
157
configurationRegistry.deltaConfiguration(_configDelta);
158
_configDelta = undefined;
159
}
160
});
161
162
if (removed.length) {
163
const removedDefaultConfigurations = removed.map<IConfigurationDefaults>(extension => ({ overrides: objects.deepClone(extension.value), source: { id: extension.description.identifier.value, displayName: extension.description.displayName } }));
164
_configDelta.removedDefaults = removedDefaultConfigurations;
165
}
166
if (added.length) {
167
const registeredProperties = configurationRegistry.getConfigurationProperties();
168
const allowedScopes = [ConfigurationScope.MACHINE_OVERRIDABLE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];
169
const addedDefaultConfigurations = added.map<IConfigurationDefaults>(extension => {
170
const overrides: IStringDictionary<any> = objects.deepClone(extension.value);
171
for (const key of Object.keys(overrides)) {
172
const registeredPropertyScheme = registeredProperties[key];
173
if (registeredPropertyScheme?.disallowConfigurationDefault) {
174
extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key));
175
delete overrides[key];
176
continue;
177
}
178
if (!OVERRIDE_PROPERTY_REGEX.test(key)) {
179
if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) {
180
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));
181
delete overrides[key];
182
continue;
183
}
184
}
185
}
186
return { overrides, source: { id: extension.description.identifier.value, displayName: extension.description.displayName } };
187
});
188
_configDelta.addedDefaults = addedDefaultConfigurations;
189
}
190
});
191
// END VSCode extension point `configurationDefaults`
192
193
194
// BEGIN VSCode extension point `configuration`
195
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({
196
extensionPoint: 'configuration',
197
deps: [defaultConfigurationExtPoint],
198
jsonSchema: {
199
description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
200
oneOf: [
201
configurationEntrySchema,
202
{
203
type: 'array',
204
items: configurationEntrySchema
205
}
206
]
207
},
208
canHandleResolver: true
209
});
210
211
const extensionConfigurations: ExtensionIdentifierMap<IConfigurationNode[]> = new ExtensionIdentifierMap<IConfigurationNode[]>();
212
213
configurationExtPoint.setHandler((extensions, { added, removed }) => {
214
215
// HIGHLY unlikely (only configuration but not defaultConfiguration EXT point changes)
216
_configDelta ??= {};
217
218
if (removed.length) {
219
const removedConfigurations: IConfigurationNode[] = [];
220
for (const extension of removed) {
221
removedConfigurations.push(...(extensionConfigurations.get(extension.description.identifier) || []));
222
extensionConfigurations.delete(extension.description.identifier);
223
}
224
_configDelta.removedConfigurations = removedConfigurations;
225
}
226
227
const seenProperties = new Set<string>();
228
229
function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<any>): IConfigurationNode {
230
const configuration = objects.deepClone(node);
231
232
if (configuration.title && (typeof configuration.title !== 'string')) {
233
extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));
234
}
235
236
validateProperties(configuration, extension);
237
238
configuration.id = node.id || extension.description.identifier.value;
239
configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName };
240
configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined;
241
configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value;
242
return configuration;
243
}
244
245
function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<any>): void {
246
const properties = configuration.properties;
247
const extensionConfigurationPolicy = product.extensionConfigurationPolicy;
248
if (properties) {
249
if (typeof properties !== 'object') {
250
extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));
251
configuration.properties = {};
252
}
253
for (const key in properties) {
254
const propertyConfiguration = properties[key];
255
const message = validateProperty(key, propertyConfiguration);
256
if (message) {
257
delete properties[key];
258
extension.collector.warn(message);
259
continue;
260
}
261
if (seenProperties.has(key)) {
262
delete properties[key];
263
extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key));
264
continue;
265
}
266
if (!isObject(propertyConfiguration)) {
267
delete properties[key];
268
extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key));
269
continue;
270
}
271
if (extensionConfigurationPolicy?.[key]) {
272
propertyConfiguration.policy = extensionConfigurationPolicy?.[key];
273
}
274
if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) {
275
propertyConfiguration.experiment = {
276
mode: 'startup'
277
};
278
}
279
seenProperties.add(key);
280
propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW;
281
}
282
}
283
const subNodes = configuration.allOf;
284
if (subNodes) {
285
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."));
286
for (const node of subNodes) {
287
validateProperties(node, extension);
288
}
289
}
290
}
291
292
if (added.length) {
293
const addedConfigurations: IConfigurationNode[] = [];
294
for (const extension of added) {
295
const configurations: IConfigurationNode[] = [];
296
const value = <IConfigurationNode | IConfigurationNode[]>extension.value;
297
if (Array.isArray(value)) {
298
value.forEach(v => configurations.push(handleConfiguration(v, extension)));
299
} else {
300
configurations.push(handleConfiguration(value, extension));
301
}
302
extensionConfigurations.set(extension.description.identifier, configurations);
303
addedConfigurations.push(...configurations);
304
}
305
306
_configDelta.addedConfigurations = addedConfigurations;
307
}
308
309
configurationRegistry.deltaConfiguration(_configDelta);
310
_configDelta = undefined;
311
});
312
// END VSCode extension point `configuration`
313
314
jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', {
315
allowComments: true,
316
allowTrailingCommas: true,
317
default: {
318
folders: [
319
{
320
path: ''
321
}
322
],
323
settings: {
324
}
325
},
326
required: ['folders'],
327
properties: {
328
'folders': {
329
minItems: 0,
330
uniqueItems: true,
331
description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."),
332
items: {
333
type: 'object',
334
defaultSnippets: [{ body: { path: '$1' } }],
335
oneOf: [{
336
properties: {
337
path: {
338
type: 'string',
339
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.")
340
},
341
name: {
342
type: 'string',
343
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
344
}
345
},
346
required: ['path']
347
}, {
348
properties: {
349
uri: {
350
type: 'string',
351
description: nls.localize('workspaceConfig.uri.description', "URI of the folder")
352
},
353
name: {
354
type: 'string',
355
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
356
}
357
},
358
required: ['uri']
359
}]
360
}
361
},
362
'settings': {
363
type: 'object',
364
default: {},
365
description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
366
$ref: workspaceSettingsSchemaId
367
},
368
'launch': {
369
type: 'object',
370
default: { configurations: [], compounds: [] },
371
description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"),
372
$ref: launchSchemaId
373
},
374
'tasks': {
375
type: 'object',
376
default: { version: '2.0.0', tasks: [] },
377
description: nls.localize('workspaceConfig.tasks.description', "Workspace task configurations"),
378
$ref: tasksSchemaId
379
},
380
'mcp': {
381
type: 'object',
382
default: {
383
inputs: [],
384
servers: {
385
'mcp-server-time': {
386
command: 'uvx',
387
args: ['mcp_server_time', '--local-timezone=America/Los_Angeles']
388
}
389
}
390
},
391
description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),
392
$ref: mcpSchemaId
393
},
394
'extensions': {
395
type: 'object',
396
default: {},
397
description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),
398
$ref: 'vscode://schemas/extensions'
399
},
400
'remoteAuthority': {
401
type: 'string',
402
doNotSuggest: true,
403
description: nls.localize('workspaceConfig.remoteAuthority', "The remote server where the workspace is located."),
404
},
405
'transient': {
406
type: 'boolean',
407
doNotSuggest: true,
408
description: nls.localize('workspaceConfig.transient', "A transient workspace will disappear when restarting or reloading."),
409
}
410
},
411
errorMessage: nls.localize('unknownWorkspaceProperty', "Unknown workspace configuration property")
412
});
413
414
415
class SettingsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {
416
417
readonly type = 'table';
418
419
shouldRender(manifest: IExtensionManifest): boolean {
420
return !!manifest.contributes?.configuration;
421
}
422
423
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
424
const configuration: IConfigurationNode[] = manifest.contributes?.configuration
425
? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]
426
: [];
427
428
const properties = getAllConfigurationProperties(configuration);
429
430
const contrib = properties ? Object.keys(properties) : [];
431
const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")];
432
const rows: IRowData[][] = contrib.sort((a, b) => a.localeCompare(b))
433
.map(key => {
434
return [
435
new MarkdownString().appendMarkdown(`\`${key}\``),
436
properties[key].markdownDescription ? new MarkdownString(properties[key].markdownDescription, false) : properties[key].description ?? '',
437
new MarkdownString().appendCodeblock('json', JSON.stringify(isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default, null, 2)),
438
];
439
});
440
441
return {
442
data: {
443
headers,
444
rows
445
},
446
dispose: () => { }
447
};
448
}
449
}
450
451
Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({
452
id: 'configuration',
453
label: nls.localize('settings', "Settings"),
454
access: {
455
canToggle: false
456
},
457
renderer: new SyncDescriptor(SettingsTableRenderer),
458
});
459
460