diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index fddde991821..fe32e55ab66 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -3,105 +3,105 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-item-value > .setting-item-control { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-item-value > .setting-item-control { width: 100%; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-pattern { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-value { margin-right: 3px; margin-left: 2px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-pattern, -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-sibling { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-value, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-sibling { display: inline-block; line-height: 22px; font-family: var(--monaco-monospace-font); } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-sibling { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-sibling { opacity: 0.7; margin-left: 0.5em; font-size: 0.9em; white-space: pre; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar { display: none; position: absolute; right: 0px; margin-top: 1px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row { position: relative; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:focus { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row:focus { outline: none; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover .monaco-action-bar, -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected .monaco-action-bar { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row:hover .monaco-action-bar, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected .monaco-action-bar { display: block; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .action-label { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .action-label { width: 16px; height: 16px; padding: 2px; margin-right: 2px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .setting-listAction-edit { margin-right: 4px; } -.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { +.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .setting-listAction-edit { background: url("edit-light.svg") center center no-repeat; } -.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-edit { +.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .setting-listAction-edit { background: url("edit-dark.svg") center center no-repeat; } -.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-remove { +.vs .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .setting-listAction-remove { background: url("remove-light.svg") center center no-repeat; } -.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row .monaco-action-bar .setting-excludeAction-remove { +.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row .monaco-action-bar .setting-listAction-remove { background: url("remove-dark.svg") center center no-repeat; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .monaco-text-button { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .monaco-text-button { width: initial; padding: 2px 14px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-item-control.setting-exclude-new-mode .setting-exclude-new-row { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-item-control.setting-list-new-mode .setting-list-new-row { display: none; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .monaco-text-button.setting-exclude-addButton { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .monaco-text-button.setting-list-addButton { margin-right: 10px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-edit-row { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-edit-row { display: flex } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-patternInput, -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-siblingInput { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-valueInput, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-siblingInput { height: 22px; max-width: 320px; flex: 1; margin-right: 10px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-okButton { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-okButton { margin-right: 10px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-widget { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-widget { margin-bottom: 1px; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 44acebf1ed4..70f8cfc7188 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -79,6 +79,7 @@ export class SettingsEditor2 extends BaseEditor { return false; } return type === SettingValueType.Enum || + type === SettingValueType.ArrayOfString || type === SettingValueType.Complex || type === SettingValueType.Boolean || type === SettingValueType.Exclude; @@ -968,7 +969,7 @@ export class SettingsEditor2 extends BaseEditor { if (key) { const focusedKey = focusedSetting.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR); if (focusedKey === key && - !DOM.hasClass(focusedSetting, 'setting-item-exclude')) { // update `exclude`s live, as they have a separate "submit edit" step built in before this + !DOM.hasClass(focusedSetting, 'setting-item-list')) { // update `list`s live, as they have a separate "submit edit" step built in before this this.updateModifiedLabelForKey(key); this.scheduleRefresh(focusedSetting, key); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 68a62920e9a..63571356cc2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -42,14 +42,15 @@ import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attach import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, IExcludeChangeEvent, IExcludeDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ListSettingWidget, IListChangeEvent, IListDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ExcludeSettingWidget } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { isArray } from 'vs/base/common/types'; const $ = DOM.$; -function getExcludeDisplayValue(element: SettingsTreeSettingElement): IExcludeDataItem[] { +function getExcludeDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] { const data = element.isConfigured ? { ...element.defaultValue, ...element.scopeValue } : element.defaultValue; @@ -62,12 +63,20 @@ function getExcludeDisplayValue(element: SettingsTreeSettingElement): IExcludeDa return { id: key, - pattern: key, + value: key, sibling }; }); } +function getListDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] { + return element.value.map((key: string) => { + return { + value: key + }; + }); +} + export function resolveSettingsTree(tocData: ITOCEntry, coreSettingsGroups: ISettingsGroup[]): { tree: ITOCEntry, leftoverSettings: Set } { const allSettings = getFlatSettings(coreSettingsGroups); return { @@ -211,8 +220,12 @@ interface ISettingComplexItemTemplate extends ISettingItemTemplate { button: Button; } +interface ISettingListItemTemplate extends ISettingItemTemplate { + listWidget: ListSettingWidget; +} + interface ISettingExcludeItemTemplate extends ISettingItemTemplate { - excludeWidget: ExcludeSettingWidget; + excludeWidget: ListSettingWidget; } interface ISettingNewExtensionsTemplate extends IDisposableTemplate { @@ -229,6 +242,7 @@ const SETTINGS_TEXT_TEMPLATE_ID = 'settings.text.template'; const SETTINGS_NUMBER_TEMPLATE_ID = 'settings.number.template'; const SETTINGS_ENUM_TEMPLATE_ID = 'settings.enum.template'; const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template'; +const SETTINGS_ARRAY_TEMPLATE_ID = 'settings.array.template'; const SETTINGS_EXCLUDE_TEMPLATE_ID = 'settings.exclude.template'; const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template'; const SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID = 'settings.newExtensions.template'; @@ -656,11 +670,81 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I } } +export class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRenderer { + templateId = SETTINGS_ARRAY_TEMPLATE_ID; + + renderTemplate(container: HTMLElement): ISettingListItemTemplate { + const common = this.renderCommonTemplate(null, container, 'list'); + + const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement); + listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + common.toDispose.push(listWidget); + + const template: ISettingListItemTemplate = { + ...common, + listWidget + }; + + this.addSettingElementFocusHandler(template); + + common.toDispose.push(listWidget.onDidChangeList(e => this.onDidChangeList(template, e))); + + return template; + } + + private onDidChangeList(template: ISettingListItemTemplate, e: IListChangeEvent): void { + if (template.context) { + const newValue: any[] | undefined = isArray(template.context.scopeValue) + ? [...template.context.scopeValue] + : [...template.context.value]; + + // Delete value + if (e.removeIndex) { + if (!e.value && e.originalValue && e.removeIndex > -1) { + newValue.splice(e.removeIndex, 1); + } + } + // Add value + else if (e.value && !e.originalValue) { + newValue.push(e.value); + } + // Update value + else if (e.value && e.originalValue) { + const valueIndex = newValue.indexOf(e.originalValue); + if (valueIndex > -1) { + newValue[valueIndex] = e.value; + } + // For some reason, we are updating and cannot find original value + // Just append the value in this case + else { + newValue.push(e.value); + } + } + + this._onDidChangeSetting.fire({ + key: template.context.setting.key, + value: newValue, + type: template.context.valueType + }); + } + } + + renderElement(element: ITreeNode, index: number, templateData: ISettingListItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } + + protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string) => void): void { + const value = getListDisplayValue(dataElement); + template.listWidget.setValue(value); + template.context = dataElement; + } +} + export class SettingExcludeRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_EXCLUDE_TEMPLATE_ID; renderTemplate(container: HTMLElement): ISettingExcludeItemTemplate { - const common = this.renderCommonTemplate(null, container, 'exclude'); + const common = this.renderCommonTemplate(null, container, 'list'); const excludeWidget = this._instantiationService.createInstance(ExcludeSettingWidget, common.controlElement); excludeWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS); @@ -673,32 +757,32 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I this.addSettingElementFocusHandler(template); - common.toDispose.push(excludeWidget.onDidChangeExclude(e => this.onDidChangeExclude(template, e))); + common.toDispose.push(excludeWidget.onDidChangeList(e => this.onDidChangeExclude(template, e))); return template; } - private onDidChangeExclude(template: ISettingExcludeItemTemplate, e: IExcludeChangeEvent): void { + private onDidChangeExclude(template: ISettingExcludeItemTemplate, e: IListChangeEvent): void { if (template.context) { const newValue = { ...template.context.scopeValue }; // first delete the existing entry, if present - if (e.originalPattern) { - if (e.originalPattern in template.context.defaultValue) { + if (e.originalValue) { + if (e.originalValue in template.context.defaultValue) { // delete a default by overriding it - newValue[e.originalPattern] = false; + newValue[e.originalValue] = false; } else { - delete newValue[e.originalPattern]; + delete newValue[e.originalValue]; } } // then add the new or updated entry, if present - if (e.pattern) { - if (e.pattern in template.context.defaultValue && !e.sibling) { + if (e.value) { + if (e.value in template.context.defaultValue && !e.sibling) { // add a default by deleting its override - delete newValue[e.pattern]; + delete newValue[e.value]; } else { - newValue[e.pattern] = e.sibling ? { when: e.sibling } : true; + newValue[e.value] = e.sibling ? { when: e.sibling } : true; } } @@ -1056,6 +1140,7 @@ export class SettingTreeRenderers { this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions), this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), + this._instantiationService.createInstance(SettingArrayRenderer, this.settingActions), this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions), this._instantiationService.createInstance(SettingTextRenderer, this.settingActions), this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions), @@ -1254,23 +1339,27 @@ class SettingsTreeDelegate implements IListVirtualDelegate -1 && this.setting.type.length === 2) { if (this.setting.type.indexOf(SettingValueType.Integer) > -1) { this.valueType = SettingValueType.NullableInteger; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index c14cb52118c..6f38e066dd4 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -87,35 +87,35 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { collector.addRule(`.settings-editor > .settings-header > .settings-header-controls .settings-tabs-widget .action-label { color: ${foregroundColor}; }`); } - // Exclude control + // List control const listHoverBackgroundColor = theme.getColor(listHoverBackground); if (listHoverBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover { background-color: ${listHoverBackgroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row:hover { background-color: ${listHoverBackgroundColor}; }`); } const listHoverForegroundColor = theme.getColor(listHoverForeground); if (listHoverForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row:hover { color: ${listHoverForegroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row:hover { color: ${listHoverForegroundColor}; }`); } const listSelectBackgroundColor = theme.getColor(listActiveSelectionBackground); if (listSelectBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:focus { background-color: ${listSelectBackgroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected:focus { background-color: ${listSelectBackgroundColor}; }`); } const listInactiveSelectionBackgroundColor = theme.getColor(listInactiveSelectionBackground); if (listInactiveSelectionBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:not(:focus) { background-color: ${listInactiveSelectionBackgroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected:not(:focus) { background-color: ${listInactiveSelectionBackgroundColor}; }`); } const listInactiveSelectionForegroundColor = theme.getColor(listInactiveSelectionForeground); if (listInactiveSelectionForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:not(:focus) { color: ${listInactiveSelectionForegroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected:not(:focus) { color: ${listInactiveSelectionForegroundColor}; }`); } const listSelectForegroundColor = theme.getColor(listActiveSelectionForeground); if (listSelectForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-exclude .setting-exclude-row.selected:focus { color: ${listSelectForegroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-row.selected:focus { color: ${listSelectForegroundColor}; }`); } const codeTextForegroundColor = theme.getColor(textPreformatForeground); @@ -131,15 +131,15 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } }); -export class ExcludeSettingListModel { - private _dataItems: IExcludeDataItem[] = []; +export class ListSettingListModel { + private _dataItems: IListDataItem[] = []; private _editKey: string | null; private _selectedIdx: number | null; - get items(): IExcludeViewItem[] { + get items(): IListViewItem[] { const items = this._dataItems.map((item, i) => { - const editing = item.pattern === this._editKey; - return { + const editing = item.value === this._editKey; + return { ...item, editing, selected: i === this._selectedIdx || editing @@ -150,7 +150,7 @@ export class ExcludeSettingListModel { items.push({ editing: true, selected: true, - pattern: '', + value: '', sibling: '' }); } @@ -162,8 +162,8 @@ export class ExcludeSettingListModel { this._editKey = key; } - setValue(excludeData: IExcludeDataItem[]): void { - this._dataItems = excludeData; + setValue(listData: IListDataItem[]): void { + this._dataItems = listData; } select(idx: number): void { @@ -191,20 +191,21 @@ export class ExcludeSettingListModel { } } -export interface IExcludeChangeEvent { - originalPattern: string; - pattern?: string; +export interface IListChangeEvent { + originalValue: string; + value?: string; sibling?: string; + removeIndex?: number; } -export class ExcludeSettingWidget extends Disposable { +export class ListSettingWidget extends Disposable { private listElement: HTMLElement; private readonly listDisposables = this._register(new DisposableStore()); - private model = new ExcludeSettingListModel(); + private model = new ListSettingListModel(); - private readonly _onDidChangeExclude = this._register(new Emitter()); - readonly onDidChangeExclude: Event = this._onDidChangeExclude.event; + private readonly _onDidChangeList = this._register(new Emitter()); + readonly onDidChangeList: Event = this._onDidChangeList.event; get domNode(): HTMLElement { return this.listElement; @@ -217,7 +218,7 @@ export class ExcludeSettingWidget extends Disposable { ) { super(); - this.listElement = DOM.append(container, $('.setting-exclude-widget')); + this.listElement = DOM.append(container, $('.setting-list-widget')); this.listElement.setAttribute('tabindex', '0'); DOM.append(container, this.renderAddButton()); this.renderList(); @@ -240,8 +241,26 @@ export class ExcludeSettingWidget extends Disposable { })); } - setValue(excludeData: IExcludeDataItem[]): void { - this.model.setValue(excludeData); + protected getLocalizedStrings() { + return { + deleteActionTooltip: localize('removeItem', "Remove Item"), + editActionTooltip: localize('editItem', "Edit Item"), + complexEditActionTooltip: localize('editItemInSettingsJson', "Edit Item in settings.json"), + addButtonLabel: localize('addItem', "Add Item"), + inputPlaceholder: localize('itemInputPlaceholder', "String Item..."), + siblingInputPlaceholder: localize('listSiblingInputPlaceholder', "Sibling...") + }; + } + + protected getSettingListRowLocalizedStrings(value?: string, sibling?: string) { + return { + settingListRowValueHintLabel: localize('listValueHintLabel', "List item `{0}`", value), + settingListRowSiblingHintLabel: localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value) + }; + } + + setValue(listData: IListDataItem[]): void { + this.model.setValue(listData); this.renderList(); } @@ -269,7 +288,7 @@ export class ExcludeSettingWidget extends Disposable { const item = this.model.items[targetIdx]; if (item) { - this.editSetting(item.pattern); + this.editSetting(item.value); e.preventDefault(); e.stopPropagation(); } @@ -286,7 +305,7 @@ export class ExcludeSettingWidget extends Disposable { return -1; } - const element = DOM.findParentWithClass((e.target), 'setting-exclude-row'); + const element = DOM.findParentWithClass((e.target), 'setting-list-row'); if (!element) { return -1; } @@ -306,8 +325,8 @@ export class ExcludeSettingWidget extends Disposable { DOM.clearNode(this.listElement); this.listDisposables.clear(); - const newMode = this.model.items.some(item => !!(item.editing && !item.pattern)); - DOM.toggleClass(this.container, 'setting-exclude-new-mode', newMode); + const newMode = this.model.items.some(item => !!(item.editing && !item.value)); + DOM.toggleClass(this.container, 'setting-list-new-mode', newMode); this.model.items .map((item, i) => this.renderItem(item, i, focused)) @@ -317,22 +336,22 @@ export class ExcludeSettingWidget extends Disposable { this.listElement.style.height = listHeight + 'px'; } - private createDeleteAction(key: string): IAction { + private createDeleteAction(key: string, idx: number): IAction { return { - class: 'setting-excludeAction-remove', + class: 'setting-listAction-remove', enabled: true, - id: 'workbench.action.removeExcludeItem', - tooltip: localize('removeExcludeItem', "Remove Exclude Item"), - run: () => this._onDidChangeExclude.fire({ originalPattern: key, pattern: undefined }) + id: 'workbench.action.removeListItem', + tooltip: this.getLocalizedStrings().deleteActionTooltip, + run: () => this._onDidChangeList.fire({ originalValue: key, value: undefined, removeIndex: idx }) }; } private createEditAction(key: string): IAction { return { - class: 'setting-excludeAction-edit', + class: 'setting-listAction-edit', enabled: true, - id: 'workbench.action.editExcludeItem', - tooltip: localize('editExcludeItem', "Edit Exclude Item"), + id: 'workbench.action.editListItem', + tooltip: this.getLocalizedStrings().editActionTooltip, run: () => { this.editSetting(key); } @@ -344,14 +363,14 @@ export class ExcludeSettingWidget extends Disposable { this.renderList(); } - private renderItem(item: IExcludeViewItem, idx: number, listFocused: boolean): HTMLElement { + private renderItem(item: IListViewItem, idx: number, listFocused: boolean): HTMLElement { return item.editing ? this.renderEditItem(item) : this.renderDataItem(item, idx, listFocused); } - private renderDataItem(item: IExcludeViewItem, idx: number, listFocused: boolean): HTMLElement { - const rowElement = $('.setting-exclude-row'); + private renderDataItem(item: IListViewItem, idx: number, listFocused: boolean): HTMLElement { + const rowElement = $('.setting-list-row'); rowElement.setAttribute('data-index', idx + ''); rowElement.setAttribute('tabindex', item.selected ? '0' : '-1'); DOM.toggleClass(rowElement, 'selected', item.selected); @@ -359,19 +378,19 @@ export class ExcludeSettingWidget extends Disposable { const actionBar = new ActionBar(rowElement); this.listDisposables.add(actionBar); - const patternElement = DOM.append(rowElement, $('.setting-exclude-pattern')); - const siblingElement = DOM.append(rowElement, $('.setting-exclude-sibling')); - patternElement.textContent = item.pattern; + const valueElement = DOM.append(rowElement, $('.setting-list-value')); + const siblingElement = DOM.append(rowElement, $('.setting-list-sibling')); + valueElement.textContent = item.value; siblingElement.textContent = item.sibling ? ('when: ' + item.sibling) : null; actionBar.push([ - this.createEditAction(item.pattern), - this.createDeleteAction(item.pattern) + this.createEditAction(item.value), + this.createDeleteAction(item.value, idx) ], { icon: true, label: false }); - rowElement.title = item.sibling ? - localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.pattern, item.sibling) : - localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.pattern); + rowElement.title = item.sibling + ? this.getSettingListRowLocalizedStrings(item.value, item.sibling).settingListRowSiblingHintLabel + : this.getSettingListRowLocalizedStrings(item.value, item.sibling).settingListRowValueHintLabel; if (item.selected) { if (listFocused) { @@ -385,11 +404,11 @@ export class ExcludeSettingWidget extends Disposable { } private renderAddButton(): HTMLElement { - const rowElement = $('.setting-exclude-new-row'); + const rowElement = $('.setting-list-new-row'); const startAddButton = this._register(new Button(rowElement)); - startAddButton.label = localize('addPattern', "Add Pattern"); - startAddButton.element.classList.add('setting-exclude-addButton'); + startAddButton.label = this.getLocalizedStrings().addButtonLabel; + startAddButton.element.classList.add('setting-list-addButton'); this._register(attachButtonStyler(startAddButton, this.themeService)); this._register(startAddButton.onDidClick(() => { @@ -400,16 +419,16 @@ export class ExcludeSettingWidget extends Disposable { return rowElement; } - private renderEditItem(item: IExcludeViewItem): HTMLElement { - const rowElement = $('.setting-exclude-edit-row'); + private renderEditItem(item: IListViewItem): HTMLElement { + const rowElement = $('.setting-list-edit-row'); const onSubmit = (edited: boolean) => { this.model.setEditKey(null); - const pattern = patternInput.value.trim(); - if (edited && pattern) { - this._onDidChangeExclude.fire({ - originalPattern: item.pattern, - pattern, + const value = valueInput.value.trim(); + if (edited && value) { + this._onDidChangeList.fire({ + originalValue: item.value, + value: value, sibling: siblingInput && siblingInput.value.trim() }); } @@ -425,25 +444,26 @@ export class ExcludeSettingWidget extends Disposable { } }; - const patternInput = new InputBox(rowElement, this.contextViewService, { - placeholder: localize('excludePatternInputPlaceholder', "Exclude Pattern...") + const valueInput = new InputBox(rowElement, this.contextViewService, { + placeholder: this.getLocalizedStrings().inputPlaceholder }); - patternInput.element.classList.add('setting-exclude-patternInput'); - this.listDisposables.add(attachInputBoxStyler(patternInput, this.themeService, { + + valueInput.element.classList.add('setting-list-valueInput'); + this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { inputBackground: settingsTextInputBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); - this.listDisposables.add(patternInput); - patternInput.value = item.pattern; - this.listDisposables.add(DOM.addStandardDisposableListener(patternInput.inputElement, DOM.EventType.KEY_DOWN, onKeydown)); + this.listDisposables.add(valueInput); + valueInput.value = item.value; + this.listDisposables.add(DOM.addStandardDisposableListener(valueInput.inputElement, DOM.EventType.KEY_DOWN, onKeydown)); let siblingInput: InputBox; if (item.sibling) { siblingInput = new InputBox(rowElement, this.contextViewService, { - placeholder: localize('excludeSiblingInputPlaceholder', "When Pattern Is Present...") + placeholder: this.getLocalizedStrings().siblingInputPlaceholder }); - siblingInput.element.classList.add('setting-exclude-siblingInput'); + siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { inputBackground: settingsTextInputBackground, @@ -456,32 +476,52 @@ export class ExcludeSettingWidget extends Disposable { const okButton = this._register(new Button(rowElement)); okButton.label = localize('okButton', "OK"); - okButton.element.classList.add('setting-exclude-okButton'); + okButton.element.classList.add('setting-list-okButton'); this.listDisposables.add(attachButtonStyler(okButton, this.themeService)); this.listDisposables.add(okButton.onDidClick(() => onSubmit(true))); const cancelButton = this._register(new Button(rowElement)); cancelButton.label = localize('cancelButton', "Cancel"); - cancelButton.element.classList.add('setting-exclude-cancelButton'); + cancelButton.element.classList.add('setting-list-okButton'); this.listDisposables.add(attachButtonStyler(cancelButton, this.themeService)); this.listDisposables.add(cancelButton.onDidClick(() => onSubmit(false))); this.listDisposables.add( disposableTimeout(() => { - patternInput.focus(); - patternInput.select(); + valueInput.focus(); + valueInput.select(); })); return rowElement; } } -export interface IExcludeDataItem { - pattern: string; +export class ExcludeSettingWidget extends ListSettingWidget { + protected getLocalizedStrings() { + return { + deleteActionTooltip: localize('removeExcludeItem', "Remove Exclude Item"), + editActionTooltip: localize('editExcludeItem', "Edit Exclude Item"), + complexEditActionTooltip: localize('editExcludeItemInSettingsJson', "Edit Exclude Item in settings.json"), + addButtonLabel: localize('addPattern', "Add Pattern"), + inputPlaceholder: localize('excludePatternInputPlaceholder', "Exclude Pattern..."), + siblingInputPlaceholder: localize('excludeSiblingInputPlaceholder', "When Pattern Is Present...") + }; + } + + protected getSettingListRowLocalizedStrings(pattern?: string, sibling?: string) { + return { + settingListRowValueHintLabel: localize('excludePatternHintLabel', "Exclude files matching `{0}`", pattern), + settingListRowSiblingHintLabel: localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", pattern, sibling) + }; + } +} + +export interface IListDataItem { + value: string; sibling?: string; } -interface IExcludeViewItem extends IExcludeDataItem { +interface IListViewItem extends IListDataItem { editing?: boolean; selected?: boolean; } diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 008d85de813..a5eb1902bae 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -25,6 +25,7 @@ export enum SettingValueType { Integer = 'integer', Number = 'number', Boolean = 'boolean', + ArrayOfString = 'array-of-string', Exclude = 'exclude', Complex = 'complex', NullableInteger = 'nullable-integer', @@ -61,6 +62,7 @@ export interface ISetting { scope?: ConfigurationScope; type?: string | string[]; + arrayItemType?: string; 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 121cfeb9f37..0241619be50 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -22,7 +22,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor'; import { IFilterMetadata, IFilterResult, IGroupFilter, IKeybindingsEditorModel, ISearchResultGroup, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { withNullAsUndefined, isArray } from 'vs/base/common/types'; export const nullRange: IRange = { startLineNumber: -1, startColumn: -1, endLineNumber: -1, endColumn: -1 }; export function isNullRange(range: IRange): boolean { return range.startLineNumber === -1 && range.startColumn === -1 && range.endLineNumber === -1 && range.endColumn === -1; } @@ -613,6 +613,10 @@ 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; + result.push({ key, value, @@ -625,6 +629,7 @@ export class DefaultSettings extends Disposable { overrides, scope: prop.scope, type: prop.type, + arrayItemType: listItemType, enum: prop.enum, enumDescriptions: prop.enumDescriptions || prop.markdownEnumDescriptions, enumDescriptionsAreMarkdown: !prop.enumDescriptions,