Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts
5267 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BrowserFeatures } from '../../../../base/browser/canIUse.js';6import * as DOM from '../../../../base/browser/dom.js';7import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';8import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';9import { Button } from '../../../../base/browser/ui/button/button.js';10import { applyDragImage } from '../../../../base/browser/ui/dnd/dnd.js';11import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';12import { SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js';13import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js';14import { IAction } from '../../../../base/common/actions.js';15import { disposableTimeout } from '../../../../base/common/async.js';16import { Codicon } from '../../../../base/common/codicons.js';17import { Emitter, Event } from '../../../../base/common/event.js';18import { MarkdownString } from '../../../../base/common/htmlContent.js';19import { KeyCode } from '../../../../base/common/keyCodes.js';20import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';21import { isIOS } from '../../../../base/common/platform.js';22import { ThemeIcon } from '../../../../base/common/themables.js';23import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js';24import { localize } from '../../../../nls.js';25import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';26import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';27import { IHoverService } from '../../../../platform/hover/browser/hover.js';28import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';29import { IThemeService } from '../../../../platform/theme/common/themeService.js';30import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';31import { SettingValueType } from '../../../services/preferences/common/preferences.js';32import { validatePropertyName } from '../../../services/preferences/common/preferencesValidation.js';33import { IJSONSchema } from '../../../../base/common/jsonSchema.js';34import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js';35import './media/settingsWidgets.css';36import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js';3738const $ = DOM.$;3940type EditKey = 'none' | 'create' | number;4142type RowElementGroup = {43rowElement: HTMLElement;44keyElement: HTMLElement;45valueElement?: HTMLElement;46};4748type IListViewItem<TDataItem extends object> = TDataItem & {49editing?: boolean;50selected?: boolean;51};5253export class ListSettingListModel<TDataItem extends object> {54protected _dataItems: TDataItem[] = [];55private _editKey: EditKey | null = null;56private _selectedIdx: number | null = null;57private _newDataItem: TDataItem;5859get items(): IListViewItem<TDataItem>[] {60const items = this._dataItems.map((item, i) => {61const editing = typeof this._editKey === 'number' && this._editKey === i;62return {63...item,64editing,65selected: i === this._selectedIdx || editing66};67});6869if (this._editKey === 'create') {70items.push({71editing: true,72selected: true,73...this._newDataItem,74});75}7677return items;78}7980constructor(newItem: TDataItem) {81this._newDataItem = newItem;82}8384setEditKey(key: EditKey): void {85this._editKey = key;86}8788setValue(listData: TDataItem[]): void {89this._dataItems = listData;90}9192select(idx: number | null): void {93this._selectedIdx = idx;94}9596getSelected(): number | null {97return this._selectedIdx;98}99100selectNext(): void {101if (typeof this._selectedIdx === 'number') {102this._selectedIdx = Math.min(this._selectedIdx + 1, this._dataItems.length - 1);103} else {104this._selectedIdx = 0;105}106}107108selectPrevious(): void {109if (typeof this._selectedIdx === 'number') {110this._selectedIdx = Math.max(this._selectedIdx - 1, 0);111} else {112this._selectedIdx = 0;113}114}115}116117export interface ISettingListChangeEvent<TDataItem extends object> {118type: 'change';119originalItem: TDataItem;120newItem: TDataItem;121targetIndex: number;122}123124export interface ISettingListAddEvent<TDataItem extends object> {125type: 'add';126newItem: TDataItem;127targetIndex: number;128}129130export interface ISettingListMoveEvent<TDataItem extends object> {131type: 'move';132originalItem: TDataItem;133newItem: TDataItem;134targetIndex: number;135sourceIndex: number;136}137138export interface ISettingListRemoveEvent<TDataItem extends object> {139type: 'remove';140originalItem: TDataItem;141targetIndex: number;142}143144export interface ISettingListResetEvent<TDataItem extends object> {145type: 'reset';146originalItem: TDataItem;147targetIndex: number;148}149150export type SettingListEvent<TDataItem extends object> = ISettingListChangeEvent<TDataItem> | ISettingListAddEvent<TDataItem> | ISettingListMoveEvent<TDataItem> | ISettingListRemoveEvent<TDataItem> | ISettingListResetEvent<TDataItem>;151152export abstract class AbstractListSettingWidget<TDataItem extends object> extends Disposable {153private listElement: HTMLElement;154private rowElements: HTMLElement[] = [];155156protected readonly _onDidChangeList = this._register(new Emitter<SettingListEvent<TDataItem>>());157protected readonly model = new ListSettingListModel<TDataItem>(this.getEmptyItem());158protected readonly listDisposables = this._register(new DisposableStore());159160readonly onDidChangeList: Event<SettingListEvent<TDataItem>> = this._onDidChangeList.event;161162get domNode(): HTMLElement {163return this.listElement;164}165166get items(): TDataItem[] {167return this.model.items;168}169170protected get isReadOnly(): boolean {171return false;172}173174constructor(175private container: HTMLElement,176@IThemeService protected readonly themeService: IThemeService,177@IContextViewService protected readonly contextViewService: IContextViewService,178@IConfigurationService protected readonly configurationService: IConfigurationService,179) {180super();181182this.listElement = DOM.append(container, $('div'));183this.listElement.setAttribute('role', 'list');184this.getContainerClasses().forEach(c => this.listElement.classList.add(c));185DOM.append(container, this.renderAddButton());186this.renderList();187188this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.POINTER_DOWN, e => this.onListClick(e)));189this._register(DOM.addDisposableListener(this.listElement, DOM.EventType.DBLCLICK, e => this.onListDoubleClick(e)));190191this._register(DOM.addStandardDisposableListener(this.listElement, 'keydown', (e: StandardKeyboardEvent) => {192if (e.equals(KeyCode.UpArrow)) {193this.selectPreviousRow();194} else if (e.equals(KeyCode.DownArrow)) {195this.selectNextRow();196} else {197return;198}199200e.preventDefault();201e.stopPropagation();202}));203}204205setValue(listData: TDataItem[]): void {206this.model.setValue(listData);207this.renderList();208}209210abstract isItemNew(item: TDataItem): boolean;211protected abstract getEmptyItem(): TDataItem;212protected abstract getContainerClasses(): string[];213protected abstract getActionsForItem(item: TDataItem, idx: number): IAction[];214protected abstract renderItem(item: TDataItem, idx: number): RowElementGroup;215protected abstract renderEdit(item: TDataItem, idx: number): HTMLElement;216protected abstract addTooltipsToRow(rowElement: RowElementGroup, item: TDataItem): void;217protected abstract getLocalizedStrings(): {218deleteActionTooltip: string;219editActionTooltip: string;220addButtonLabel: string;221};222223protected renderHeader(): HTMLElement | undefined {224return;225}226227protected isAddButtonVisible(): boolean {228return true;229}230231protected renderList(): void {232const focused = DOM.isAncestorOfActiveElement(this.listElement);233234DOM.clearNode(this.listElement);235this.listDisposables.clear();236237const newMode = this.model.items.some(item => !!(item.editing && this.isItemNew(item)));238this.container.classList.toggle('setting-list-hide-add-button', !this.isAddButtonVisible() || newMode);239240if (this.model.items.length) {241this.listElement.tabIndex = 0;242} else {243this.listElement.removeAttribute('tabIndex');244}245246const header = this.renderHeader();247248if (header) {249this.listElement.appendChild(header);250}251252this.rowElements = this.model.items.map((item, i) => this.renderDataOrEditItem(item, i, focused));253this.rowElements.forEach(rowElement => this.listElement.appendChild(rowElement));254255}256257protected createBasicSelectBox(value: IObjectEnumData): SelectBox {258const selectBoxOptions = value.options.map(({ value, description }) => ({ text: value, description }));259const selected = value.options.findIndex(option => value.data === option.value);260261const styles = getSelectBoxStyles({262selectBackground: settingsSelectBackground,263selectForeground: settingsSelectForeground,264selectBorder: settingsSelectBorder,265selectListBorder: settingsSelectListBorder266});267268269const selectBox = new SelectBox(selectBoxOptions, selected, this.contextViewService, styles, {270useCustomDrawn: !hasNativeContextMenu(this.configurationService) || !(isIOS && BrowserFeatures.pointerEvents)271});272return selectBox;273}274275protected editSetting(idx: number): void {276this.model.setEditKey(idx);277this.renderList();278}279280public cancelEdit(): void {281this.model.setEditKey('none');282this.renderList();283}284285protected handleItemChange(originalItem: TDataItem, changedItem: TDataItem, idx: number) {286this.model.setEditKey('none');287288if (this.isItemNew(originalItem)) {289this._onDidChangeList.fire({290type: 'add',291newItem: changedItem,292targetIndex: idx,293});294} else {295this._onDidChangeList.fire({296type: 'change',297originalItem,298newItem: changedItem,299targetIndex: idx,300});301}302303this.renderList();304}305306protected renderDataOrEditItem(item: IListViewItem<TDataItem>, idx: number, listFocused: boolean): HTMLElement {307const rowElement = item.editing ?308this.renderEdit(item, idx) :309this.renderDataItem(item, idx, listFocused);310311rowElement.setAttribute('role', 'listitem');312313return rowElement;314}315316private renderDataItem(item: IListViewItem<TDataItem>, idx: number, listFocused: boolean): HTMLElement {317const rowElementGroup = this.renderItem(item, idx);318const rowElement = rowElementGroup.rowElement;319320rowElement.setAttribute('data-index', idx + '');321rowElement.setAttribute('tabindex', item.selected ? '0' : '-1');322rowElement.classList.toggle('selected', item.selected);323324const actionBar = new ActionBar(rowElement);325this.listDisposables.add(actionBar);326327actionBar.push(this.getActionsForItem(item, idx), { icon: true, label: true });328this.addTooltipsToRow(rowElementGroup, item);329330if (item.selected && listFocused) {331disposableTimeout(() => rowElement.focus(), undefined, this.listDisposables);332}333334this.listDisposables.add(DOM.addDisposableListener(rowElement, 'click', (e) => {335// There is a parent list widget, which is the one that holds the list of settings.336// Prevent the parent widget from trying to interpret this click event.337e.stopPropagation();338}));339340return rowElement;341}342343private renderAddButton(): HTMLElement {344const rowElement = $('.setting-list-new-row');345346const startAddButton = this._register(new Button(rowElement, defaultButtonStyles));347startAddButton.label = this.getLocalizedStrings().addButtonLabel;348startAddButton.element.classList.add('setting-list-addButton');349350this._register(startAddButton.onDidClick(() => {351this.model.setEditKey('create');352this.renderList();353}));354355return rowElement;356}357358private onListClick(e: PointerEvent): void {359const targetIdx = this.getClickedItemIndex(e);360if (targetIdx < 0) {361return;362}363364e.preventDefault();365e.stopImmediatePropagation();366if (this.model.getSelected() === targetIdx) {367return;368}369370this.selectRow(targetIdx);371}372373private onListDoubleClick(e: MouseEvent): void {374const targetIdx = this.getClickedItemIndex(e);375if (targetIdx < 0) {376return;377}378379if (this.isReadOnly) {380return;381}382383const item = this.model.items[targetIdx];384if (item) {385this.editSetting(targetIdx);386e.preventDefault();387e.stopPropagation();388}389}390391private getClickedItemIndex(e: MouseEvent): number {392if (!e.target) {393return -1;394}395396const actionbar = DOM.findParentWithClass(e.target as HTMLElement, 'monaco-action-bar');397if (actionbar) {398// Don't handle doubleclicks inside the action bar399return -1;400}401402const element = DOM.findParentWithClass(e.target as HTMLElement, 'setting-list-row');403if (!element) {404return -1;405}406407const targetIdxStr = element.getAttribute('data-index');408if (!targetIdxStr) {409return -1;410}411412const targetIdx = parseInt(targetIdxStr);413return targetIdx;414}415416private selectRow(idx: number): void {417this.model.select(idx);418this.rowElements.forEach(row => row.classList.remove('selected'));419420const selectedRow = this.rowElements[this.model.getSelected()!];421422selectedRow.classList.add('selected');423selectedRow.focus();424}425426private selectNextRow(): void {427this.model.selectNext();428this.selectRow(this.model.getSelected()!);429}430431private selectPreviousRow(): void {432this.model.selectPrevious();433this.selectRow(this.model.getSelected()!);434}435}436437interface IListSetValueOptions {438showAddButton?: boolean;439keySuggester?: IObjectKeySuggester;440isReadOnly?: boolean;441}442443export interface IListDataItem {444value: ObjectKey;445sibling?: string;446}447448interface ListSettingWidgetDragDetails<TListDataItem extends IListDataItem> {449element: HTMLElement;450item: TListDataItem;451itemIndex: number;452}453454export class ListSettingWidget<TListDataItem extends IListDataItem> extends AbstractListSettingWidget<TListDataItem> {455private keyValueSuggester: IObjectKeySuggester | undefined;456private showAddButton: boolean = true;457private isEditable: boolean = true;458459override setValue(listData: TListDataItem[], options?: IListSetValueOptions) {460this.keyValueSuggester = options?.keySuggester;461this.isEditable = options?.isReadOnly === undefined ? true : !options.isReadOnly;462this.showAddButton = this.isEditable ? (options?.showAddButton ?? true) : false;463super.setValue(listData);464}465466constructor(467container: HTMLElement,468@IThemeService themeService: IThemeService,469@IContextViewService contextViewService: IContextViewService,470@IHoverService protected readonly hoverService: IHoverService,471@IConfigurationService configurationService: IConfigurationService,472) {473super(container, themeService, contextViewService, configurationService);474}475476protected getEmptyItem(): TListDataItem {477// eslint-disable-next-line local/code-no-dangerous-type-assertions478return {479value: {480type: 'string',481data: ''482}483} as TListDataItem;484}485486protected override isAddButtonVisible(): boolean {487return this.showAddButton;488}489490protected getContainerClasses(): string[] {491return ['setting-list-widget'];492}493494protected getActionsForItem(item: TListDataItem, idx: number): IAction[] {495if (this.isReadOnly) {496return [];497}498return [499{500class: ThemeIcon.asClassName(settingsEditIcon),501enabled: true,502id: 'workbench.action.editListItem',503tooltip: this.getLocalizedStrings().editActionTooltip,504run: () => this.editSetting(idx)505},506{507class: ThemeIcon.asClassName(settingsRemoveIcon),508enabled: true,509id: 'workbench.action.removeListItem',510tooltip: this.getLocalizedStrings().deleteActionTooltip,511run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx })512}513] as IAction[];514}515516private dragDetails: ListSettingWidgetDragDetails<TListDataItem> | undefined;517518protected renderItem(item: TListDataItem, idx: number): RowElementGroup {519const rowElement = $('.setting-list-row');520const valueElement = DOM.append(rowElement, $('.setting-list-value'));521const siblingElement = DOM.append(rowElement, $('.setting-list-sibling'));522523valueElement.textContent = item.value.data.toString();524if (item.sibling) {525siblingElement.textContent = `when: ${item.sibling}`;526} else {527siblingElement.textContent = null;528valueElement.classList.add('no-sibling');529}530531this.addDragAndDrop(rowElement, item, idx);532return { rowElement, keyElement: valueElement, valueElement: siblingElement };533}534535protected addDragAndDrop(rowElement: HTMLElement, item: TListDataItem, idx: number) {536if (this.model.items.every(item => !item.editing)) {537rowElement.draggable = true;538rowElement.classList.add('draggable');539} else {540rowElement.draggable = false;541rowElement.classList.remove('draggable');542}543544this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_START, (ev) => {545this.dragDetails = {546element: rowElement,547item,548itemIndex: idx549};550551applyDragImage(ev, rowElement, item.value.data);552}));553this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => {554if (!this.dragDetails) {555return false;556}557ev.preventDefault();558if (ev.dataTransfer) {559ev.dataTransfer.dropEffect = 'move';560}561return true;562}));563let counter = 0;564this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_ENTER, (ev) => {565counter++;566rowElement.classList.add('drag-hover');567}));568this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_LEAVE, (ev) => {569counter--;570if (!counter) {571rowElement.classList.remove('drag-hover');572}573}));574this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DROP, (ev) => {575// cancel the op if we dragged to a completely different setting576if (!this.dragDetails) {577return false;578}579ev.preventDefault();580counter = 0;581if (this.dragDetails.element !== rowElement) {582this._onDidChangeList.fire({583type: 'move',584originalItem: this.dragDetails.item,585sourceIndex: this.dragDetails.itemIndex,586newItem: item,587targetIndex: idx588});589}590return true;591}));592this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_END, (ev) => {593counter = 0;594rowElement.classList.remove('drag-hover');595ev.dataTransfer?.clearData();596if (this.dragDetails) {597this.dragDetails = undefined;598}599}));600}601602protected renderEdit(item: TListDataItem, idx: number): HTMLElement {603const rowElement = $('.setting-list-edit-row');604let valueInput: InputBox | SelectBox;605let currentDisplayValue: string;606let currentEnumOptions: IObjectEnumOption[] | undefined;607608if (this.keyValueSuggester) {609const enumData = this.keyValueSuggester(this.model.items.map(({ value: { data } }) => data), idx);610item = {611...item,612value: {613type: 'enum',614data: item.value.data,615options: enumData ? enumData.options : []616}617};618}619620switch (item.value.type) {621case 'string':622valueInput = this.renderInputBox(item.value, rowElement);623break;624case 'enum':625valueInput = this.renderDropdown(item.value, rowElement);626currentEnumOptions = item.value.options;627if (item.value.options.length) {628currentDisplayValue = this.isItemNew(item) ?629currentEnumOptions[0].value : item.value.data;630}631break;632}633634const updatedInputBoxItem = (): TListDataItem => {635const inputBox = valueInput as InputBox;636// eslint-disable-next-line local/code-no-dangerous-type-assertions637return {638value: {639type: 'string',640data: inputBox.value641},642sibling: siblingInput?.value643} as TListDataItem;644};645const updatedSelectBoxItem = (selectedValue: string): TListDataItem => {646// eslint-disable-next-line local/code-no-dangerous-type-assertions647return {648value: {649type: 'enum',650data: selectedValue,651options: currentEnumOptions ?? []652}653} as TListDataItem;654};655const onKeyDown = (e: StandardKeyboardEvent) => {656if (e.equals(KeyCode.Enter)) {657this.handleItemChange(item, updatedInputBoxItem(), idx);658} else if (e.equals(KeyCode.Escape)) {659this.cancelEdit();660e.preventDefault();661}662rowElement?.focus();663};664665if (item.value.type !== 'string') {666const selectBox = valueInput as SelectBox;667this.listDisposables.add(668selectBox.onDidSelect(({ selected }) => {669currentDisplayValue = selected;670})671);672} else {673const inputBox = valueInput as InputBox;674this.listDisposables.add(675DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)676);677}678679let siblingInput: InputBox | undefined;680if (!isUndefinedOrNull(item.sibling)) {681siblingInput = new InputBox(rowElement, this.contextViewService, {682placeholder: this.getLocalizedStrings().siblingInputPlaceholder,683inputBoxStyles: getInputBoxStyle({684inputBackground: settingsTextInputBackground,685inputForeground: settingsTextInputForeground,686inputBorder: settingsTextInputBorder687})688});689siblingInput.element.classList.add('setting-list-siblingInput');690this.listDisposables.add(siblingInput);691siblingInput.value = item.sibling;692693this.listDisposables.add(694DOM.addStandardDisposableListener(siblingInput.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)695);696} else if (valueInput instanceof InputBox) {697valueInput.element.classList.add('no-sibling');698}699700const okButton = this.listDisposables.add(new Button(rowElement, defaultButtonStyles));701okButton.label = localize('okButton', "OK");702okButton.element.classList.add('setting-list-ok-button');703704this.listDisposables.add(okButton.onDidClick(() => {705if (item.value.type === 'string') {706this.handleItemChange(item, updatedInputBoxItem(), idx);707} else {708this.handleItemChange(item, updatedSelectBoxItem(currentDisplayValue), idx);709}710}));711712const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles }));713cancelButton.label = localize('cancelButton', "Cancel");714cancelButton.element.classList.add('setting-list-cancel-button');715716this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit()));717718this.listDisposables.add(719disposableTimeout(() => {720valueInput.focus();721if (valueInput instanceof InputBox) {722valueInput.select();723}724})725);726727return rowElement;728}729730override isItemNew(item: TListDataItem): boolean {731return item.value.data === '';732}733734protected addTooltipsToRow(rowElementGroup: RowElementGroup, { value, sibling }: TListDataItem) {735const title = isUndefinedOrNull(sibling)736? localize('listValueHintLabel', "List item `{0}`", value.data)737: localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling);738739const { rowElement } = rowElementGroup;740this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: title }));741rowElement.setAttribute('aria-label', title);742}743744protected getLocalizedStrings() {745return {746deleteActionTooltip: localize('removeItem', "Remove Item"),747editActionTooltip: localize('editItem', "Edit Item"),748addButtonLabel: localize('addItem', "Add Item"),749inputPlaceholder: localize('itemInputPlaceholder', "Item..."),750siblingInputPlaceholder: localize('listSiblingInputPlaceholder', "Sibling..."),751};752}753754private renderInputBox(value: ObjectValue, rowElement: HTMLElement): InputBox {755const valueInput = new InputBox(rowElement, this.contextViewService, {756placeholder: this.getLocalizedStrings().inputPlaceholder,757inputBoxStyles: getInputBoxStyle({758inputBackground: settingsTextInputBackground,759inputForeground: settingsTextInputForeground,760inputBorder: settingsTextInputBorder761})762});763764valueInput.element.classList.add('setting-list-valueInput');765this.listDisposables.add(valueInput);766valueInput.value = value.data.toString();767768return valueInput;769}770771private renderDropdown(value: ObjectKey, rowElement: HTMLElement): SelectBox {772if (value.type !== 'enum') {773throw new Error('Valuetype must be enum.');774}775const selectBox = this.createBasicSelectBox(value);776777const wrapper = $('.setting-list-object-list-row');778selectBox.render(wrapper);779rowElement.appendChild(wrapper);780781return selectBox;782}783}784785export class ExcludeSettingWidget extends ListSettingWidget<IIncludeExcludeDataItem> {786protected override getContainerClasses() {787return ['setting-list-include-exclude-widget'];788}789790protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) {791return;792}793794protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void {795let title = isUndefinedOrNull(item.sibling)796? localize('excludePatternHintLabel', "Exclude files matching `{0}`", item.value.data)797: localize('excludeSiblingHintLabel', "Exclude files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling);798799if (item.source) {800title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source);801}802803const markdownTitle = new MarkdownString().appendMarkdown(title);804805const { rowElement } = rowElementGroup;806this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: markdownTitle }));807rowElement.setAttribute('aria-label', title);808}809810protected override getLocalizedStrings() {811return {812deleteActionTooltip: localize('removeExcludeItem', "Remove Exclude Item"),813editActionTooltip: localize('editExcludeItem', "Edit Exclude Item"),814addButtonLabel: localize('addPattern', "Add Pattern"),815inputPlaceholder: localize('excludePatternInputPlaceholder', "Exclude Pattern..."),816siblingInputPlaceholder: localize('excludeSiblingInputPlaceholder', "When Pattern Is Present..."),817};818}819}820821export class IncludeSettingWidget extends ListSettingWidget<IIncludeExcludeDataItem> {822protected override getContainerClasses() {823return ['setting-list-include-exclude-widget'];824}825826protected override addDragAndDrop(rowElement: HTMLElement, item: IIncludeExcludeDataItem, idx: number) {827return;828}829830protected override addTooltipsToRow(rowElementGroup: RowElementGroup, item: IIncludeExcludeDataItem): void {831let title = isUndefinedOrNull(item.sibling)832? localize('includePatternHintLabel', "Include files matching `{0}`", item.value.data)833: localize('includeSiblingHintLabel', "Include files matching `{0}`, only when a file matching `{1}` is present", item.value.data, item.sibling);834835if (item.source) {836title += localize('excludeIncludeSource', ". Default value provided by `{0}`", item.source);837}838839const markdownTitle = new MarkdownString().appendMarkdown(title);840841const { rowElement } = rowElementGroup;842this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: markdownTitle }));843rowElement.setAttribute('aria-label', title);844}845846protected override getLocalizedStrings() {847return {848deleteActionTooltip: localize('removeIncludeItem', "Remove Include Item"),849editActionTooltip: localize('editIncludeItem', "Edit Include Item"),850addButtonLabel: localize('addPattern', "Add Pattern"),851inputPlaceholder: localize('includePatternInputPlaceholder', "Include Pattern..."),852siblingInputPlaceholder: localize('includeSiblingInputPlaceholder', "When Pattern Is Present..."),853};854}855}856857interface IObjectStringData {858type: 'string';859data: string;860}861862export interface IObjectEnumOption {863value: string;864description?: string;865}866867interface IObjectEnumData {868type: 'enum';869data: string;870options: IObjectEnumOption[];871}872873interface IObjectBoolData {874type: 'boolean';875data: boolean;876}877878type ObjectKey = IObjectStringData | IObjectEnumData;879export type ObjectValue = IObjectStringData | IObjectEnumData | IObjectBoolData;880type ObjectWidget = InputBox | SelectBox;881882export interface IObjectDataItem {883key: ObjectKey;884value: ObjectValue;885keyDescription?: string;886source?: string;887removable: boolean;888resetable: boolean;889}890891export interface IIncludeExcludeDataItem {892value: ObjectKey;893elementType: SettingValueType;894sibling?: string;895source?: string;896}897898export interface IObjectValueSuggester {899(key: string): ObjectValue | undefined;900}901902export interface IObjectKeySuggester {903(existingKeys: string[], idx?: number): IObjectEnumData | undefined;904}905906interface IObjectSetValueOptions {907settingKey: string;908showAddButton: boolean;909isReadOnly?: boolean;910keySuggester?: IObjectKeySuggester;911valueSuggester?: IObjectValueSuggester;912propertyNames?: IJSONSchema;913}914915interface IObjectRenderEditWidgetOptions {916isKey: boolean;917idx: number;918readonly originalItem: IObjectDataItem;919readonly changedItem: IObjectDataItem;920update(keyOrValue: ObjectKey | ObjectValue): void;921}922923export class ObjectSettingDropdownWidget extends AbstractListSettingWidget<IObjectDataItem> {924private editable: boolean = true;925private currentSettingKey: string = '';926private showAddButton: boolean = true;927private keySuggester: IObjectKeySuggester = () => undefined;928private valueSuggester: IObjectValueSuggester = () => undefined;929private propertyNames: IJSONSchema | undefined;930931constructor(932container: HTMLElement,933@IThemeService themeService: IThemeService,934@IContextViewService contextViewService: IContextViewService,935@IHoverService private readonly hoverService: IHoverService,936@IConfigurationService configurationService: IConfigurationService,937) {938super(container, themeService, contextViewService, configurationService);939}940941override setValue(listData: IObjectDataItem[], options?: IObjectSetValueOptions): void {942this.editable = !options?.isReadOnly;943this.showAddButton = options?.showAddButton ?? this.showAddButton;944this.keySuggester = options?.keySuggester ?? this.keySuggester;945this.valueSuggester = options?.valueSuggester ?? this.valueSuggester;946this.propertyNames = options?.propertyNames;947948if (isDefined(options) && options.settingKey !== this.currentSettingKey) {949this.model.setEditKey('none');950this.model.select(null);951this.currentSettingKey = options.settingKey;952}953954super.setValue(listData);955}956957override isItemNew(item: IObjectDataItem): boolean {958return item.key.data === '' && item.value.data === '';959}960961protected override isAddButtonVisible(): boolean {962return this.showAddButton;963}964965protected override get isReadOnly(): boolean {966return !this.editable;967}968969protected getEmptyItem(): IObjectDataItem {970return {971key: { type: 'string', data: '' },972value: { type: 'string', data: '' },973removable: true,974resetable: false975};976}977978protected getContainerClasses() {979return ['setting-list-object-widget'];980}981982protected getActionsForItem(item: IObjectDataItem, idx: number): IAction[] {983if (this.isReadOnly) {984return [];985}986987const actions: IAction[] = [988{989class: ThemeIcon.asClassName(settingsEditIcon),990enabled: true,991id: 'workbench.action.editListItem',992label: '',993tooltip: this.getLocalizedStrings().editActionTooltip,994run: () => this.editSetting(idx)995},996];997998if (item.resetable) {999actions.push({1000class: ThemeIcon.asClassName(settingsDiscardIcon),1001enabled: true,1002id: 'workbench.action.resetListItem',1003label: '',1004tooltip: this.getLocalizedStrings().resetActionTooltip,1005run: () => this._onDidChangeList.fire({ type: 'reset', originalItem: item, targetIndex: idx })1006});1007}10081009if (item.removable) {1010actions.push({1011class: ThemeIcon.asClassName(settingsRemoveIcon),1012enabled: true,1013id: 'workbench.action.removeListItem',1014label: '',1015tooltip: this.getLocalizedStrings().deleteActionTooltip,1016run: () => this._onDidChangeList.fire({ type: 'remove', originalItem: item, targetIndex: idx })1017});1018}10191020return actions;1021}10221023protected override renderHeader() {1024const header = $('.setting-list-row-header');1025const keyHeader = DOM.append(header, $('.setting-list-object-key'));1026const valueHeader = DOM.append(header, $('.setting-list-object-value'));1027const { keyHeaderText, valueHeaderText } = this.getLocalizedStrings();10281029keyHeader.textContent = keyHeaderText;1030valueHeader.textContent = valueHeaderText;10311032return header;1033}10341035protected renderItem(item: IObjectDataItem, idx: number): RowElementGroup {1036const rowElement = $('.setting-list-row');1037rowElement.classList.add('setting-list-object-row');10381039// Mark row as invalid if the key doesn't match propertyNames.pattern1040if (this.propertyNames && item.key.data && !validatePropertyName(this.propertyNames, item.key.data)) {1041rowElement.classList.add('invalid-key');1042}10431044const keyElement = DOM.append(rowElement, $('.setting-list-object-key'));1045const valueElement = DOM.append(rowElement, $('.setting-list-object-value'));10461047keyElement.textContent = item.key.data;1048valueElement.textContent = item.value.data.toString();10491050return { rowElement, keyElement, valueElement };1051}10521053protected renderEdit(item: IObjectDataItem, idx: number): HTMLElement {1054const rowElement = $('.setting-list-edit-row.setting-list-object-row');10551056const changedItem = { ...item };1057const onKeyChange = (key: ObjectKey) => {1058changedItem.key = key;1059okButton.enabled = key.data !== '';10601061const suggestedValue = this.valueSuggester(key.data) ?? item.value;10621063if (this.shouldUseSuggestion(item.value, changedItem.value, suggestedValue)) {1064onValueChange(suggestedValue);1065renderLatestValue();1066}1067};1068const onValueChange = (value: ObjectValue) => {1069changedItem.value = value;1070};10711072let keyWidget: ObjectWidget | undefined;1073let keyElement: HTMLElement;10741075if (this.showAddButton) {1076if (this.isItemNew(item)) {1077const suggestedKey = this.keySuggester(this.model.items.map(({ key: { data } }) => data));10781079if (isDefined(suggestedKey)) {1080changedItem.key = suggestedKey;1081const suggestedValue = this.valueSuggester(changedItem.key.data);1082onValueChange(suggestedValue ?? changedItem.value);1083}1084}10851086const { widget, element } = this.renderEditWidget(changedItem.key, {1087idx,1088isKey: true,1089originalItem: item,1090changedItem,1091update: onKeyChange,1092});1093keyWidget = widget;1094keyElement = element;1095} else {1096keyElement = $('.setting-list-object-key');1097keyElement.textContent = item.key.data;1098}10991100let valueWidget: ObjectWidget;1101const valueContainer = $('.setting-list-object-value-container');11021103const renderLatestValue = () => {1104const { widget, element } = this.renderEditWidget(changedItem.value, {1105idx,1106isKey: false,1107originalItem: item,1108changedItem,1109update: onValueChange,1110});11111112valueWidget = widget;11131114DOM.clearNode(valueContainer);1115valueContainer.append(element);1116};11171118renderLatestValue();11191120rowElement.append(keyElement, valueContainer);11211122const okButton = this.listDisposables.add(new Button(rowElement, defaultButtonStyles));1123okButton.enabled = changedItem.key.data !== '';1124okButton.label = localize('okButton', "OK");1125okButton.element.classList.add('setting-list-ok-button');11261127this.listDisposables.add(okButton.onDidClick(() => this.handleItemChange(item, changedItem, idx)));11281129const cancelButton = this.listDisposables.add(new Button(rowElement, { secondary: true, ...defaultButtonStyles }));1130cancelButton.label = localize('cancelButton', "Cancel");1131cancelButton.element.classList.add('setting-list-cancel-button');11321133this.listDisposables.add(cancelButton.onDidClick(() => this.cancelEdit()));11341135this.listDisposables.add(1136disposableTimeout(() => {1137const widget = keyWidget ?? valueWidget;11381139widget.focus();11401141if (widget instanceof InputBox) {1142widget.select();1143}1144})1145);11461147return rowElement;1148}11491150private renderEditWidget(1151keyOrValue: ObjectKey | ObjectValue,1152options: IObjectRenderEditWidgetOptions,1153) {1154switch (keyOrValue.type) {1155case 'string':1156return this.renderStringEditWidget(keyOrValue, options);1157case 'enum':1158return this.renderEnumEditWidget(keyOrValue, options);1159case 'boolean':1160return this.renderEnumEditWidget(1161{1162type: 'enum',1163data: keyOrValue.data.toString(),1164options: [{ value: 'true' }, { value: 'false' }],1165},1166options,1167);1168}1169}11701171private renderStringEditWidget(1172keyOrValue: IObjectStringData,1173{ idx, isKey, originalItem, changedItem, update }: IObjectRenderEditWidgetOptions,1174) {1175const wrapper = $(isKey ? '.setting-list-object-input-key' : '.setting-list-object-input-value');1176const inputBox = new InputBox(wrapper, this.contextViewService, {1177placeholder: isKey1178? localize('objectKeyInputPlaceholder', "Key")1179: localize('objectValueInputPlaceholder', "Value"),1180inputBoxStyles: getInputBoxStyle({1181inputBackground: settingsTextInputBackground,1182inputForeground: settingsTextInputForeground,1183inputBorder: settingsTextInputBorder1184})1185});11861187inputBox.element.classList.add('setting-list-object-input');11881189this.listDisposables.add(inputBox);1190inputBox.value = keyOrValue.data;11911192this.listDisposables.add(inputBox.onDidChange(value => update({ ...keyOrValue, data: value })));11931194const onKeyDown = (e: StandardKeyboardEvent) => {1195if (e.equals(KeyCode.Enter)) {1196this.handleItemChange(originalItem, changedItem, idx);1197} else if (e.equals(KeyCode.Escape)) {1198this.cancelEdit();1199e.preventDefault();1200}1201};12021203this.listDisposables.add(1204DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, onKeyDown)1205);12061207return { widget: inputBox, element: wrapper };1208}12091210private renderEnumEditWidget(1211keyOrValue: IObjectEnumData,1212{ isKey, changedItem, update }: IObjectRenderEditWidgetOptions,1213) {1214const selectBox = this.createBasicSelectBox(keyOrValue);12151216const changedKeyOrValue = isKey ? changedItem.key : changedItem.value;1217this.listDisposables.add(1218selectBox.onDidSelect(({ selected }) =>1219update(1220changedKeyOrValue.type === 'boolean'1221? { ...changedKeyOrValue, data: selected === 'true' ? true : false }1222: { ...changedKeyOrValue, data: selected },1223)1224)1225);12261227const wrapper = $('.setting-list-object-input');1228wrapper.classList.add(1229isKey ? 'setting-list-object-input-key' : 'setting-list-object-input-value',1230);12311232selectBox.render(wrapper);12331234// Switch to the first item if the user set something invalid in the json1235const selected = keyOrValue.options.findIndex(option => keyOrValue.data === option.value);1236if (selected === -1 && keyOrValue.options.length) {1237update(1238changedKeyOrValue.type === 'boolean'1239? { ...changedKeyOrValue, data: true }1240: { ...changedKeyOrValue, data: keyOrValue.options[0].value }1241);1242} else if (changedKeyOrValue.type === 'boolean') {1243// https://github.com/microsoft/vscode/issues/1295811244update({ ...changedKeyOrValue, data: keyOrValue.data === 'true' });1245}12461247return { widget: selectBox, element: wrapper };1248}12491250private shouldUseSuggestion(originalValue: ObjectValue, previousValue: ObjectValue, newValue: ObjectValue): boolean {1251// suggestion is exactly the same1252if (newValue.type !== 'enum' && newValue.type === previousValue.type && newValue.data === previousValue.data) {1253return false;1254}12551256// item is new, use suggestion1257if (originalValue.data === '') {1258return true;1259}12601261if (previousValue.type === newValue.type && newValue.type !== 'enum') {1262return false;1263}12641265// check if all enum options are the same1266if (previousValue.type === 'enum' && newValue.type === 'enum') {1267const previousEnums = new Set(previousValue.options.map(({ value }) => value));1268newValue.options.forEach(({ value }) => previousEnums.delete(value));12691270// all options are the same1271if (previousEnums.size === 0) {1272return false;1273}1274}12751276return true;1277}12781279protected addTooltipsToRow(rowElementGroup: RowElementGroup, item: IObjectDataItem): void {1280const { keyElement, valueElement, rowElement } = rowElementGroup;12811282let accessibleDescription;1283if (item.source) {1284accessibleDescription = localize('objectPairHintLabelWithSource', "The property `{0}` is set to `{1}` by `{2}`.", item.key.data, item.value.data, item.source);1285} else {1286accessibleDescription = localize('objectPairHintLabel', "The property `{0}` is set to `{1}`.", item.key.data, item.value.data);1287}12881289const markdownString = new MarkdownString().appendMarkdown(accessibleDescription);12901291const keyDescription: string | MarkdownString = this.getEnumDescription(item.key) ?? item.keyDescription ?? markdownString;1292this.listDisposables.add(this.hoverService.setupDelayedHover(keyElement, { content: keyDescription }));12931294const valueDescription: string | MarkdownString = this.getEnumDescription(item.value) ?? markdownString;1295this.listDisposables.add(this.hoverService.setupDelayedHover(valueElement!, { content: valueDescription }));12961297rowElement.setAttribute('aria-label', accessibleDescription);1298}12991300private getEnumDescription(keyOrValue: ObjectKey | ObjectValue): string | undefined {1301const enumDescription = keyOrValue.type === 'enum'1302? keyOrValue.options.find(({ value }) => keyOrValue.data === value)?.description1303: undefined;1304return enumDescription;1305}13061307protected getLocalizedStrings() {1308return {1309deleteActionTooltip: localize('removeItem', "Remove Item"),1310resetActionTooltip: localize('resetItem', "Reset Item"),1311editActionTooltip: localize('editItem', "Edit Item"),1312addButtonLabel: localize('addItem', "Add Item"),1313keyHeaderText: localize('objectKeyHeader', "Item"),1314valueHeaderText: localize('objectValueHeader', "Value"),1315};1316}1317}13181319interface IBoolObjectSetValueOptions {1320settingKey: string;1321}13221323export interface IBoolObjectDataItem {1324key: IObjectStringData;1325value: IObjectBoolData;1326keyDescription?: string;1327source?: string;1328removable: false;1329resetable: boolean;1330}13311332export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget<IBoolObjectDataItem> {1333private currentSettingKey: string = '';13341335constructor(1336container: HTMLElement,1337@IThemeService themeService: IThemeService,1338@IContextViewService contextViewService: IContextViewService,1339@IHoverService private readonly hoverService: IHoverService,1340@IConfigurationService configurationService: IConfigurationService,1341) {1342super(container, themeService, contextViewService, configurationService);1343}13441345override setValue(listData: IBoolObjectDataItem[], options?: IBoolObjectSetValueOptions): void {1346if (isDefined(options) && options.settingKey !== this.currentSettingKey) {1347this.model.setEditKey('none');1348this.model.select(null);1349this.currentSettingKey = options.settingKey;1350}13511352super.setValue(listData);1353}13541355override isItemNew(item: IBoolObjectDataItem): boolean {1356return !item.key.data && !item.value.data;1357}13581359protected getEmptyItem(): IBoolObjectDataItem {1360return {1361key: { type: 'string', data: '' },1362value: { type: 'boolean', data: false },1363removable: false,1364resetable: true1365};1366}13671368protected getContainerClasses() {1369return ['setting-list-object-widget'];1370}13711372protected getActionsForItem(item: IBoolObjectDataItem, idx: number): IAction[] {1373return [];1374}13751376protected override isAddButtonVisible(): boolean {1377return false;1378}13791380protected override renderHeader() {1381return undefined;1382}13831384protected override renderDataOrEditItem(item: IListViewItem<IBoolObjectDataItem>, idx: number, listFocused: boolean): HTMLElement {1385const rowElement = this.renderEdit(item, idx);1386rowElement.setAttribute('role', 'listitem');1387return rowElement;1388}13891390protected renderItem(item: IBoolObjectDataItem, idx: number): RowElementGroup {1391// Return just the containers, since we always render in edit mode anyway1392const rowElement = $('.blank-row');1393const keyElement = $('.blank-row-key');1394return { rowElement, keyElement };1395}13961397protected renderEdit(item: IBoolObjectDataItem, idx: number): HTMLElement {1398const rowElement = $('.setting-list-edit-row.setting-list-object-row.setting-item-bool');13991400const changedItem = { ...item };1401const onValueChange = (newValue: boolean) => {1402changedItem.value.data = newValue;1403this.handleItemChange(item, changedItem, idx);1404};1405const checkboxDescription = item.keyDescription ? `${item.keyDescription} (${item.key.data})` : item.key.data;1406const { element, widget: checkbox } = this.renderEditWidget((changedItem.value as IObjectBoolData).data, checkboxDescription, onValueChange);1407rowElement.appendChild(element);14081409const valueElement = DOM.append(rowElement, $('.setting-list-object-value'));1410valueElement.textContent = checkboxDescription;14111412// We add the tooltips here, because the method is not called by default1413// for widgets in edit mode1414const rowElementGroup = { rowElement, keyElement: valueElement, valueElement: checkbox.domNode };1415this.addTooltipsToRow(rowElementGroup, item);14161417this._register(DOM.addDisposableListener(valueElement, DOM.EventType.MOUSE_DOWN, e => {1418const targetElement = <HTMLElement>e.target;1419if (targetElement.tagName.toLowerCase() !== 'a') {1420checkbox.checked = !checkbox.checked;1421onValueChange(checkbox.checked);1422}1423DOM.EventHelper.stop(e);1424}));14251426return rowElement;1427}14281429private renderEditWidget(1430value: boolean,1431checkboxDescription: string,1432onValueChange: (newValue: boolean) => void1433) {1434const checkbox = new Toggle({1435icon: Codicon.check,1436actionClassName: 'setting-value-checkbox',1437isChecked: value,1438title: checkboxDescription,1439...unthemedToggleStyles1440});14411442this.listDisposables.add(checkbox);14431444const wrapper = $('.setting-list-object-input');1445wrapper.classList.add('setting-list-object-input-key-checkbox');1446checkbox.domNode.classList.add('setting-value-checkbox');1447wrapper.appendChild(checkbox.domNode);14481449this._register(DOM.addDisposableListener(wrapper, DOM.EventType.MOUSE_DOWN, e => {1450checkbox.checked = !checkbox.checked;1451onValueChange(checkbox.checked);14521453// Without this line, the settings editor assumes1454// we lost focus on this setting completely.1455e.stopImmediatePropagation();1456}));14571458return { widget: checkbox, element: wrapper };1459}14601461protected addTooltipsToRow(rowElementGroup: RowElementGroup, item: IBoolObjectDataItem): void {1462const accessibleDescription = localize('objectPairHintLabel', "The property `{0}` is set to `{1}`.", item.key.data, item.value.data);1463const title = item.keyDescription ?? accessibleDescription;1464const { rowElement, keyElement, valueElement } = rowElementGroup;14651466this.listDisposables.add(this.hoverService.setupDelayedHover(keyElement, { content: title }));1467valueElement!.setAttribute('aria-label', accessibleDescription);1468rowElement.setAttribute('aria-label', accessibleDescription);1469}14701471protected getLocalizedStrings() {1472return {1473deleteActionTooltip: localize('removeItem', "Remove Item"),1474resetActionTooltip: localize('resetItem', "Reset Item"),1475editActionTooltip: localize('editItem', "Edit Item"),1476addButtonLabel: localize('addItem', "Add Item"),1477keyHeaderText: localize('objectKeyHeader', "Item"),1478valueHeaderText: localize('objectValueHeader', "Value"),1479};1480}1481}148214831484