UNPKG

monaco-editor-core

Version:

A browser based code editor

1,164 lines • 59.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var QuickPickItemElementRenderer_1; import * as dom from '../../../base/browser/dom.js'; import { Emitter, Event, EventBufferer } from '../../../base/common/event.js'; import { localize } from '../../../nls.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { WorkbenchObjectTree } from '../../list/browser/listService.js'; import { IThemeService } from '../../theme/common/themeService.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { QuickPickFocus } from '../common/quickInput.js'; import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js'; import { OS } from '../../../base/common/platform.js'; import { memoize } from '../../../base/common/decorators.js'; import { IconLabel } from '../../../base/browser/ui/iconLabel/iconLabel.js'; import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; import { isDark } from '../../theme/common/theme.js'; import { URI } from '../../../base/common/uri.js'; import { quickInputButtonToAction } from './quickInputUtils.js'; import { Lazy } from '../../../base/common/lazy.js'; import { getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from '../../../base/common/iconLabels.js'; import { compareAnything } from '../../../base/common/comparers.js'; import { ltrim } from '../../../base/common/strings.js'; import { RenderIndentGuides } from '../../../base/browser/ui/tree/abstractTree.js'; import { ThrottledDelayer } from '../../../base/common/async.js'; import { isCancellationError } from '../../../base/common/errors.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; import { equals } from '../../../base/common/arrays.js'; const $ = dom.$; class BaseQuickPickItemElement { constructor(index, hasCheckbox, mainItem) { this.index = index; this.hasCheckbox = hasCheckbox; this._hidden = false; this._init = new Lazy(() => { const saneLabel = mainItem.label ?? ''; const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim(); const saneAriaLabel = mainItem.ariaLabel || [saneLabel, this.saneDescription, this.saneDetail] .map(s => getCodiconAriaLabel(s)) .filter(s => !!s) .join(', '); return { saneLabel, saneSortLabel, saneAriaLabel }; }); this._saneDescription = mainItem.description; this._saneTooltip = mainItem.tooltip; } // #region Lazy Getters get saneLabel() { return this._init.value.saneLabel; } get saneSortLabel() { return this._init.value.saneSortLabel; } get saneAriaLabel() { return this._init.value.saneAriaLabel; } get element() { return this._element; } set element(value) { this._element = value; } get hidden() { return this._hidden; } set hidden(value) { this._hidden = value; } get saneDescription() { return this._saneDescription; } set saneDescription(value) { this._saneDescription = value; } get saneDetail() { return this._saneDetail; } set saneDetail(value) { this._saneDetail = value; } get saneTooltip() { return this._saneTooltip; } set saneTooltip(value) { this._saneTooltip = value; } get labelHighlights() { return this._labelHighlights; } set labelHighlights(value) { this._labelHighlights = value; } get descriptionHighlights() { return this._descriptionHighlights; } set descriptionHighlights(value) { this._descriptionHighlights = value; } get detailHighlights() { return this._detailHighlights; } set detailHighlights(value) { this._detailHighlights = value; } } class QuickPickItemElement extends BaseQuickPickItemElement { constructor(index, hasCheckbox, fireButtonTriggered, _onChecked, item, _separator) { super(index, hasCheckbox, item); this.fireButtonTriggered = fireButtonTriggered; this._onChecked = _onChecked; this.item = item; this._separator = _separator; this._checked = false; this.onChecked = hasCheckbox ? Event.map(Event.filter(this._onChecked.event, e => e.element === this), e => e.checked) : Event.None; this._saneDetail = item.detail; this._labelHighlights = item.highlights?.label; this._descriptionHighlights = item.highlights?.description; this._detailHighlights = item.highlights?.detail; } get separator() { return this._separator; } set separator(value) { this._separator = value; } get checked() { return this._checked; } set checked(value) { if (value !== this._checked) { this._checked = value; this._onChecked.fire({ element: this, checked: value }); } } get checkboxDisabled() { return !!this.item.disabled; } } var QuickPickSeparatorFocusReason; (function (QuickPickSeparatorFocusReason) { /** * No item is hovered or active */ QuickPickSeparatorFocusReason[QuickPickSeparatorFocusReason["NONE"] = 0] = "NONE"; /** * Some item within this section is hovered */ QuickPickSeparatorFocusReason[QuickPickSeparatorFocusReason["MOUSE_HOVER"] = 1] = "MOUSE_HOVER"; /** * Some item within this section is active */ QuickPickSeparatorFocusReason[QuickPickSeparatorFocusReason["ACTIVE_ITEM"] = 2] = "ACTIVE_ITEM"; })(QuickPickSeparatorFocusReason || (QuickPickSeparatorFocusReason = {})); class QuickPickSeparatorElement extends BaseQuickPickItemElement { constructor(index, fireSeparatorButtonTriggered, separator) { super(index, false, separator); this.fireSeparatorButtonTriggered = fireSeparatorButtonTriggered; this.separator = separator; this.children = new Array(); /** * If this item is >0, it means that there is some item in the list that is either: * * hovered over * * active */ this.focusInsideSeparator = QuickPickSeparatorFocusReason.NONE; } } class QuickInputItemDelegate { getHeight(element) { if (element instanceof QuickPickSeparatorElement) { return 30; } return element.saneDetail ? 44 : 22; } getTemplateId(element) { if (element instanceof QuickPickItemElement) { return QuickPickItemElementRenderer.ID; } else { return QuickPickSeparatorElementRenderer.ID; } } } class QuickInputAccessibilityProvider { getWidgetAriaLabel() { return localize('quickInput', "Quick Input"); } getAriaLabel(element) { return element.separator?.label ? `${element.saneAriaLabel}, ${element.separator.label}` : element.saneAriaLabel; } getWidgetRole() { return 'listbox'; } getRole(element) { return element.hasCheckbox ? 'checkbox' : 'option'; } isChecked(element) { if (!element.hasCheckbox || !(element instanceof QuickPickItemElement)) { return undefined; } return { get value() { return element.checked; }, onDidChange: e => element.onChecked(() => e()), }; } } class BaseQuickInputListRenderer { constructor(hoverDelegate) { this.hoverDelegate = hoverDelegate; } // TODO: only do the common stuff here and have a subclass handle their specific stuff renderTemplate(container) { const data = Object.create(null); data.toDisposeElement = new DisposableStore(); data.toDisposeTemplate = new DisposableStore(); data.entry = dom.append(container, $('.quick-input-list-entry')); // Checkbox const label = dom.append(data.entry, $('label.quick-input-list-label')); data.toDisposeTemplate.add(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { if (!data.checkbox.offsetParent) { // If checkbox not visible: e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740 } })); data.checkbox = dom.append(label, $('input.quick-input-list-checkbox')); data.checkbox.type = 'checkbox'; // Rows const rows = dom.append(label, $('.quick-input-list-rows')); const row1 = dom.append(rows, $('.quick-input-list-row')); const row2 = dom.append(rows, $('.quick-input-list-row')); // Label data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); data.toDisposeTemplate.add(data.label); data.icon = dom.prepend(data.label.element, $('.quick-input-list-icon')); // Keybinding const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding')); data.keybinding = new KeybindingLabel(keybindingContainer, OS); data.toDisposeTemplate.add(data.keybinding); // Detail const detailContainer = dom.append(row2, $('.quick-input-list-label-meta')); data.detail = new IconLabel(detailContainer, { supportHighlights: true, supportIcons: true, hoverDelegate: this.hoverDelegate }); data.toDisposeTemplate.add(data.detail); // Separator data.separator = dom.append(data.entry, $('.quick-input-list-separator')); // Actions data.actionBar = new ActionBar(data.entry, this.hoverDelegate ? { hoverDelegate: this.hoverDelegate } : undefined); data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar'); data.toDisposeTemplate.add(data.actionBar); return data; } disposeTemplate(data) { data.toDisposeElement.dispose(); data.toDisposeTemplate.dispose(); } disposeElement(_element, _index, data) { data.toDisposeElement.clear(); data.actionBar.clear(); } } let QuickPickItemElementRenderer = class QuickPickItemElementRenderer extends BaseQuickInputListRenderer { static { QuickPickItemElementRenderer_1 = this; } static { this.ID = 'quickpickitem'; } constructor(hoverDelegate, themeService) { super(hoverDelegate); this.themeService = themeService; // Follow what we do in the separator renderer this._itemsWithSeparatorsFrequency = new Map(); } get templateId() { return QuickPickItemElementRenderer_1.ID; } renderTemplate(container) { const data = super.renderTemplate(container); data.toDisposeTemplate.add(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { data.element.checked = data.checkbox.checked; })); return data; } renderElement(node, index, data) { const element = node.element; data.element = element; element.element = data.entry ?? undefined; const mainItem = element.item; data.checkbox.checked = element.checked; data.toDisposeElement.add(element.onChecked(checked => data.checkbox.checked = checked)); data.checkbox.disabled = element.checkboxDisabled; const { labelHighlights, descriptionHighlights, detailHighlights } = element; // Icon if (mainItem.iconPath) { const icon = isDark(this.themeService.getColorTheme().type) ? mainItem.iconPath.dark : (mainItem.iconPath.light ?? mainItem.iconPath.dark); const iconUrl = URI.revive(icon); data.icon.className = 'quick-input-list-icon'; data.icon.style.backgroundImage = dom.asCSSUrl(iconUrl); } else { data.icon.style.backgroundImage = ''; data.icon.className = mainItem.iconClass ? `quick-input-list-icon ${mainItem.iconClass}` : ''; } // Label let descriptionTitle; // if we have a tooltip, that will be the hover, // with the saneDescription as fallback if it // is defined if (!element.saneTooltip && element.saneDescription) { descriptionTitle = { markdown: { value: element.saneDescription, supportThemeIcons: true }, markdownNotSupportedFallback: element.saneDescription }; } const options = { matches: labelHighlights || [], // If we have a tooltip, we want that to be shown and not any other hover descriptionTitle, descriptionMatches: descriptionHighlights || [], labelEscapeNewLines: true }; options.extraClasses = mainItem.iconClasses; options.italic = mainItem.italic; options.strikethrough = mainItem.strikethrough; data.entry.classList.remove('quick-input-list-separator-as-item'); data.label.setLabel(element.saneLabel, element.saneDescription, options); // Keybinding data.keybinding.set(mainItem.keybinding); // Detail if (element.saneDetail) { let title; // If we have a tooltip, we want that to be shown and not any other hover if (!element.saneTooltip) { title = { markdown: { value: element.saneDetail, supportThemeIcons: true }, markdownNotSupportedFallback: element.saneDetail }; } data.detail.element.style.display = ''; data.detail.setLabel(element.saneDetail, undefined, { matches: detailHighlights, title, labelEscapeNewLines: true }); } else { data.detail.element.style.display = 'none'; } // Separator if (element.separator?.label) { data.separator.textContent = element.separator.label; data.separator.style.display = ''; this.addItemWithSeparator(element); } else { data.separator.style.display = 'none'; } data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator); // Actions const buttons = mainItem.buttons; if (buttons && buttons.length) { data.actionBar.push(buttons.map((button, index) => quickInputButtonToAction(button, `id-${index}`, () => element.fireButtonTriggered({ button, item: element.item }))), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { data.entry.classList.remove('has-actions'); } } disposeElement(element, _index, data) { this.removeItemWithSeparator(element.element); super.disposeElement(element, _index, data); } isItemWithSeparatorVisible(item) { return this._itemsWithSeparatorsFrequency.has(item); } addItemWithSeparator(item) { this._itemsWithSeparatorsFrequency.set(item, (this._itemsWithSeparatorsFrequency.get(item) || 0) + 1); } removeItemWithSeparator(item) { const frequency = this._itemsWithSeparatorsFrequency.get(item) || 0; if (frequency > 1) { this._itemsWithSeparatorsFrequency.set(item, frequency - 1); } else { this._itemsWithSeparatorsFrequency.delete(item); } } }; QuickPickItemElementRenderer = QuickPickItemElementRenderer_1 = __decorate([ __param(1, IThemeService) ], QuickPickItemElementRenderer); class QuickPickSeparatorElementRenderer extends BaseQuickInputListRenderer { constructor() { super(...arguments); // This is a frequency map because sticky scroll re-uses the same renderer to render a second // instance of the same separator. this._visibleSeparatorsFrequency = new Map(); } static { this.ID = 'quickpickseparator'; } get templateId() { return QuickPickSeparatorElementRenderer.ID; } get visibleSeparators() { return [...this._visibleSeparatorsFrequency.keys()]; } isSeparatorVisible(separator) { return this._visibleSeparatorsFrequency.has(separator); } renderTemplate(container) { const data = super.renderTemplate(container); data.checkbox.style.display = 'none'; return data; } renderElement(node, index, data) { const element = node.element; data.element = element; element.element = data.entry ?? undefined; element.element.classList.toggle('focus-inside', !!element.focusInsideSeparator); const mainItem = element.separator; const { labelHighlights, descriptionHighlights, detailHighlights } = element; // Icon data.icon.style.backgroundImage = ''; data.icon.className = ''; // Label let descriptionTitle; // if we have a tooltip, that will be the hover, // with the saneDescription as fallback if it // is defined if (!element.saneTooltip && element.saneDescription) { descriptionTitle = { markdown: { value: element.saneDescription, supportThemeIcons: true }, markdownNotSupportedFallback: element.saneDescription }; } const options = { matches: labelHighlights || [], // If we have a tooltip, we want that to be shown and not any other hover descriptionTitle, descriptionMatches: descriptionHighlights || [], labelEscapeNewLines: true }; data.entry.classList.add('quick-input-list-separator-as-item'); data.label.setLabel(element.saneLabel, element.saneDescription, options); // Detail if (element.saneDetail) { let title; // If we have a tooltip, we want that to be shown and not any other hover if (!element.saneTooltip) { title = { markdown: { value: element.saneDetail, supportThemeIcons: true }, markdownNotSupportedFallback: element.saneDetail }; } data.detail.element.style.display = ''; data.detail.setLabel(element.saneDetail, undefined, { matches: detailHighlights, title, labelEscapeNewLines: true }); } else { data.detail.element.style.display = 'none'; } // Separator data.separator.style.display = 'none'; data.entry.classList.add('quick-input-list-separator-border'); // Actions const buttons = mainItem.buttons; if (buttons && buttons.length) { data.actionBar.push(buttons.map((button, index) => quickInputButtonToAction(button, `id-${index}`, () => element.fireSeparatorButtonTriggered({ button, separator: element.separator }))), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { data.entry.classList.remove('has-actions'); } this.addSeparator(element); } disposeElement(element, _index, data) { this.removeSeparator(element.element); if (!this.isSeparatorVisible(element.element)) { element.element.element?.classList.remove('focus-inside'); } super.disposeElement(element, _index, data); } addSeparator(separator) { this._visibleSeparatorsFrequency.set(separator, (this._visibleSeparatorsFrequency.get(separator) || 0) + 1); } removeSeparator(separator) { const frequency = this._visibleSeparatorsFrequency.get(separator) || 0; if (frequency > 1) { this._visibleSeparatorsFrequency.set(separator, frequency - 1); } else { this._visibleSeparatorsFrequency.delete(separator); } } } let QuickInputTree = class QuickInputTree extends Disposable { constructor(parent, hoverDelegate, linkOpenerDelegate, id, instantiationService, accessibilityService) { super(); this.parent = parent; this.hoverDelegate = hoverDelegate; this.linkOpenerDelegate = linkOpenerDelegate; this.accessibilityService = accessibilityService; //#region QuickInputTree Events this._onKeyDown = new Emitter(); this._onLeave = new Emitter(); /** * Event that is fired when the tree would no longer have focus. */ this.onLeave = this._onLeave.event; this._visibleCountObservable = observableValue('VisibleCount', 0); this.onChangedVisibleCount = Event.fromObservable(this._visibleCountObservable, this._store); this._allVisibleCheckedObservable = observableValue('AllVisibleChecked', false); this.onChangedAllVisibleChecked = Event.fromObservable(this._allVisibleCheckedObservable, this._store); this._checkedCountObservable = observableValue('CheckedCount', 0); this.onChangedCheckedCount = Event.fromObservable(this._checkedCountObservable, this._store); this._checkedElementsObservable = observableValueOpts({ equalsFn: equals }, new Array()); this.onChangedCheckedElements = Event.fromObservable(this._checkedElementsObservable, this._store); this._onButtonTriggered = new Emitter(); this.onButtonTriggered = this._onButtonTriggered.event; this._onSeparatorButtonTriggered = new Emitter(); this.onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; this._elementChecked = new Emitter(); this._elementCheckedEventBufferer = new EventBufferer(); //#endregion this._hasCheckboxes = false; this._inputElements = new Array(); this._elementTree = new Array(); this._itemElements = new Array(); // Elements that apply to the current set of elements this._elementDisposable = this._register(new DisposableStore()); this._matchOnDescription = false; this._matchOnDetail = false; this._matchOnLabel = true; this._matchOnLabelMode = 'fuzzy'; this._sortByLabel = true; this._shouldLoop = true; this._container = dom.append(this.parent, $('.quick-input-list')); this._separatorRenderer = new QuickPickSeparatorElementRenderer(hoverDelegate); this._itemRenderer = instantiationService.createInstance(QuickPickItemElementRenderer, hoverDelegate); this._tree = this._register(instantiationService.createInstance((WorkbenchObjectTree), 'QuickInput', this._container, new QuickInputItemDelegate(), [this._itemRenderer, this._separatorRenderer], { filter: { filter(element) { return element.hidden ? 0 /* TreeVisibility.Hidden */ : element instanceof QuickPickSeparatorElement ? 2 /* TreeVisibility.Recurse */ : 1 /* TreeVisibility.Visible */; }, }, sorter: { compare: (element, otherElement) => { if (!this.sortByLabel || !this._lastQueryString) { return 0; } const normalizedSearchValue = this._lastQueryString.toLowerCase(); return compareEntries(element, otherElement, normalizedSearchValue); }, }, accessibilityProvider: new QuickInputAccessibilityProvider(), setRowLineHeight: false, multipleSelectionSupport: false, hideTwistiesOfChildlessElements: true, renderIndentGuides: RenderIndentGuides.None, findWidgetEnabled: false, indent: 0, horizontalScrolling: false, allowNonCollapsibleParents: true, alwaysConsumeMouseWheel: true })); this._tree.getHTMLElement().id = id; this._registerListeners(); } //#region public getters/setters get onDidChangeFocus() { return Event.map(this._tree.onDidChangeFocus, e => e.elements.filter((e) => e instanceof QuickPickItemElement).map(e => e.item), this._store); } get onDidChangeSelection() { return Event.map(this._tree.onDidChangeSelection, e => ({ items: e.elements.filter((e) => e instanceof QuickPickItemElement).map(e => e.item), event: e.browserEvent }), this._store); } get displayed() { return this._container.style.display !== 'none'; } set displayed(value) { this._container.style.display = value ? '' : 'none'; } get scrollTop() { return this._tree.scrollTop; } set scrollTop(scrollTop) { this._tree.scrollTop = scrollTop; } get ariaLabel() { return this._tree.ariaLabel; } set ariaLabel(label) { this._tree.ariaLabel = label ?? ''; } set enabled(value) { this._tree.getHTMLElement().style.pointerEvents = value ? '' : 'none'; } get matchOnDescription() { return this._matchOnDescription; } set matchOnDescription(value) { this._matchOnDescription = value; } get matchOnDetail() { return this._matchOnDetail; } set matchOnDetail(value) { this._matchOnDetail = value; } get matchOnLabel() { return this._matchOnLabel; } set matchOnLabel(value) { this._matchOnLabel = value; } get matchOnLabelMode() { return this._matchOnLabelMode; } set matchOnLabelMode(value) { this._matchOnLabelMode = value; } get sortByLabel() { return this._sortByLabel; } set sortByLabel(value) { this._sortByLabel = value; } get shouldLoop() { return this._shouldLoop; } set shouldLoop(value) { this._shouldLoop = value; } //#endregion //#region register listeners _registerListeners() { this._registerOnKeyDown(); this._registerOnContainerClick(); this._registerOnMouseMiddleClick(); this._registerOnTreeModelChanged(); this._registerOnElementChecked(); this._registerOnContextMenu(); this._registerHoverListeners(); this._registerSelectionChangeListener(); this._registerSeparatorActionShowingListeners(); } _registerOnKeyDown() { // TODO: Should this be added at a higher level? this._register(this._tree.onKeyDown(e => { const event = new StandardKeyboardEvent(e); switch (event.keyCode) { case 10 /* KeyCode.Space */: this.toggleCheckbox(); break; } this._onKeyDown.fire(event); })); } _registerOnContainerClick() { this._register(dom.addDisposableListener(this._container, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox. this._onLeave.fire(); } })); } _registerOnMouseMiddleClick() { this._register(dom.addDisposableListener(this._container, dom.EventType.AUXCLICK, e => { if (e.button === 1) { this._onLeave.fire(); } })); } _registerOnTreeModelChanged() { this._register(this._tree.onDidChangeModel(() => { const visibleCount = this._itemElements.filter(e => !e.hidden).length; this._visibleCountObservable.set(visibleCount, undefined); if (this._hasCheckboxes) { this._updateCheckedObservables(); } })); } _registerOnElementChecked() { // Only fire the last event when buffered this._register(this._elementCheckedEventBufferer.wrapEvent(this._elementChecked.event, (_, e) => e)(_ => this._updateCheckedObservables())); } _registerOnContextMenu() { this._register(this._tree.onContextMenu(e => { if (e.element) { e.browserEvent.preventDefault(); // we want to treat a context menu event as // a gesture to open the item at the index // since we do not have any context menu // this enables for example macOS to Ctrl- // click on an item to open it. this._tree.setSelection([e.element]); } })); } _registerHoverListeners() { const delayer = this._register(new ThrottledDelayer(this.hoverDelegate.delay)); this._register(this._tree.onMouseOver(async (e) => { // If we hover over an anchor element, we don't want to show the hover because // the anchor may have a tooltip that we want to show instead. if (dom.isHTMLAnchorElement(e.browserEvent.target)) { delayer.cancel(); return; } if ( // anchors are an exception as called out above so we skip them here !(dom.isHTMLAnchorElement(e.browserEvent.relatedTarget)) && // check if the mouse is still over the same element dom.isAncestor(e.browserEvent.relatedTarget, e.element?.element)) { return; } try { await delayer.trigger(async () => { if (e.element instanceof QuickPickItemElement) { this.showHover(e.element); } }); } catch (e) { // Ignore cancellation errors due to mouse out if (!isCancellationError(e)) { throw e; } } })); this._register(this._tree.onMouseOut(e => { // onMouseOut triggers every time a new element has been moused over // even if it's on the same list item. We only want one event, so we // check if the mouse is still over the same element. if (dom.isAncestor(e.browserEvent.relatedTarget, e.element?.element)) { return; } delayer.cancel(); })); } /** * Register's focus change and mouse events so that we can track when items inside of a * separator's section are focused or hovered so that we can display the separator's actions */ _registerSeparatorActionShowingListeners() { this._register(this._tree.onDidChangeFocus(e => { const parent = e.elements[0] ? this._tree.getParentElement(e.elements[0]) // treat null as focus lost and when we have no separators : null; for (const separator of this._separatorRenderer.visibleSeparators) { const value = separator === parent; // get bitness of ACTIVE_ITEM and check if it changed const currentActive = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.ACTIVE_ITEM); if (currentActive !== value) { if (value) { separator.focusInsideSeparator |= QuickPickSeparatorFocusReason.ACTIVE_ITEM; } else { separator.focusInsideSeparator &= ~QuickPickSeparatorFocusReason.ACTIVE_ITEM; } this._tree.rerender(separator); } } })); this._register(this._tree.onMouseOver(e => { const parent = e.element ? this._tree.getParentElement(e.element) : null; for (const separator of this._separatorRenderer.visibleSeparators) { if (separator !== parent) { continue; } const currentMouse = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.MOUSE_HOVER); if (!currentMouse) { separator.focusInsideSeparator |= QuickPickSeparatorFocusReason.MOUSE_HOVER; this._tree.rerender(separator); } } })); this._register(this._tree.onMouseOut(e => { const parent = e.element ? this._tree.getParentElement(e.element) : null; for (const separator of this._separatorRenderer.visibleSeparators) { if (separator !== parent) { continue; } const currentMouse = !!(separator.focusInsideSeparator & QuickPickSeparatorFocusReason.MOUSE_HOVER); if (currentMouse) { separator.focusInsideSeparator &= ~QuickPickSeparatorFocusReason.MOUSE_HOVER; this._tree.rerender(separator); } } })); } _registerSelectionChangeListener() { // When the user selects a separator, the separator will move to the top and focus will be // set to the first element after the separator. this._register(this._tree.onDidChangeSelection(e => { const elementsWithoutSeparators = e.elements.filter((e) => e instanceof QuickPickItemElement); if (elementsWithoutSeparators.length !== e.elements.length) { if (e.elements.length === 1 && e.elements[0] instanceof QuickPickSeparatorElement) { this._tree.setFocus([e.elements[0].children[0]]); this._tree.reveal(e.elements[0], 0); } this._tree.setSelection(elementsWithoutSeparators); } })); } //#endregion //#region public methods setAllVisibleChecked(checked) { this._elementCheckedEventBufferer.bufferEvents(() => { this._itemElements.forEach(element => { if (!element.hidden && !element.checkboxDisabled) { // Would fire an event if we didn't beffer the events element.checked = checked; } }); }); } setElements(inputElements) { this._elementDisposable.clear(); this._lastQueryString = undefined; this._inputElements = inputElements; this._hasCheckboxes = this.parent.classList.contains('show-checkboxes'); let currentSeparatorElement; this._itemElements = new Array(); this._elementTree = inputElements.reduce((result, item, index) => { let element; if (item.type === 'separator') { if (!item.buttons) { // This separator will be rendered as a part of the list item return result; } currentSeparatorElement = new QuickPickSeparatorElement(index, e => this._onSeparatorButtonTriggered.fire(e), item); element = currentSeparatorElement; } else { const previous = index > 0 ? inputElements[index - 1] : undefined; let separator; if (previous && previous.type === 'separator' && !previous.buttons) { // Found an inline separator so we clear out the current separator element currentSeparatorElement = undefined; separator = previous; } const qpi = new QuickPickItemElement(index, this._hasCheckboxes, e => this._onButtonTriggered.fire(e), this._elementChecked, item, separator); this._itemElements.push(qpi); if (currentSeparatorElement) { currentSeparatorElement.children.push(qpi); return result; } element = qpi; } result.push(element); return result; }, new Array()); this._setElementsToTree(this._elementTree); // Accessibility hack, unfortunately on next tick // https://github.com/microsoft/vscode/issues/211976 if (this.accessibilityService.isScreenReaderOptimized()) { setTimeout(() => { const focusedElement = this._tree.getHTMLElement().querySelector(`.monaco-list-row.focused`); const parent = focusedElement?.parentNode; if (focusedElement && parent) { const nextSibling = focusedElement.nextSibling; focusedElement.remove(); parent.insertBefore(focusedElement, nextSibling); } }, 0); } } setFocusedElements(items) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) .filter((e) => !!e) .filter(e => !e.hidden); this._tree.setFocus(elements); if (items.length > 0) { const focused = this._tree.getFocus()[0]; if (focused) { this._tree.reveal(focused); } } } getActiveDescendant() { return this._tree.getHTMLElement().getAttribute('aria-activedescendant'); } setSelectedElements(items) { const elements = items.map(item => this._itemElements.find(e => e.item === item)) .filter((e) => !!e); this._tree.setSelection(elements); } getCheckedElements() { return this._itemElements.filter(e => e.checked) .map(e => e.item); } setCheckedElements(items) { this._elementCheckedEventBufferer.bufferEvents(() => { const checked = new Set(); for (const item of items) { checked.add(item); } for (const element of this._itemElements) { // Would fire an event if we didn't beffer the events element.checked = checked.has(element.item); } }); } focus(what) { if (!this._itemElements.length) { return; } if (what === QuickPickFocus.Second && this._itemElements.length < 2) { what = QuickPickFocus.First; } switch (what) { case QuickPickFocus.First: this._tree.scrollTop = 0; this._tree.focusFirst(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickPickFocus.Second: { this._tree.scrollTop = 0; let isSecondItem = false; this._tree.focusFirst(undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } if (isSecondItem) { return true; } isSecondItem = !isSecondItem; return false; }); break; } case QuickPickFocus.Last: this._tree.scrollTop = this._tree.scrollHeight; this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickPickFocus.Next: { const prevFocus = this._tree.getFocus(); this._tree.focusNext(undefined, this._shouldLoop, undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } this._tree.reveal(e.element); return true; }); const currentFocus = this._tree.getFocus(); if (prevFocus.length && prevFocus[0] === currentFocus[0] && prevFocus[0] === this._itemElements[this._itemElements.length - 1]) { this._onLeave.fire(); } break; } case QuickPickFocus.Previous: { const prevFocus = this._tree.getFocus(); this._tree.focusPrevious(undefined, this._shouldLoop, undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } const parent = this._tree.getParentElement(e.element); if (parent === null || parent.children[0] !== e.element) { this._tree.reveal(e.element); } else { // Only if we are the first child of a separator do we reveal the separator this._tree.reveal(parent); } return true; }); const currentFocus = this._tree.getFocus(); if (prevFocus.length && prevFocus[0] === currentFocus[0] && prevFocus[0] === this._itemElements[0]) { this._onLeave.fire(); } break; } case QuickPickFocus.NextPage: this._tree.focusNextPage(undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } this._tree.reveal(e.element); return true; }); break; case QuickPickFocus.PreviousPage: this._tree.focusPreviousPage(undefined, (e) => { if (!(e.element instanceof QuickPickItemElement)) { return false; } const parent = this._tree.getParentElement(e.element); if (parent === null || parent.children[0] !== e.element) { this._tree.reveal(e.element); } else { this._tree.reveal(parent); } return true; }); break; case QuickPickFocus.NextSeparator: { let foundSeparatorAsItem = false; const before = this._tree.getFocus()[0]; this._tree.focusNext(undefined, true, undefined, (e) => { if (foundSeparatorAsItem) { // This should be the index right after the separator so it // is the item we want to focus. return true; } if (e.element instanceof QuickPickSeparatorElement) { foundSeparatorAsItem = true; // If the separator is visible, then we should just reveal its first child so it's not as jarring. if (this._separatorRenderer.isSeparatorVisible(e.element)) { this._tree.reveal(e.element.children[0]); } else { // If the separator is not visible, then we should // push it up to the top of the list. this._tree.reveal(e.element, 0); } } else if (e.element instanceof QuickPickItemElement) { if (e.element.separator) { if (this._itemRenderer.isItemWithSeparatorVisible(e.element)) { this._tree.reveal(e.element); } else { this._tree.reveal(e.element, 0); } return true; } else if (e.element === this._elementTree[0]) { // We should stop at the first item in the list if it's a regular item. this._tree.reveal(e.element, 0); return true; } } return false; }); const after = this._tree.getFocus()[0]; if (before === after) { // If we didn't move, then we should just move to the end // of the list. this._tree.scrollTop = this._tree.scrollHeight; this._tree.focusLast(undefined, (e) => e.element instanceof QuickPickItemElement); } break; } case QuickPickFocus.PreviousSeparator: { let focusElement; // If we are already sitting on an inline separator, then we // have already found the _current_ separator and need to // move to the previous one. let foundSeparator = !!this._tree.getFocus()[0]?.separator; this._tree.focusPrevious(undefined, true, undefined, (e) => { if (e.element instanceof QuickPickSeparatorElement) { if (foundSeparator) { if (!focusElement) { if (this._separatorRenderer.isSeparatorVisible(e.element)) { this._tree.reveal(e.element); } else { this._tree.reveal(e.element, 0); } focusElement = e.element.children[0]; } } else { foundSeparator = true; } } else if (e.element instanceof QuickPickItemElement) { if (!focusElement) { if (e.element.separator) { if (this._itemRenderer.isItemWithSeparatorVisible(e.element)) { this._tree.reveal(e.element); } else { this._tree.reveal(e.element, 0); } focusElement = e.element; } else if (e.element === this._elementTree[0]) { // We should stop at the first item in the list if it's a regular item. this._tree.reveal(e.element, 0); return true; } } } return false; }); if (focusElement) { this._tree.setFocus([focusElement]); } break; } } } clearFocus() { this._tree.setFocus([]); } domFocus() { this._tree.domFocus(); } layout(maxHeight) { this._tree.getHTMLElement().style.maxHeight = maxHeight ? `${ // Make sure height aligns with list item heights Math.floor(maxHeight / 44) * 44 // Add some extra height so that it's clear there's more to scroll + 6}px` : ''; this._tree.layout(); } filter(query) { this._lastQueryString = query; if (!(this._sortByLabel || this._matchOnLabel || this._matchOnDescription || this._matchOnDetail)) { this._tree.layout(); return false; } const queryWithWhitespace = query; query = query.trim(); // Reset filtering if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { this._itemElements.forEach(element => { element.labelHighlights = undefined; element.descriptionHighlights = undefined; element.detailHighlights = undefined; element.hidden = false; const previous = element.index && this._inputElements[element.index - 1]; if (element.item) { element.separator = previous && previous.type === 'separator' && !previous.butto