Support additionalProperties

This commit is contained in:
Aditya Thakral 2020-06-10 10:55:28 -04:00
parent fe91c2b910
commit 3431da2b80
6 changed files with 109 additions and 83 deletions

View file

@ -49,8 +49,8 @@
},
"emmet.includeLanguages": {
"type": "object",
"patternProperties": {
".*": { "type": "string" }
"additionalProperties": {
"type": "string"
},
"default": {},
"markdownDescription": "%emmetIncludeLanguages%"

View file

@ -238,9 +238,9 @@ configurationRegistry.registerConfiguration({
[FILES_ASSOCIATIONS_CONFIG]: {
'type': 'object',
'markdownDescription': nls.localize('associations', "Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed."),
'patternProperties': {
'.*': { type: 'string' },
},
'additionalProperties': {
'type': 'string'
}
},
'files.encoding': {
'type': 'string',

View file

@ -93,67 +93,81 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData
{ ...element.defaultValue, ...element.scopeValue } :
element.defaultValue;
let items: IObjectDataItem[] = [];
const items: IObjectDataItem[] = [];
if (element.setting.objectProperties) {
const wellDefinedKeys = Object.keys(element.setting.objectProperties);
const keyOptions = wellDefinedKeys.map(value => ({ value }));
const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting;
items = items.concat(
wellDefinedKeys
.filter(key => !!data[key])
.map(key => {
const valueEnumOptions = getEnumOptionsFromSchema(element.setting.objectProperties![key]);
return {
key: {
type: 'enum',
data: key,
options: keyOptions,
},
value: {
type: valueEnumOptions.length > 0 ? 'enum' : 'string',
data: data[key],
options: valueEnumOptions,
},
};
})
);
}
if (element.setting.objectPatternProperties) {
const patternsAndSchemas = Object
.entries(element.setting.objectPatternProperties)
.map(([pattern, schema]) => ({
pattern: new RegExp(pattern),
schema
}));
const keysWithSchema = Object.keys(data)
.filter(key => !!data[key] && !(key in (element.setting.objectProperties ?? {})))
.map(key => {
const patternAndSchema = patternsAndSchemas.find(({ pattern }) => pattern.test(key));
return patternAndSchema
? { key, schema: patternAndSchema.schema }
: undefined;
})
.filter(isDefined);
items = items.concat(keysWithSchema.map(({ key, schema }) => {
const valueEnumOptions = getEnumOptionsFromSchema(schema);
return {
key: { type: 'string', data: key },
value: {
type: valueEnumOptions.length > 0 ? 'enum' : 'string',
data: data[key],
options: valueEnumOptions,
}
};
const patternsAndSchemas = Object
.entries(objectPatternProperties ?? {})
.map(([pattern, schema]) => ({
pattern: new RegExp(pattern),
schema
}));
}
// TODO @9at8: What should we do if properties don't match?
const allKeys = new Set<string>(Object.keys(data).concat(Object.keys(objectProperties ?? {})));
const wellDefinedKeys: string[] = [];
const patternKeysWithSchema = new Map<string, IJSONSchema>();
const additionalKeys: string[] = [];
const additionalValueEnums = getEnumOptionsFromSchema(
typeof objectAdditionalProperties === 'boolean'
? {}
: objectAdditionalProperties ?? {}
);
// copy the keys into appropriate buckets
allKeys.forEach(key => {
if (key in (objectProperties ?? {})) {
wellDefinedKeys.push(key);
return;
}
const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema;
if (isDefined(schema)) {
patternKeysWithSchema.set(key, schema);
} else {
additionalKeys.push(key);
}
});
wellDefinedKeys.forEach(key => {
const valueEnumOptions = getEnumOptionsFromSchema(objectProperties![key]);
items.push({
key: {
type: 'enum',
data: key,
options: wellDefinedKeys.map(value => ({ value })),
},
value: {
type: valueEnumOptions.length > 0 ? 'enum' : 'string',
data: data[key],
options: valueEnumOptions,
},
});
});
patternKeysWithSchema.forEach((schema, key) => {
const valueEnumOptions = getEnumOptionsFromSchema(schema);
items.push({
key: { type: 'string', data: key },
value: {
type: valueEnumOptions.length > 0 ? 'enum' : 'string',
data: data[key],
options: valueEnumOptions,
},
});
});
additionalKeys.forEach(key => {
items.push({
key: { type: 'string', data: key },
value: {
type: additionalValueEnums.length > 0 ? 'enum' : 'string',
data: data[key],
options: additionalValueEnums,
},
});
});
return items;
}
@ -984,20 +998,9 @@ export class SettingObjectRenderer extends AbstractSettingRenderer implements IT
newValue[e.item.key.data] = e.item.value.data;
}
function sortKeys<T extends object>(obj: T) {
const sortedKeys = Object.keys(obj)
.sort((a, b) => a.localeCompare(b)) as Array<keyof T>;
const retVal: Partial<T> = {};
for (const key of sortedKeys) {
retVal[key] = obj[key];
}
return retVal;
}
this._onDidChangeSetting.fire({
key: template.context.setting.key,
value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue),
value: Object.keys(newValue).length === 0 ? undefined : newValue,
type: template.context.valueType
});
}

View file

@ -15,7 +15,7 @@ import { MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/pr
import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { FOLDER_SCOPES, WORKSPACE_SCOPES, REMOTE_MACHINE_SCOPES, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration';
import { IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices';
@ -468,21 +468,41 @@ export function isExcludeSetting(setting: ISetting): boolean {
setting.key === 'files.watcherExclude';
}
function isObjectRenderableSchemaMap(schemaMap: IJSONSchemaMap): boolean {
return Object.values(schemaMap).every(({ type }) => type === 'string');
function isObjectRenderableSchema({ type }: IJSONSchema): boolean {
return type === 'string';
}
function isObjectSetting(setting: ISetting): boolean {
if (setting.type !== 'object') {
function isObjectSetting({
type,
objectProperties,
objectPatternProperties,
objectAdditionalProperties
}: ISetting): boolean {
if (type !== 'object') {
return false;
}
if (isUndefinedOrNull(setting.objectProperties) && isUndefinedOrNull(setting.objectPatternProperties)) {
// object can have any shape
if (
isUndefinedOrNull(objectProperties) &&
isUndefinedOrNull(objectPatternProperties) &&
isUndefinedOrNull(objectAdditionalProperties)
) {
return false;
}
return isObjectRenderableSchemaMap(setting.objectProperties ?? {})
&& isObjectRenderableSchemaMap(setting.objectPatternProperties ?? {});
// object additional properties allow it to have any shape
if (objectAdditionalProperties === true) {
return false;
}
return Object.values(objectProperties ?? {}).every(isObjectRenderableSchema) &&
Object.values(objectPatternProperties ?? {}).every(isObjectRenderableSchema) &&
(
typeof objectAdditionalProperties === 'object'
? isObjectRenderableSchema(objectAdditionalProperties)
: true
);
}
function settingTypeEnumRenderable(_type: string | string[]) {

View file

@ -7,7 +7,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { IJSONSchemaMap, IJSONSchema } from 'vs/base/common/jsonSchema';
import { ITextModel } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
@ -68,6 +68,7 @@ export interface ISetting {
arrayItemType?: string;
objectProperties?: IJSONSchemaMap,
objectPatternProperties?: IJSONSchemaMap,
objectAdditionalProperties?: boolean | IJSONSchema,
enum?: string[];
enumDescriptions?: string[];
enumDescriptionsAreMarkdown?: boolean;

View file

@ -622,6 +622,7 @@ export class DefaultSettings extends Disposable {
const objectProperties = prop.type === 'object' ? prop.properties : undefined;
const objectPatternProperties = prop.type === 'object' ? prop.patternProperties : undefined;
const objectAdditionalProperties = prop.type === 'object' ? prop.additionalProperties : undefined;
result.push({
key,
@ -638,6 +639,7 @@ export class DefaultSettings extends Disposable {
arrayItemType: listItemType,
objectProperties,
objectPatternProperties,
objectAdditionalProperties,
enum: prop.enum,
enumDescriptions: prop.enumDescriptions || prop.markdownEnumDescriptions,
enumDescriptionsAreMarkdown: !prop.enumDescriptions,