Skip to content

Commit

Permalink
Allow multiple extensions to provide default values for object settings
Browse files Browse the repository at this point in the history
  • Loading branch information
benibenj committed Jun 24, 2024
1 parent 62fbb97 commit ec6cdfd
Show file tree
Hide file tree
Showing 9 changed files with 413 additions and 153 deletions.
103 changes: 89 additions & 14 deletions src/vs/platform/configuration/common/configurationRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,21 +233,24 @@ export interface IConfigurationNode {
restrictedProperties?: string[];
}

export type ConfigurationDefaultSource = IExtensionInfo | string;
export type ConfigurationDefaultValueSource = ConfigurationDefaultSource | Map<string, ConfigurationDefaultSource>;

export interface IConfigurationDefaults {
overrides: IStringDictionary<any>;
source?: IExtensionInfo | string;
source?: ConfigurationDefaultSource;
}

export type IRegisteredConfigurationPropertySchema = IConfigurationPropertySchema & {
defaultDefaultValue?: any;
source?: IExtensionInfo; // Source of the Property
defaultValueSource?: IExtensionInfo | string; // Source of the Default Value
defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value
};

export type IConfigurationDefaultOverride = {
readonly value: any;
readonly source?: IExtensionInfo | string; // Source of the default override
readonly valuesSources?: Map<string, IExtensionInfo | string>; // Source of each value in default language overrides
readonly source?: ConfigurationDefaultValueSource; // Source of the default override
readonly valuesSources?: Map<string, ConfigurationDefaultValueSource>; // Source of each value in default language overrides
};

