monaco-editor-core
Version:
A browser based code editor
1,164 lines • 59.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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