Path: blob/main/src/vs/workbench/api/common/configurationExtensionPoint.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../nls.js';6import * as objects from '../../../base/common/objects.js';7import { Registry } from '../../../platform/registry/common/platform.js';8import { IJSONSchema } from '../../../base/common/jsonSchema.js';9import { ExtensionsRegistry, IExtensionPointUser } from '../../services/extensions/common/extensionsRegistry.js';10import { IConfigurationNode, IConfigurationRegistry, Extensions, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_REGEX, IConfigurationDefaults, configurationDefaultsSchemaId, IConfigurationDelta, getDefaultValue, getAllConfigurationProperties, parseScope } from '../../../platform/configuration/common/configurationRegistry.js';11import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../platform/jsonschemas/common/jsonContributionRegistry.js';12import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId, mcpSchemaId } from '../../services/configuration/common/configuration.js';13import { isObject, isUndefined } from '../../../base/common/types.js';14import { ExtensionIdentifierMap, IExtensionManifest } from '../../../platform/extensions/common/extensions.js';15import { IStringDictionary } from '../../../base/common/collections.js';16import { Extensions as ExtensionFeaturesExtensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from '../../services/extensionManagement/common/extensionFeatures.js';17import { Disposable } from '../../../base/common/lifecycle.js';18import { SyncDescriptor } from '../../../platform/instantiation/common/descriptors.js';19import { MarkdownString } from '../../../base/common/htmlContent.js';20import product from '../../../platform/product/common/product.js';2122const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);23const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);2425const configurationEntrySchema: IJSONSchema = {26type: 'object',27defaultSnippets: [{ body: { title: '', properties: {} } }],28properties: {29title: {30description: 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.'),31type: 'string'32},33order: {34description: nls.localize('vscode.extension.contributes.configuration.order', 'When specified, gives the order of this category of settings relative to other categories.'),35type: 'integer'36},37properties: {38description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),39type: 'object',40propertyNames: {41pattern: '\\S+',42patternErrorMessage: nls.localize('vscode.extension.contributes.configuration.property.empty', 'Property should not be empty.'),43},44additionalProperties: {45anyOf: [46{47title: nls.localize('vscode.extension.contributes.configuration.properties.schema', 'Schema of the configuration property.'),48$ref: 'http://json-schema.org/draft-07/schema#'49},50{51type: 'object',52properties: {53scope: {54type: 'string',55enum: ['application', 'machine', 'window', 'resource', 'language-overridable', 'machine-overridable'],56default: 'window',57enumDescriptions: [58nls.localize('scope.application.description', "Configuration that can be configured only in the user settings."),59nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings or only in the remote settings."),60nls.localize('scope.window.description', "Configuration that can be configured in the user, remote or workspace settings."),61nls.localize('scope.resource.description', "Configuration that can be configured in the user, remote, workspace or folder settings."),62nls.localize('scope.language-overridable.description', "Resource configuration that can be configured in language specific settings."),63nls.localize('scope.machine-overridable.description', "Machine configuration that can be configured also in workspace or folder settings.")64],65markdownDescription: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `application`, `machine`, `window`, `resource`, and `machine-overridable`.")66},67enumDescriptions: {68type: 'array',69items: {70type: 'string',71},72description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values')73},74markdownEnumDescriptions: {75type: 'array',76items: {77type: 'string',78},79description: nls.localize('scope.markdownEnumDescriptions', 'Descriptions for enum values in the markdown format.')80},81enumItemLabels: {82type: 'array',83items: {84type: 'string'85},86markdownDescription: 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`')87},88markdownDescription: {89type: 'string',90description: nls.localize('scope.markdownDescription', 'The description in the markdown format.')91},92deprecationMessage: {93type: 'string',94description: nls.localize('scope.deprecationMessage', 'If set, the property is marked as deprecated and the given message is shown as an explanation.')95},96markdownDeprecationMessage: {97type: 'string',98description: 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.')99},100editPresentation: {101type: 'string',102enum: ['singlelineText', 'multilineText'],103enumDescriptions: [104nls.localize('scope.singlelineText.description', 'The value will be shown in an inputbox.'),105nls.localize('scope.multilineText.description', 'The value will be shown in a textarea.')106],107default: 'singlelineText',108description: nls.localize('scope.editPresentation', 'When specified, controls the presentation format of the string setting.')109},110order: {111type: 'integer',112description: 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.')113},114ignoreSync: {115type: 'boolean',116description: nls.localize('scope.ignoreSync', 'When enabled, Settings Sync will not sync the user value of this configuration by default.')117},118tags: {119type: 'array',120items: {121type: 'string'122},123markdownDescription: 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`.'),124}125}126}127]128}129}130}131};132133// build up a delta across two ext points and only apply it once134let _configDelta: IConfigurationDelta | undefined;135136137// BEGIN VSCode extension point `configurationDefaults`138const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({139extensionPoint: 'configurationDefaults',140jsonSchema: {141$ref: configurationDefaultsSchemaId,142},143canHandleResolver: true144});145defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {146147if (_configDelta) {148// HIGHLY unlikely, but just in case149configurationRegistry.deltaConfiguration(_configDelta);150}151152const configNow = _configDelta = {};153// schedule a HIGHLY unlikely task in case only the default configurations EXT point changes154queueMicrotask(() => {155if (_configDelta === configNow) {156configurationRegistry.deltaConfiguration(_configDelta);157_configDelta = undefined;158}159});160161if (removed.length) {162const removedDefaultConfigurations = removed.map<IConfigurationDefaults>(extension => ({ overrides: objects.deepClone(extension.value), source: { id: extension.description.identifier.value, displayName: extension.description.displayName } }));163_configDelta.removedDefaults = removedDefaultConfigurations;164}165if (added.length) {166const registeredProperties = configurationRegistry.getConfigurationProperties();167const allowedScopes = [ConfigurationScope.MACHINE_OVERRIDABLE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];168const addedDefaultConfigurations = added.map<IConfigurationDefaults>(extension => {169const overrides: IStringDictionary<any> = objects.deepClone(extension.value);170for (const key of Object.keys(overrides)) {171const registeredPropertyScheme = registeredProperties[key];172if (registeredPropertyScheme?.disallowConfigurationDefault) {173extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key));174delete overrides[key];175continue;176}177if (!OVERRIDE_PROPERTY_REGEX.test(key)) {178if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) {179extension.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));180delete overrides[key];181continue;182}183}184}185return { overrides, source: { id: extension.description.identifier.value, displayName: extension.description.displayName } };186});187_configDelta.addedDefaults = addedDefaultConfigurations;188}189});190// END VSCode extension point `configurationDefaults`191192193// BEGIN VSCode extension point `configuration`194const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({195extensionPoint: 'configuration',196deps: [defaultConfigurationExtPoint],197jsonSchema: {198description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),199oneOf: [200configurationEntrySchema,201{202type: 'array',203items: configurationEntrySchema204}205]206},207canHandleResolver: true208});209210const extensionConfigurations: ExtensionIdentifierMap<IConfigurationNode[]> = new ExtensionIdentifierMap<IConfigurationNode[]>();211212configurationExtPoint.setHandler((extensions, { added, removed }) => {213214// HIGHLY unlikely (only configuration but not defaultConfiguration EXT point changes)215_configDelta ??= {};216217if (removed.length) {218const removedConfigurations: IConfigurationNode[] = [];219for (const extension of removed) {220removedConfigurations.push(...(extensionConfigurations.get(extension.description.identifier) || []));221extensionConfigurations.delete(extension.description.identifier);222}223_configDelta.removedConfigurations = removedConfigurations;224}225226const seenProperties = new Set<string>();227228function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<any>): IConfigurationNode {229const configuration = objects.deepClone(node);230231if (configuration.title && (typeof configuration.title !== 'string')) {232extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));233}234235validateProperties(configuration, extension);236237configuration.id = node.id || extension.description.identifier.value;238configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName };239configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined;240configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value;241return configuration;242}243244function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<any>): void {245const properties = configuration.properties;246const extensionConfigurationPolicy = product.extensionConfigurationPolicy;247if (properties) {248if (typeof properties !== 'object') {249extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));250configuration.properties = {};251}252for (const key in properties) {253const propertyConfiguration = properties[key];254const message = validateProperty(key, propertyConfiguration);255if (message) {256delete properties[key];257extension.collector.warn(message);258continue;259}260if (seenProperties.has(key)) {261delete properties[key];262extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key));263continue;264}265if (!isObject(propertyConfiguration)) {266delete properties[key];267extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key));268continue;269}270if (extensionConfigurationPolicy?.[key]) {271propertyConfiguration.policy = extensionConfigurationPolicy?.[key];272}273if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) {274propertyConfiguration.experiment = {275mode: 'startup'276};277}278seenProperties.add(key);279propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW;280}281}282const subNodes = configuration.allOf;283if (subNodes) {284extension.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."));285for (const node of subNodes) {286validateProperties(node, extension);287}288}289}290291if (added.length) {292const addedConfigurations: IConfigurationNode[] = [];293for (const extension of added) {294const configurations: IConfigurationNode[] = [];295const value = <IConfigurationNode | IConfigurationNode[]>extension.value;296if (Array.isArray(value)) {297value.forEach(v => configurations.push(handleConfiguration(v, extension)));298} else {299configurations.push(handleConfiguration(value, extension));300}301extensionConfigurations.set(extension.description.identifier, configurations);302addedConfigurations.push(...configurations);303}304305_configDelta.addedConfigurations = addedConfigurations;306}307308configurationRegistry.deltaConfiguration(_configDelta);309_configDelta = undefined;310});311// END VSCode extension point `configuration`312313jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', {314allowComments: true,315allowTrailingCommas: true,316default: {317folders: [318{319path: ''320}321],322settings: {323}324},325required: ['folders'],326properties: {327'folders': {328minItems: 0,329uniqueItems: true,330description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."),331items: {332type: 'object',333defaultSnippets: [{ body: { path: '$1' } }],334oneOf: [{335properties: {336path: {337type: 'string',338description: 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.")339},340name: {341type: 'string',342description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")343}344},345required: ['path']346}, {347properties: {348uri: {349type: 'string',350description: nls.localize('workspaceConfig.uri.description', "URI of the folder")351},352name: {353type: 'string',354description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")355}356},357required: ['uri']358}]359}360},361'settings': {362type: 'object',363default: {},364description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),365$ref: workspaceSettingsSchemaId366},367'launch': {368type: 'object',369default: { configurations: [], compounds: [] },370description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"),371$ref: launchSchemaId372},373'tasks': {374type: 'object',375default: { version: '2.0.0', tasks: [] },376description: nls.localize('workspaceConfig.tasks.description', "Workspace task configurations"),377$ref: tasksSchemaId378},379'mcp': {380type: 'object',381default: {382inputs: [],383servers: {384'mcp-server-time': {385command: 'uvx',386args: ['mcp_server_time', '--local-timezone=America/Los_Angeles']387}388}389},390description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),391$ref: mcpSchemaId392},393'extensions': {394type: 'object',395default: {},396description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),397$ref: 'vscode://schemas/extensions'398},399'remoteAuthority': {400type: 'string',401doNotSuggest: true,402description: nls.localize('workspaceConfig.remoteAuthority', "The remote server where the workspace is located."),403},404'transient': {405type: 'boolean',406doNotSuggest: true,407description: nls.localize('workspaceConfig.transient', "A transient workspace will disappear when restarting or reloading."),408}409},410errorMessage: nls.localize('unknownWorkspaceProperty', "Unknown workspace configuration property")411});412413414class SettingsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {415416readonly type = 'table';417418shouldRender(manifest: IExtensionManifest): boolean {419return !!manifest.contributes?.configuration;420}421422render(manifest: IExtensionManifest): IRenderedData<ITableData> {423const configuration: IConfigurationNode[] = manifest.contributes?.configuration424? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]425: [];426427const properties = getAllConfigurationProperties(configuration);428429const contrib = properties ? Object.keys(properties) : [];430const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")];431const rows: IRowData[][] = contrib.sort((a, b) => a.localeCompare(b))432.map(key => {433return [434new MarkdownString().appendMarkdown(`\`${key}\``),435properties[key].markdownDescription ? new MarkdownString(properties[key].markdownDescription, false) : properties[key].description ?? '',436new MarkdownString().appendCodeblock('json', JSON.stringify(isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default, null, 2)),437];438});439440return {441data: {442headers,443rows444},445dispose: () => { }446};447}448}449450Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({451id: 'configuration',452label: nls.localize('settings', "Settings"),453access: {454canToggle: false455},456renderer: new SyncDescriptor(SettingsTableRenderer),457});458459460