export const allSettings: { properties: IStringDictionary<IConfigurationPropertySchema>; patternProperties: IStringDictionary<IConfigurationPropertySchema> } = { properties: {}, patternProperties: {} };
Expand Down Expand Up @@ -351,13 +354,38 @@ class ConfigurationRegistry implements IConfigurationRegistry {

if (OVERRIDE_PROPERTY_REGEX.test(key)) {
const configurationDefaultOverride = this.configurationDefaultsOverrides.get(key);
const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map<string, IExtensionInfo | string>();
if (source) {
for (const configuration of Object.keys(overrides[key])) {
valuesSources.set(configuration, source);
const valuesSources = configurationDefaultOverride?.valuesSources ?? new Map<string, ConfigurationDefaultValueSource>();

const defaultValue = configurationDefaultOverride?.value || {};
for (const configuration of Object.keys(overrides[key])) {
const overrideValue = overrides[key][configuration];

const isObjectSetting = types.isObject(overrideValue) && (types.isUndefined(defaultValue[configuration] || types.isObject(defaultValue[configuration])));
if (isObjectSetting) {
// Objects are merged instead of overridden
defaultValue[configuration] = { ...(defaultValue[configuration] ?? {}), ...overrideValue };

// Track the source of each value in the object
if (source) {
let objectConfigurationSources = valuesSources.get(configuration) as Map<string, ConfigurationDefaultSource> | undefined;
if (!objectConfigurationSources) {
objectConfigurationSources = new Map<string, ConfigurationDefaultSource>();
valuesSources.set(configuration, objectConfigurationSources);
}

for (const objectKey in overrideValue) {
objectConfigurationSources.set(objectKey, source);
}
}
} else {
// Primitive values are overridden
defaultValue[configuration] = overrideValue;
if (source) {
valuesSources.set(configuration, source);
}
}
}
const defaultValue = { ...(configurationDefaultOverride?.value || {}), ...overrides[key] };

this.configurationDefaultsOverrides.set(key, { source, value: defaultValue, valuesSources });
const plainKey = getLanguageTagSettingPlainKey(key);
const property: IRegisteredConfigurationPropertySchema = {
Expand All @@ -373,8 +401,26 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this.configurationProperties[key] = property;
this.defaultLanguageConfigurationOverridesNode.properties![key] = property;
} else {
this.configurationDefaultsOverrides.set(key, { value: overrides[key], source });
const property = this.configurationProperties[key];
let defaultValue = overrides[key];
let defaultValueSource: ConfigurationDefaultValueSource | undefined = source;

// If the default value is an object, merge the objects and store the source of each keys
if (property.type === 'object' && types.isObject(overrides[key])) {
const objectDefaults = this.configurationDefaultsOverrides.get(key);
const existingDefaultValue = objectDefaults?.value ?? property.defaultDefaultValue ?? {};
defaultValue = { ...existingDefaultValue, ...overrides[key] };

if (source) {
defaultValueSource = objectDefaults?.source as Map<string, ConfigurationDefaultSource> ?? new Map<string, ConfigurationDefaultSource>();
for (const objectKey in overrides[key]) {
defaultValueSource.set(objectKey, source);
}
}
}

this.configurationDefaultsOverrides.set(key, { value: defaultValue, source: defaultValueSource });

if (property) {
this.updatePropertyDefaultValue(key, property);
this.updateSchema(key, property);
Expand All @@ -397,14 +443,43 @@ class ConfigurationRegistry implements IConfigurationRegistry {

for (const { overrides, source } of defaultConfigurations) {
for (const key in overrides) {
const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key);
const id = types.isString(source) ? source : source?.id;
const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride?.source) ? configurationDefaultsOverride?.source : configurationDefaultsOverride?.source?.id;
if (id !== configurationDefaultsOverrideSourceId) {

const configurationDefaultsOverride = this.configurationDefaultsOverrides.get(key);
if (!configurationDefaultsOverride || !configurationDefaultsOverride.source) {
continue;
}

// If the default value is an object, remove the source of each key
if (configurationDefaultsOverride.source instanceof Map) {

const keySources = configurationDefaultsOverride.source;
for (const objectKey in overrides[key]) {
const keySource = keySources.get(objectKey);
const keySourceId = types.isString(keySource) ? keySource : keySource?.id;

if (keySourceId === id) {
keySources.delete(objectKey);
delete configurationDefaultsOverride.value[objectKey];
}
}

if (keySources.size === 0) {
this.configurationDefaultsOverrides.delete(key);
}
}
// Otherwise, remove the default value if the source matches
else {
const configurationDefaultsOverrideSourceId = types.isString(configurationDefaultsOverride.source) ? configurationDefaultsOverride.source : configurationDefaultsOverride.source.id;
if (id !== configurationDefaultsOverrideSourceId) {
continue; // Another source is overriding this default value
}

this.configurationDefaultsOverrides.delete(key);
}

bucket.add(key);
this.configurationDefaultsOverrides.delete(key);

if (OVERRIDE_PROPERTY_REGEX.test(key)) {
delete this.configurationProperties[key];
delete this.defaultLanguageConfigurationOverridesNode.properties![key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ suite('ConfigurationRegistry', () => {
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['[lang]'].default, { a: 2, b: 2, c: 3 });
});

test('configuration defaults - overrides defaults', async () => {
test('configuration defaults - merge object default overrides', async () => {
configurationRegistry.registerConfiguration({
'id': '_test_default',
'type': 'object',
Expand All @@ -51,7 +51,7 @@ suite('ConfigurationRegistry', () => {
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 1, b: 2 } } }]);
configurationRegistry.registerDefaultConfigurations([{ overrides: { 'config': { a: 2, c: 3 } } }]);

assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 });
assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 });
});

test('registering multiple settings with same policy', async () => {
Expand Down Expand Up @@ -79,4 +79,32 @@ suite('ConfigurationRegistry', () => {
assert.ok(actual['policy1'] !== undefined);
assert.ok(actual['policy2'] === undefined);
});

test('configuration defaults - deregister merged object default override', async () => {
configurationRegistry.registerConfiguration({
'id': '_test_default',
'type': 'object',
'properties': {
'config': {
'type': 'object',
}
}
});

const overrides1 = [{ overrides: { 'config': { a: 1, b: 2 } }, source: 'source1' }];
const overrides2 = [{ overrides: { 'config': { a: 2, c: 3 } }, source: 'source2' }];

configurationRegistry.registerDefaultConfigurations(overrides1);
configurationRegistry.registerDefaultConfigurations(overrides2);

assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, b: 2, c: 3 });

configurationRegistry.deregisterDefaultConfigurations(overrides2);

assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { b: 2 }); // TODO this should actualy equal overrides1

configurationRegistry.deregisterDefaultConfigurations(overrides1);

assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, {});
});
});
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/workbench.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
})(),
additionalProperties:
{
type: 'string',
type: ['string', 'null'],
markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."),
minLength: 1,
pattern: '.*[a-zA-Z0-9].*'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ILanguageService } from 'vs/editor/common/languages/language';
Expand Down Expand Up @@ -448,15 +448,27 @@ export class SettingsTreeIndicatorsLabel implements IDisposable {

updateDefaultOverrideIndicator(element: SettingsTreeSettingElement) {
this.defaultOverrideIndicator.element.style.display = 'none';
const sourceToDisplay = getDefaultValueSourceToDisplay(element);
let sourceToDisplay = getDefaultValueSourceToDisplay(element);
if (sourceToDisplay !== undefined) {
this.defaultOverrideIndicator.element.style.display = 'inline';
this.defaultOverrideIndicator.disposables.clear();

const defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by {0}", sourceToDisplay);
// Show source of default value when hovered
if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) {
sourceToDisplay = sourceToDisplay[0];
}

let defaultOverrideHoverContent;
if (!Array.isArray(sourceToDisplay)) {
defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by `{0}`", sourceToDisplay);
} else {
sourceToDisplay = sourceToDisplay.map(source => `\`${source}\``);
defaultOverrideHoverContent = localize('multipledefaultOverriddenDetails', "A default values has been set by {0}", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1));
}

const showHover = (focus: boolean) => {
return this.hoverService.showHover({
content: defaultOverrideHoverContent,
content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent),
target: this.defaultOverrideIndicator.element,
position: {
hoverPosition: HoverPosition.BELOW,
Expand All @@ -473,14 +485,22 @@ export class SettingsTreeIndicatorsLabel implements IDisposable {
}
}

function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined {
let sourceToDisplay: string | undefined;
function getDefaultValueSourceToDisplay(element: SettingsTreeSettingElement): string | undefined | string[] {
let sourceToDisplay: string | undefined | string[];
const defaultValueSource = element.defaultValueSource;
if (defaultValueSource) {
if (typeof defaultValueSource !== 'string') {
sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id;
if (defaultValueSource instanceof Map) {
sourceToDisplay = [];
for (const [, value] of defaultValueSource) {
const newValue = typeof value !== 'string' ? value.displayName ?? value.id : value;
if (!sourceToDisplay.includes(newValue)) {
sourceToDisplay.push(newValue);
}
}
} else if (typeof defaultValueSource === 'string') {
sourceToDisplay = defaultValueSource;
} else {
sourceToDisplay = defaultValueSource.displayName ?? defaultValueSource.id;
}
}
return sourceToDisplay;
Expand Down Expand Up @@ -538,9 +558,19 @@ export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement,
}

// Add default override indicator text
const sourceToDisplay = getDefaultValueSourceToDisplay(element);
let sourceToDisplay = getDefaultValueSourceToDisplay(element);
if (sourceToDisplay !== undefined) {
ariaLabelSections.push(localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay));
if (Array.isArray(sourceToDisplay) && sourceToDisplay.length === 1) {
sourceToDisplay = sourceToDisplay[0];
}

let overriddenDetailsText;
if (!Array.isArray(sourceToDisplay)) {
overriddenDetailsText = localize('defaultOverriddenDetailsAriaLabel', "{0} overrides the default value", sourceToDisplay);
} else {
overriddenDetailsText = localize('multipleDefaultOverriddenDetailsAriaLabel', "{0} override the default value", sourceToDisplay.slice(0, -1).join(', ') + ' & ' + sourceToDisplay.slice(-1));
}
ariaLabelSections.push(overriddenDetailsText);
}

// Add text about default values being overridden in other languages
Expand Down
Loading

0 comments on commit ec6cdfd

Please sign in to comment.