From 3431da2b80aeb47f492fc9f49850299248f6fdaf Mon Sep 17 00:00:00 2001 From: Aditya Thakral Date: Wed, 10 Jun 2020 10:55:28 -0400 Subject: [PATCH] Support additionalProperties --- extensions/emmet/package.json | 4 +- .../files/browser/files.contribution.ts | 6 +- .../preferences/browser/settingsTree.ts | 141 +++++++++--------- .../preferences/browser/settingsTreeModels.ts | 36 ++++- .../preferences/common/preferences.ts | 3 +- .../preferences/common/preferencesModels.ts | 2 + 6 files changed, 109 insertions(+), 83 deletions(-) diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index 2949ecaf42c..cb72a53e8a3 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -49,8 +49,8 @@ }, "emmet.includeLanguages": { "type": "object", - "patternProperties": { - ".*": { "type": "string" } + "additionalProperties": { + "type": "string" }, "default": {}, "markdownDescription": "%emmetIncludeLanguages%" diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 7841b4ef787..72c8c3ca3ef 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -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', diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index d75bdaf6d94..23927d89d43 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -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(Object.keys(data).concat(Object.keys(objectProperties ?? {}))); + const wellDefinedKeys: string[] = []; + const patternKeysWithSchema = new Map(); + 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(obj: T) { - const sortedKeys = Object.keys(obj) - .sort((a, b) => a.localeCompare(b)) as Array; - - const retVal: Partial = {}; - 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 }); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index 384503cf3c0..429ec670b5d 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -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[]) { diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 58f480727c3..0d414997afc 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -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; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 418049f8e78..c6315e072ec 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -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,