Add initial draft of enum array searchbox

This commit is contained in:
Raymond Zhao 2021-05-25 11:43:35 -07:00
parent 7adcbfdd68
commit 5556b444bc
No known key found for this signature in database
GPG key ID: D36E5FCE46B63B58
8 changed files with 268 additions and 68 deletions

View file

@ -48,6 +48,13 @@
"default": true,
"markdownDescription": "%emmetShowAbbreviationSuggestions%"
},
"emmet.hmm": {
"type": "array",
"items": {
"type": "string",
"enum": ["a", "b", "c"]
}
},
"emmet.includeLanguages": {
"type": "object",
"additionalProperties": {

View file

@ -573,3 +573,10 @@
.settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count {
display: block;
}
.settings-editor > .settings-body > .settings-tree-container .setting-list-widget .setting-list-object-input.select-container {
width: 320px;
}
.settings-editor > .settings-body > .settings-tree-container .setting-list-widget .setting-list-object-input.select-container > select {
width: inherit;
}

View file

@ -118,7 +118,7 @@ export class SettingsEditor2 extends EditorPane {
return false;
}
return type === SettingValueType.Enum ||
type === SettingValueType.ArrayOfString ||
type === SettingValueType.StringOrEnumArray ||
type === SettingValueType.Complex ||
type === SettingValueType.Boolean ||
type === SettingValueType.Exclude;

View file

@ -73,11 +73,13 @@ function getExcludeDisplayValue(element: SettingsTreeSettingElement): IListDataI
.map(key => {
const value = data[key];
const sibling = typeof value === 'boolean' ? undefined : value.when;
return {
id: key,
value: key,
sibling
value: {
type: 'string',
data: key
},
sibling,
elementType: element.valueType
};
});
}
@ -200,6 +202,29 @@ function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectData
});
}
function createArraySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {
return keys => {
// let existingKeys: Set<string>;
// if (element.setting.uniqueItems) {
// existingKeys = new Set(keys);
// }
const enumOptions: IObjectEnumOption[] = [];
if (element.setting.enum) {
element.setting.enum.forEach((staticKey, i) => {
// if (!element.setting.uniqueItems || !existingKeys.has(staticKey)) {
const description = element.setting.enumDescriptions?.[i];
enumOptions.push({ value: staticKey, description });
// }
});
}
return enumOptions.length > 0
? { type: 'enum', data: enumOptions[0].value, options: enumOptions }
: undefined;
};
}
function createObjectKeySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {
const { objectProperties } = element.setting;
const allStaticKeys = Object.keys(objectProperties ?? {});
@ -267,11 +292,43 @@ function getListDisplayValue(element: SettingsTreeSettingElement): IListDataItem
return [];
}
return element.value.map((key: string) => {
return {
value: key
};
});
if (element.setting.arrayItemType === 'enum') {
let enumOptions: IObjectEnumOption[] = [];
if (element.setting.enum) {
enumOptions = element.setting.enum.map((setting, i) => {
return {
value: setting,
description: element.setting.enumDescriptions?.[i]
};
});
}
return element.value.map((key: string) => {
return {
value: {
type: 'enum',
data: key,
options: enumOptions
}
};
});
} else {
return element.value.map((key: string) => {
return {
value: {
type: 'string',
data: key
}
};
});
}
}
function getShowAddButtonList(dataElement: SettingsTreeSettingElement, listDisplayValue: IListDataItem[]): boolean {
if (dataElement.setting.enum && dataElement.setting.uniqueItems) {
return dataElement.setting.enum.length - listDisplayValue.length > 0;
} else {
return true;
}
}
export function resolveSettingsTree(tocData: ITOCEntry<string>, coreSettingsGroups: ISettingsGroup[], logService: ILogService): { tree: ITOCEntry<ISetting>, leftoverSettings: Set<ISetting> } {
@ -978,7 +1035,7 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
});
}
private computeNewList(template: ISettingListItemTemplate, e: ISettingListChangeEvent<IListDataItem>): string[] | undefined | null {
private computeNewList(template: ISettingListItemTemplate, e: ISettingListChangeEvent<IListDataItem>): string[] | undefined {
if (template.context) {
let newValue: string[] = [];
if (isArray(template.context.scopeValue)) {
@ -989,23 +1046,23 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
if (e.targetIndex !== undefined) {
// Delete value
if (!e.item?.value && e.originalItem.value && e.targetIndex > -1) {
if (!e.item?.value.data && e.originalItem.value.data && e.targetIndex > -1) {
newValue.splice(e.targetIndex, 1);
}
// Update value
else if (e.item?.value && e.originalItem.value) {
else if (e.item?.value.data && e.originalItem.value.data) {
if (e.targetIndex > -1) {
newValue[e.targetIndex] = e.item.value;
newValue[e.targetIndex] = e.item.value.data.toString();
}
// For some reason, we are updating and cannot find original value
// Just append the value in this case
else {
newValue.push(e.item.value);
newValue.push(e.item.value.data.toString());
}
}
// Add value
else if (e.item?.value && !e.originalItem.value && e.targetIndex >= newValue.length) {
newValue.push(e.item.value);
else if (e.item?.value.data && !e.originalItem.value.data && e.targetIndex >= newValue.length) {
newValue.push(e.item.value.data.toString());
}
}
if (
@ -1029,7 +1086,10 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string[] | undefined) => void): void {
const value = getListDisplayValue(dataElement);
template.listWidget.setValue(value);
template.listWidget.setValue(value, {
keySuggester: createArraySuggester(dataElement),
showAddButton: getShowAddButtonList(dataElement, value)
});
template.context = dataElement;
template.onChange = (v) => {
@ -1037,7 +1097,7 @@ export class SettingArrayRenderer extends AbstractSettingRenderer implements ITr
renderArrayValidations(dataElement, template, v, false);
};
renderArrayValidations(dataElement, template, value.map(v => v.value), true);
renderArrayValidations(dataElement, template, value.map(v => v.value.data.toString()), true);
}
}
@ -1177,22 +1237,22 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I
const newValue = { ...template.context.scopeValue };
// first delete the existing entry, if present
if (e.originalItem.value) {
if (e.originalItem.value in template.context.defaultValue) {
if (e.originalItem.value.data) {
if (e.originalItem.value.data.toString() in template.context.defaultValue) {
// delete a default by overriding it
newValue[e.originalItem.value] = false;
newValue[e.originalItem.value.data.toString()] = false;
} else {
delete newValue[e.originalItem.value];
delete newValue[e.originalItem.value.data.toString()];
}
}
// then add the new or updated entry, if present
if (e.item?.value) {
if (e.item.value in template.context.defaultValue && !e.item.sibling) {
if (e.item.value.data.toString() in template.context.defaultValue && !e.item.sibling) {
// add a default by deleting its override
delete newValue[e.item.value];
delete newValue[e.item.value.data.toString()];
} else {
newValue[e.item.value] = e.item.sibling ? { when: e.item.sibling } : true;
newValue[e.item.value.data.toString()] = e.item.sibling ? { when: e.item.sibling } : true;
}
}
@ -1848,7 +1908,7 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate<SettingsTreeGroupCh
return SETTINGS_ENUM_TEMPLATE_ID;
}
if (element.valueType === SettingValueType.ArrayOfString) {
if (element.valueType === SettingValueType.StringOrEnumArray) {
return SETTINGS_ARRAY_TEMPLATE_ID;
}

View file

@ -236,8 +236,8 @@ export class SettingsTreeSettingElement extends SettingsTreeElement {
this.valueType = SettingValueType.Number;
} else if (this.setting.type === 'boolean') {
this.valueType = SettingValueType.Boolean;
} else if (this.setting.type === 'array' && this.setting.arrayItemType === 'string') {
this.valueType = SettingValueType.ArrayOfString;
} else if (this.setting.type === 'array' && (this.setting.arrayItemType === 'string' || this.setting.arrayItemType === 'enum')) {
this.valueType = SettingValueType.StringOrEnumArray;
} else if (isArray(this.setting.type) && this.setting.type.indexOf(SettingValueType.Null) > -1 && this.setting.type.length === 2) {
if (this.setting.type.indexOf(SettingValueType.Integer) > -1) {
this.valueType = SettingValueType.NullableInteger;

View file

@ -487,14 +487,37 @@ export abstract class AbstractListSettingWidget<TDataItem extends object> extend
}
}
interface IListSetValueOptions {
keySuggester: IObjectKeySuggester;
showAddButton: boolean;
}
export interface IListDataItem {
value: string
value: ObjectKey,
sibling?: string
}
export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem> {
private keyValueSuggester: IObjectKeySuggester | undefined;
private showAddButton: boolean = true;
override setValue(listData: IListDataItem[], options?: IListSetValueOptions) {
this.keyValueSuggester = options?.keySuggester;
this.showAddButton = options?.showAddButton ?? true;
super.setValue(listData);
}
protected getEmptyItem(): IListDataItem {
return { value: '' };
return {
value: {
type: 'string',
data: ''
}
};
}
protected override isAddButtonVisible(): boolean {
return this.showAddButton;
}
protected getContainerClasses(): string[] {
@ -525,7 +548,7 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
const valueElement = DOM.append(rowElement, $('.setting-list-value'));
const siblingElement = DOM.append(rowElement, $('.setting-list-sibling'));
valueElement.textContent = item.value;
valueElement.textContent = item.value.data.toString();
siblingElement.textContent = item.sibling ? `when: ${item.sibling}` : null;
return rowElement;
@ -533,15 +556,57 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
protected renderEdit(item: IListDataItem, idx: number): HTMLElement {
const rowElement = $('.setting-list-edit-row');
let valueInput: InputBox | SelectBox;
let currentDisplayValue: string;
let currentEnumOptions: IObjectEnumOption[] | undefined;
const updatedItem = () => ({
value: valueInput.value,
sibling: siblingInput?.value
});
if (this.isItemNew(item) && this.keyValueSuggester) {
const enumData = this.keyValueSuggester(this.model.items.map(({ value: { data } }) => data));
item = {
...item,
value: {
type: 'enum',
data: item.value.data,
options: enumData ? enumData.options : []
}
};
}
switch (item.value.type) {
case 'string':
valueInput = this.renderInputBox(item.value, rowElement);
break;
case 'enum':
valueInput = this.renderDropdown(item.value, rowElement);
currentEnumOptions = item.value.options;
if (item.value.options.length) {
currentDisplayValue = currentEnumOptions[0].value;
}
break;
}
const updatedInputBoxItem = (): IListDataItem => {
const inputBox = valueInput as InputBox;
return {
value: {
type: 'string',
data: inputBox.value
},
sibling: siblingInput?.value
};
};
const updatedSelectBoxItem = (selectedValue: string): IListDataItem => {
return {
value: {
type: 'enum',
data: selectedValue,
options: currentEnumOptions ?? []
}
};
};
const onKeyDown = (e: StandardKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
this.handleItemChange(item, updatedItem(), idx);
this.handleItemChange(item, updatedInputBoxItem(), idx);
} else if (e.equals(KeyCode.Escape)) {
this.cancelEdit();
e.preventDefault();
@ -549,22 +614,19 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
rowElement?.focus();
};
const valueInput = new InputBox(rowElement, this.contextViewService, {
placeholder: this.getLocalizedStrings().inputPlaceholder
});
valueInput.element.classList.add('setting-list-valueInput');
this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, {
inputBackground: settingsSelectBackground,
inputForeground: settingsTextInputForeground,
inputBorder: settingsTextInputBorder
}));
this.listDisposables.add(valueInput);
valueInput.value = item.value;
this.listDisposables.add(
DOM.addStandardDisposableListener(valueInput.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)
);
if (item.value.type !== 'string') {
const selectBox = valueInput as SelectBox;
this.listDisposables.add(
selectBox.onDidSelect(({ selected }) => {
currentDisplayValue = selected;
})
);
} else {
const inputBox = valueInput as InputBox;
this.listDisposables.add(
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)
);
}
let siblingInput: InputBox | undefined;
if (!isUndefinedOrNull(item.sibling)) {
@ -590,7 +652,13 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
okButton.element.classList.add('setting-list-ok-button');
this.listDisposables.add(attachButtonStyler(okButton, this.themeService));
this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, updatedItem(), idx)));
this.listDisposables.add(okButton.onDidClick(() => {
if (item.value.type === 'string') {
this.handleItemChange(item, updatedInputBoxItem(), idx);
} else {
this.handleItemChange(item, updatedSelectBoxItem(currentDisplayValue), idx);
}
}));
const cancelButton = this._register(new Button(rowElement));
cancelButton.label = localize('cancelButton', "Cancel");
@ -602,7 +670,9 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
this.listDisposables.add(
disposableTimeout(() => {
valueInput.focus();
valueInput.select();
if (item.value.type === 'string') {
(valueInput as InputBox).select();
}
})
);
@ -610,13 +680,13 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
}
protected isItemNew(item: IListDataItem): boolean {
return item.value === '';
return item.value.data === '';
}
protected getLocalizedRowTitle({ value, sibling }: IListDataItem): string {
return isUndefinedOrNull(sibling)
? localize('listValueHintLabel', "List item `{0}`", value)
: localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value, sibling);
? localize('listValueHintLabel', "List item `{0}`", value.data)
: localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling);
}
protected getLocalizedStrings() {
@ -628,6 +698,48 @@ export class ListSettingWidget extends AbstractListSettingWidget<IListDataItem>
siblingInputPlaceholder: localize('listSiblingInputPlaceholder', "Sibling..."),
};
}
private renderInputBox(value: ObjectValue, rowElement: HTMLElement): InputBox {
const valueInput = new InputBox(rowElement, this.contextViewService, {
placeholder: this.getLocalizedStrings().inputPlaceholder
});
valueInput.element.classList.add('setting-list-valueInput');
this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, {
inputBackground: settingsSelectBackground,
inputForeground: settingsTextInputForeground,
inputBorder: settingsTextInputBorder
}));
this.listDisposables.add(valueInput);
valueInput.value = value.data.toString();
return valueInput;
}
private renderDropdown(value: ObjectKey, rowElement: HTMLElement): SelectBox {
if (value.type !== 'enum') {
throw new Error('Valuetype must be enum.');
}
const selectBoxOptions = value.options.map(({ value, description }) => ({ text: value, description }));
const selected = value.options.findIndex(option => value.data === option.value);
const selectBox = new SelectBox(selectBoxOptions, selected, this.contextViewService, undefined, {
useCustomDrawn: !(isIOS && BrowserFeatures.pointerEvents)
});
this.listDisposables.add(attachSelectBoxStyler(selectBox, this.themeService, {
selectBackground: settingsSelectBackground,
selectForeground: settingsSelectForeground,
selectBorder: settingsSelectBorder,
selectListBorder: settingsSelectListBorder
}));
const wrapper = $('.setting-list-object-input');
selectBox.render(wrapper);
rowElement.appendChild(wrapper);
return selectBox;
}
}
export class ExcludeSettingWidget extends ListSettingWidget {
@ -637,8 +749,8 @@ export class ExcludeSettingWidget extends ListSettingWidget {
protected override getLocalizedRowTitle({ value, sibling }: IListDataItem): string {
return isUndefinedOrNull(sibling)
? localize('excludePatternHintLabel', "Exclude files matching `{0}`", value)
: localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", value, sibling);
? localize('excludePatternHintLabel', "Exclude files matching `{0}`", value.data)
: localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", value.data, sibling);
}
protected override getLocalizedStrings() {
@ -673,7 +785,7 @@ interface IObjectBoolData {
data: boolean;
}
type ObjectKey = IObjectStringData | IObjectEnumData;
export type ObjectKey = IObjectStringData | IObjectEnumData;
export type ObjectValue = IObjectStringData | IObjectEnumData | IObjectBoolData;
export interface IObjectDataItem {

View file

@ -29,7 +29,7 @@ export enum SettingValueType {
Integer = 'integer',
Number = 'number',
Boolean = 'boolean',
ArrayOfString = 'array-of-string',
StringOrEnumArray = 'string-or-enum-array',
Exclude = 'exclude',
Complex = 'complex',
NullableInteger = 'nullable-integer',
@ -75,6 +75,7 @@ export interface ISetting {
enum?: string[];
enumDescriptions?: string[];
enumDescriptionsAreMarkdown?: boolean;
uniqueItems?: boolean;
tags?: string[];
disallowSyncIgnore?: boolean;
restricted?: boolean;

View file

@ -614,14 +614,26 @@ export class DefaultSettings extends Disposable {
const value = prop.default;
const description = (prop.description || prop.markdownDescription || '').split('\n');
const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : [];
const listItemType = prop.type === 'array' && prop.items && !isArray(prop.items) && prop.items.type && !isArray(prop.items.type)
? prop.items.type
: undefined;
let listItemType: string | undefined;
if (prop.type === 'array' && prop.items && !isArray(prop.items) && prop.items.type) {
if (prop.items.enum) {
listItemType = 'enum';
} else if (!isArray(prop.items.type)) {
listItemType = prop.items.type;
}
}
const objectProperties = prop.type === 'object' ? prop.properties : undefined;
const objectPatternProperties = prop.type === 'object' ? prop.patternProperties : undefined;
const objectAdditionalProperties = prop.type === 'object' ? prop.additionalProperties : undefined;
let enumToUse = prop.enum;
let enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions;
if (listItemType === 'enum' && !isArray(prop.items)) {
enumToUse = prop.items!.enum;
enumDescriptions = prop.items!.enumDescriptions || prop.items!.markdownEnumDescriptions;
}
result.push({
key,
value,
@ -638,9 +650,10 @@ export class DefaultSettings extends Disposable {
objectProperties,
objectPatternProperties,
objectAdditionalProperties,
enum: prop.enum,
enumDescriptions: prop.enumDescriptions || prop.markdownEnumDescriptions,
enumDescriptionsAreMarkdown: !prop.enumDescriptions,
enum: enumToUse,
enumDescriptions: enumDescriptions,
enumDescriptionsAreMarkdown: !enumDescriptions,
uniqueItems: prop.uniqueItems,
tags: prop.tags,
disallowSyncIgnore: prop.disallowSyncIgnore,
restricted: prop.restricted,