Path: blob/main/src/vs/workbench/api/common/configurationExtensionPoint.ts
5239 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, EXTENSION_UNIFICATION_EXTENSION_IDS, overrideIdentifiersFromKey } 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',122enum: [123'accessibility',124'advanced',125'experimental',126'telemetry',127'usesOnlineServices',128],129enumDescriptions: [130nls.localize('accessibility', 'Accessibility settings'),131nls.localize('advanced', 'Advanced settings are hidden by default in the Settings editor unless the user chooses to show advanced settings.'),132nls.localize('experimental', 'Experimental settings are subject to change and may be removed in future releases.'),133nls.localize('preview', 'Preview settings can be used to try out new features before they are finalized.'),134nls.localize('telemetry', 'Telemetry settings'),135nls.localize('usesOnlineServices', 'Settings that use online services')136],137},138additionalItems: true,139markdownDescription: 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`.'),140}141}142}143]144}145}146}147};148149// build up a delta across two ext points and only apply it once150let _configDelta: IConfigurationDelta | undefined;151152153// BEGIN VSCode extension point `configurationDefaults`154const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IStringDictionary<IStringDictionary<unknown>>>({155extensionPoint: 'configurationDefaults',156jsonSchema: {157$ref: configurationDefaultsSchemaId,158},159canHandleResolver: true160});161defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {162163if (_configDelta) {164// HIGHLY unlikely, but just in case165configurationRegistry.deltaConfiguration(_configDelta);166}167168const configNow = _configDelta = {};169// schedule a HIGHLY unlikely task in case only the default configurations EXT point changes170queueMicrotask(() => {171if (_configDelta === configNow) {172configurationRegistry.deltaConfiguration(_configDelta);173_configDelta = undefined;174}175});176177if (removed.length) {178const removedDefaultConfigurations = removed.map<IConfigurationDefaults>(extension => ({ overrides: objects.deepClone(extension.value), source: { id: extension.description.identifier.value, displayName: extension.description.displayName } }));179_configDelta.removedDefaults = removedDefaultConfigurations;180}181if (added.length) {182const registeredProperties = configurationRegistry.getConfigurationProperties();183const allowedScopes = [ConfigurationScope.MACHINE_OVERRIDABLE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];184const addedDefaultConfigurations = added.map<IConfigurationDefaults>(extension => {185const overrides = objects.deepClone(extension.value);186for (const key of Object.keys(overrides)) {187const registeredPropertyScheme = registeredProperties[key];188if (registeredPropertyScheme?.disallowConfigurationDefault) {189extension.collector.warn(nls.localize('config.property.preventDefaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. This setting does not allow contributing configuration defaults.", key));190delete overrides[key];191continue;192}193if (!OVERRIDE_PROPERTY_REGEX.test(key)) {194if (registeredPropertyScheme?.scope && !allowedScopes.includes(registeredPropertyScheme.scope)) {195extension.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));196delete overrides[key];197continue;198}199}200}201return { overrides, source: { id: extension.description.identifier.value, displayName: extension.description.displayName } };202});203_configDelta.addedDefaults = addedDefaultConfigurations;204}205});206// END VSCode extension point `configurationDefaults`207208209// BEGIN VSCode extension point `configuration`210const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>({211extensionPoint: 'configuration',212deps: [defaultConfigurationExtPoint],213jsonSchema: {214description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),215oneOf: [216configurationEntrySchema,217{218type: 'array',219items: configurationEntrySchema220}221]222},223canHandleResolver: true224});225226const extensionConfigurations: ExtensionIdentifierMap<IConfigurationNode[]> = new ExtensionIdentifierMap<IConfigurationNode[]>();227228configurationExtPoint.setHandler((extensions, { added, removed }) => {229230// HIGHLY unlikely (only configuration but not defaultConfiguration EXT point changes)231_configDelta ??= {};232233if (removed.length) {234const removedConfigurations: IConfigurationNode[] = [];235for (const extension of removed) {236removedConfigurations.push(...(extensionConfigurations.get(extension.description.identifier) || []));237extensionConfigurations.delete(extension.description.identifier);238}239_configDelta.removedConfigurations = removedConfigurations;240}241242const seenProperties = new Set<string>();243244function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<unknown>): IConfigurationNode {245const configuration = objects.deepClone(node);246247if (configuration.title && (typeof configuration.title !== 'string')) {248extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));249}250251validateProperties(configuration, extension);252253configuration.id = node.id || extension.description.identifier.value;254configuration.extensionInfo = { id: extension.description.identifier.value, displayName: extension.description.displayName };255configuration.restrictedProperties = extension.description.capabilities?.untrustedWorkspaces?.supported === 'limited' ? extension.description.capabilities?.untrustedWorkspaces.restrictedConfigurations : undefined;256configuration.title = configuration.title || extension.description.displayName || extension.description.identifier.value;257return configuration;258}259260function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<unknown>): void {261const properties = configuration.properties;262const extensionConfigurationPolicy = product.extensionConfigurationPolicy;263if (properties) {264if (typeof properties !== 'object') {265extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));266configuration.properties = {};267}268for (const key in properties) {269const propertyConfiguration = properties[key];270const message = validateProperty(key, propertyConfiguration, extension.description.identifier.value);271if (message) {272delete properties[key];273extension.collector.warn(message);274continue;275}276if (seenProperties.has(key) && !EXTENSION_UNIFICATION_EXTENSION_IDS.has(extension.description.identifier.value.toLowerCase())) {277delete properties[key];278extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key));279continue;280}281if (!isObject(propertyConfiguration)) {282delete properties[key];283extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key));284continue;285}286if (extensionConfigurationPolicy?.[key]) {287propertyConfiguration.policy = extensionConfigurationPolicy?.[key];288}289if (propertyConfiguration.tags?.some(tag => tag.toLowerCase() === 'onexp')) {290propertyConfiguration.experiment = {291mode: 'startup'292};293}294seenProperties.add(key);295propertyConfiguration.scope = propertyConfiguration.scope ? parseScope(propertyConfiguration.scope.toString()) : ConfigurationScope.WINDOW;296}297}298const subNodes = configuration.allOf;299if (subNodes) {300extension.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."));301for (const node of subNodes) {302validateProperties(node, extension);303}304}305}306307if (added.length) {308const addedConfigurations: IConfigurationNode[] = [];309for (const extension of added) {310const configurations: IConfigurationNode[] = [];311const value = <IConfigurationNode | IConfigurationNode[]>extension.value;312if (Array.isArray(value)) {313value.forEach(v => configurations.push(handleConfiguration(v, extension)));314} else {315configurations.push(handleConfiguration(value, extension));316}317extensionConfigurations.set(extension.description.identifier, configurations);318addedConfigurations.push(...configurations);319}320321_configDelta.addedConfigurations = addedConfigurations;322}323324configurationRegistry.deltaConfiguration(_configDelta);325_configDelta = undefined;326});327// END VSCode extension point `configuration`328329jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', {330allowComments: true,331allowTrailingCommas: true,332default: {333folders: [334{335path: ''336}337],338settings: {339}340},341required: ['folders'],342properties: {343'folders': {344minItems: 0,345uniqueItems: true,346description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."),347items: {348type: 'object',349defaultSnippets: [{ body: { path: '$1' } }],350oneOf: [{351properties: {352path: {353type: 'string',354description: 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.")355},356name: {357type: 'string',358description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")359}360},361required: ['path']362}, {363properties: {364uri: {365type: 'string',366description: nls.localize('workspaceConfig.uri.description', "URI of the folder")367},368name: {369type: 'string',370description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")371}372},373required: ['uri']374}]375}376},377'settings': {378type: 'object',379default: {},380description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),381$ref: workspaceSettingsSchemaId382},383'launch': {384type: 'object',385default: { configurations: [], compounds: [] },386description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"),387$ref: launchSchemaId388},389'tasks': {390type: 'object',391default: { version: '2.0.0', tasks: [] },392description: nls.localize('workspaceConfig.tasks.description', "Workspace task configurations"),393$ref: tasksSchemaId394},395'mcp': {396type: 'object',397default: {398inputs: [],399servers: {400'mcp-server-time': {401command: 'uvx',402args: ['mcp_server_time', '--local-timezone=America/Los_Angeles']403}404}405},406description: nls.localize('workspaceConfig.mcp.description', "Model Context Protocol server configurations"),407$ref: mcpSchemaId408},409'extensions': {410type: 'object',411default: {},412description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),413$ref: 'vscode://schemas/extensions'414},415'remoteAuthority': {416type: 'string',417doNotSuggest: true,418description: nls.localize('workspaceConfig.remoteAuthority', "The remote server where the workspace is located."),419},420'transient': {421type: 'boolean',422doNotSuggest: true,423description: nls.localize('workspaceConfig.transient', "A transient workspace will disappear when restarting or reloading."),424}425},426errorMessage: nls.localize('unknownWorkspaceProperty', "Unknown workspace configuration property")427});428429430class SettingsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {431432readonly type = 'table';433434shouldRender(manifest: IExtensionManifest): boolean {435return !!manifest.contributes?.configuration;436}437438render(manifest: IExtensionManifest): IRenderedData<ITableData> {439const configuration: IConfigurationNode[] = manifest.contributes?.configuration440? Array.isArray(manifest.contributes.configuration) ? manifest.contributes.configuration : [manifest.contributes.configuration]441: [];442443const properties = getAllConfigurationProperties(configuration);444445const contrib = properties ? Object.keys(properties) : [];446const headers = [nls.localize('setting name', "ID"), nls.localize('description', "Description"), nls.localize('default', "Default")];447const rows: IRowData[][] = contrib.sort((a, b) => a.localeCompare(b))448.map(key => {449return [450new MarkdownString().appendMarkdown(`\`${key}\``),451properties[key].markdownDescription ? new MarkdownString(properties[key].markdownDescription, false) : properties[key].description ?? '',452new MarkdownString().appendCodeblock('json', JSON.stringify(isUndefined(properties[key].default) ? getDefaultValue(properties[key].type) : properties[key].default, null, 2)),453];454});455456return {457data: {458headers,459rows460},461dispose: () => { }462};463}464}465466Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({467id: 'configuration',468label: nls.localize('settings', "Settings"),469access: {470canToggle: false471},472renderer: new SyncDescriptor(SettingsTableRenderer),473});474475class ConfigurationDefaultsTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {476477readonly type = 'table';478479shouldRender(manifest: IExtensionManifest): boolean {480return !!manifest.contributes?.configurationDefaults;481}482483render(manifest: IExtensionManifest): IRenderedData<ITableData> {484const configurationDefaults = manifest.contributes?.configurationDefaults ?? {};485486const headers = [nls.localize('language', "Languages"), nls.localize('setting', "Setting"), nls.localize('default override value', "Override Value")];487const rows: IRowData[][] = [];488489for (const key of Object.keys(configurationDefaults).sort((a, b) => a.localeCompare(b))) {490const value = configurationDefaults[key];491if (OVERRIDE_PROPERTY_REGEX.test(key)) {492const languages = overrideIdentifiersFromKey(key);493const languageMarkdown = new MarkdownString().appendMarkdown(`${languages.join(', ')}`);494for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b))) {495const row: IRowData[] = [];496row.push(languageMarkdown);497row.push(new MarkdownString().appendMarkdown(`\`${key}\``));498row.push(new MarkdownString().appendCodeblock('json', JSON.stringify(value[key], null, 2)));499rows.push(row);500}501} else {502const row: IRowData[] = [];503row.push('');504row.push(new MarkdownString().appendMarkdown(`\`${key}\``));505row.push(new MarkdownString().appendCodeblock('json', JSON.stringify(value, null, 2)));506rows.push(row);507}508}509510return {511data: {512headers,513rows514},515dispose: () => { }516};517}518}519520Registry.as<IExtensionFeaturesRegistry>(ExtensionFeaturesExtensions.ExtensionFeaturesRegistry).registerExtensionFeature({521id: 'configurationDefaults',522label: nls.localize('settings default overrides', "Settings Default Overrides"),523access: {524canToggle: false525},526renderer: new SyncDescriptor(ConfigurationDefaultsTableRenderer),527});528529530