sussudio
Version:
An unofficial VS Code Internal API
1,238 lines (1,237 loc) • 66.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from "../../../browser/dom.mjs";
import { StandardKeyboardEvent } from "../../../browser/keyboardEvent.mjs";
import { ActionBar } from "../../../browser/ui/actionbar/actionbar.mjs";
import { Button } from "../../../browser/ui/button/button.mjs";
import { CountBadge } from "../../../browser/ui/countBadge/countBadge.mjs";
import { renderLabelWithIcons } from "../../../browser/ui/iconLabel/iconLabels.mjs";
import { ProgressBar } from "../../../browser/ui/progressbar/progressbar.mjs";
import { Toggle } from "../../../browser/ui/toggle/toggle.mjs";
import { Action } from "../../../common/actions.mjs";
import { equals } from "../../../common/arrays.mjs";
import { TimeoutTimer } from "../../../common/async.mjs";
import { CancellationToken } from "../../../common/cancellation.mjs";
import { Codicon } from "../../../common/codicons.mjs";
import { Emitter, Event } from "../../../common/event.mjs";
import { Disposable, DisposableStore, dispose } from "../../../common/lifecycle.mjs";
import { isIOS } from "../../../common/platform.mjs";
import Severity from "../../../common/severity.mjs";
import { isString, withNullAsUndefined } from "../../../common/types.mjs";
import { getIconClass } from "./quickInputUtils.mjs";
import { ItemActivation, NO_KEY_MODS, QuickInputHideReason } from "../common/quickInput.mjs";
import "../../../../css!./media/quickInput.mjs";
import { localize } from "../../../../nls.mjs";
import { QuickInputBox } from './quickInputBox';
import { QuickInputList, QuickInputListFocus } from './quickInputList';
const $ = dom.$;
const backButton = {
iconClass: Codicon.quickInputBack.classNames,
tooltip: localize('quickInput.back', "Back"),
handle: -1 // TODO
};
class QuickInput extends Disposable {
ui;
static noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel");
_title;
_description;
_steps;
_totalSteps;
visible = false;
_enabled = true;
_contextKey;
_busy = false;
_ignoreFocusOut = false;
_buttons = [];
buttonsUpdated = false;
_toggles = [];
togglesUpdated = false;
noValidationMessage = QuickInput.noPromptMessage;
_validationMessage;
_lastValidationMessage;
_severity = Severity.Ignore;
_lastSeverity;
onDidTriggerButtonEmitter = this._register(new Emitter());
onDidHideEmitter = this._register(new Emitter());
onDisposeEmitter = this._register(new Emitter());
visibleDisposables = this._register(new DisposableStore());
busyDelay;
constructor(ui) {
super();
this.ui = ui;
}
get title() {
return this._title;
}
set title(title) {
this._title = title;
this.update();
}
get description() {
return this._description;
}
set description(description) {
this._description = description;
this.update();
}
get step() {
return this._steps;
}
set step(step) {
this._steps = step;
this.update();
}
get totalSteps() {
return this._totalSteps;
}
set totalSteps(totalSteps) {
this._totalSteps = totalSteps;
this.update();
}
get enabled() {
return this._enabled;
}
set enabled(enabled) {
this._enabled = enabled;
this.update();
}
get contextKey() {
return this._contextKey;
}
set contextKey(contextKey) {
this._contextKey = contextKey;
this.update();
}
get busy() {
return this._busy;
}
set busy(busy) {
this._busy = busy;
this.update();
}
get ignoreFocusOut() {
return this._ignoreFocusOut;
}
set ignoreFocusOut(ignoreFocusOut) {
const shouldUpdate = this._ignoreFocusOut !== ignoreFocusOut && !isIOS;
this._ignoreFocusOut = ignoreFocusOut && !isIOS;
if (shouldUpdate) {
this.update();
}
}
get buttons() {
return this._buttons;
}
set buttons(buttons) {
this._buttons = buttons;
this.buttonsUpdated = true;
this.update();
}
get toggles() {
return this._toggles;
}
set toggles(toggles) {
this._toggles = toggles ?? [];
this.togglesUpdated = true;
this.update();
}
get validationMessage() {
return this._validationMessage;
}
set validationMessage(validationMessage) {
this._validationMessage = validationMessage;
this.update();
}
get severity() {
return this._severity;
}
set severity(severity) {
this._severity = severity;
this.update();
}
onDidTriggerButton = this.onDidTriggerButtonEmitter.event;
show() {
if (this.visible) {
return;
}
this.visibleDisposables.add(this.ui.onDidTriggerButton(button => {
if (this.buttons.indexOf(button) !== -1) {
this.onDidTriggerButtonEmitter.fire(button);
}
}));
this.ui.show(this);
// update properties in the controller that get reset in the ui.show() call
this.visible = true;
// This ensures the message/prompt gets rendered
this._lastValidationMessage = undefined;
// This ensures the input box has the right severity applied
this._lastSeverity = undefined;
if (this.buttons.length) {
// if there are buttons, the ui.show() clears them out of the UI so we should
// rerender them.
this.buttonsUpdated = true;
}
if (this.toggles.length) {
// if there are toggles, the ui.show() clears them out of the UI so we should
// rerender them.
this.togglesUpdated = true;
}
this.update();
}
hide() {
if (!this.visible) {
return;
}
this.ui.hide();
}
didHide(reason = QuickInputHideReason.Other) {
this.visible = false;
this.visibleDisposables.clear();
this.onDidHideEmitter.fire({ reason });
}
onDidHide = this.onDidHideEmitter.event;
update() {
if (!this.visible) {
return;
}
const title = this.getTitle();
if (title && this.ui.title.textContent !== title) {
this.ui.title.textContent = title;
}
else if (!title && this.ui.title.innerHTML !== ' ') {
this.ui.title.innerText = '\u00a0';
}
const description = this.getDescription();
if (this.ui.description1.textContent !== description) {
this.ui.description1.textContent = description;
}
if (this.ui.description2.textContent !== description) {
this.ui.description2.textContent = description;
}
if (this.busy && !this.busyDelay) {
this.busyDelay = new TimeoutTimer();
this.busyDelay.setIfNotSet(() => {
if (this.visible) {
this.ui.progressBar.infinite();
}
}, 800);
}
if (!this.busy && this.busyDelay) {
this.ui.progressBar.stop();
this.busyDelay.cancel();
this.busyDelay = undefined;
}
if (this.buttonsUpdated) {
this.buttonsUpdated = false;
this.ui.leftActionBar.clear();
const leftButtons = this.buttons.filter(button => button === backButton);
this.ui.leftActionBar.push(leftButtons.map((button, index) => {
const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => {
this.onDidTriggerButtonEmitter.fire(button);
});
action.tooltip = button.tooltip || '';
return action;
}), { icon: true, label: false });
this.ui.rightActionBar.clear();
const rightButtons = this.buttons.filter(button => button !== backButton);
this.ui.rightActionBar.push(rightButtons.map((button, index) => {
const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => {
this.onDidTriggerButtonEmitter.fire(button);
});
action.tooltip = button.tooltip || '';
return action;
}), { icon: true, label: false });
}
if (this.togglesUpdated) {
this.togglesUpdated = false;
// HACK: Filter out toggles here that are not concrete Toggle objects. This is to workaround
// a layering issue as quick input's interface is in common but Toggle is in browser and
// it requires a HTMLElement on its interface
const concreteToggles = this.toggles?.filter(opts => opts instanceof Toggle) ?? [];
this.ui.inputBox.toggles = concreteToggles;
}
this.ui.ignoreFocusOut = this.ignoreFocusOut;
this.ui.setEnabled(this.enabled);
this.ui.setContextKey(this.contextKey);
const validationMessage = this.validationMessage || this.noValidationMessage;
if (this._lastValidationMessage !== validationMessage) {
this._lastValidationMessage = validationMessage;
dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage));
}
if (this._lastSeverity !== this.severity) {
this._lastSeverity = this.severity;
this.showMessageDecoration(this.severity);
}
}
getTitle() {
if (this.title && this.step) {
return `${this.title} (${this.getSteps()})`;
}
if (this.title) {
return this.title;
}
if (this.step) {
return this.getSteps();
}
return '';
}
getDescription() {
return this.description || '';
}
getSteps() {
if (this.step && this.totalSteps) {
return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps);
}
if (this.step) {
return String(this.step);
}
return '';
}
showMessageDecoration(severity) {
this.ui.inputBox.showDecoration(severity);
if (severity !== Severity.Ignore) {
const styles = this.ui.inputBox.stylesForType(severity);
this.ui.message.style.color = styles.foreground ? `${styles.foreground}` : '';
this.ui.message.style.backgroundColor = styles.background ? `${styles.background}` : '';
this.ui.message.style.border = styles.border ? `1px solid ${styles.border}` : '';
this.ui.message.style.marginBottom = '-2px';
}
else {
this.ui.message.style.color = '';
this.ui.message.style.backgroundColor = '';
this.ui.message.style.border = '';
this.ui.message.style.marginBottom = '';
}
}
onDispose = this.onDisposeEmitter.event;
dispose() {
this.hide();
this.onDisposeEmitter.fire();
super.dispose();
}
}
class QuickPick extends QuickInput {
static DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
_value = '';
_ariaLabel;
_placeholder;
onDidChangeValueEmitter = this._register(new Emitter());
onWillAcceptEmitter = this._register(new Emitter());
onDidAcceptEmitter = this._register(new Emitter());
onDidCustomEmitter = this._register(new Emitter());
_items = [];
itemsUpdated = false;
_canSelectMany = false;
_canAcceptInBackground = false;
_matchOnDescription = false;
_matchOnDetail = false;
_matchOnLabel = true;
_matchOnLabelMode = 'fuzzy';
_sortByLabel = true;
_autoFocusOnList = true;
_keepScrollPosition = false;
_itemActivation = this.ui.isScreenReaderOptimized() ? ItemActivation.NONE /* https://github.com/microsoft/vscode/issues/57501 */ : ItemActivation.FIRST;
_activeItems = [];
activeItemsUpdated = false;
activeItemsToConfirm = [];
onDidChangeActiveEmitter = this._register(new Emitter());
_selectedItems = [];
selectedItemsUpdated = false;
selectedItemsToConfirm = [];
onDidChangeSelectionEmitter = this._register(new Emitter());
onDidTriggerItemButtonEmitter = this._register(new Emitter());
onDidTriggerSeparatorButtonEmitter = this._register(new Emitter());
_valueSelection;
valueSelectionUpdated = true;
_ok = 'default';
_customButton = false;
_customButtonLabel;
_customButtonHover;
_quickNavigate;
_hideInput;
_hideCheckAll;
get quickNavigate() {
return this._quickNavigate;
}
set quickNavigate(quickNavigate) {
this._quickNavigate = quickNavigate;
this.update();
}
get value() {
return this._value;
}
set value(value) {
this.doSetValue(value);
}
doSetValue(value, skipUpdate) {
if (this._value !== value) {
this._value = value;
if (!skipUpdate) {
this.update();
}
if (this.visible) {
const didFilter = this.ui.list.filter(this.filterValue(this._value));
if (didFilter) {
this.trySelectFirst();
}
}
this.onDidChangeValueEmitter.fire(this._value);
}
}
filterValue = (value) => value;
set ariaLabel(ariaLabel) {
this._ariaLabel = ariaLabel;
this.update();
}
get ariaLabel() {
return this._ariaLabel;
}
get placeholder() {
return this._placeholder;
}
set placeholder(placeholder) {
this._placeholder = placeholder;
this.update();
}
onDidChangeValue = this.onDidChangeValueEmitter.event;
onWillAccept = this.onWillAcceptEmitter.event;
onDidAccept = this.onDidAcceptEmitter.event;
onDidCustom = this.onDidCustomEmitter.event;
get items() {
return this._items;
}
get scrollTop() {
return this.ui.list.scrollTop;
}
set scrollTop(scrollTop) {
this.ui.list.scrollTop = scrollTop;
}
set items(items) {
this._items = items;
this.itemsUpdated = true;
this.update();
}
get canSelectMany() {
return this._canSelectMany;
}
set canSelectMany(canSelectMany) {
this._canSelectMany = canSelectMany;
this.update();
}
get canAcceptInBackground() {
return this._canAcceptInBackground;
}
set canAcceptInBackground(canAcceptInBackground) {
this._canAcceptInBackground = canAcceptInBackground;
}
get matchOnDescription() {
return this._matchOnDescription;
}
set matchOnDescription(matchOnDescription) {
this._matchOnDescription = matchOnDescription;
this.update();
}
get matchOnDetail() {
return this._matchOnDetail;
}
set matchOnDetail(matchOnDetail) {
this._matchOnDetail = matchOnDetail;
this.update();
}
get matchOnLabel() {
return this._matchOnLabel;
}
set matchOnLabel(matchOnLabel) {
this._matchOnLabel = matchOnLabel;
this.update();
}
get matchOnLabelMode() {
return this._matchOnLabelMode;
}
set matchOnLabelMode(matchOnLabelMode) {
this._matchOnLabelMode = matchOnLabelMode;
this.update();
}
get sortByLabel() {
return this._sortByLabel;
}
set sortByLabel(sortByLabel) {
this._sortByLabel = sortByLabel;
this.update();
}
get autoFocusOnList() {
return this._autoFocusOnList;
}
set autoFocusOnList(autoFocusOnList) {
this._autoFocusOnList = autoFocusOnList;
this.update();
}
get keepScrollPosition() {
return this._keepScrollPosition;
}
set keepScrollPosition(keepScrollPosition) {
this._keepScrollPosition = keepScrollPosition;
}
get itemActivation() {
return this._itemActivation;
}
set itemActivation(itemActivation) {
this._itemActivation = itemActivation;
}
get activeItems() {
return this._activeItems;
}
set activeItems(activeItems) {
this._activeItems = activeItems;
this.activeItemsUpdated = true;
this.update();
}
onDidChangeActive = this.onDidChangeActiveEmitter.event;
get selectedItems() {
return this._selectedItems;
}
set selectedItems(selectedItems) {
this._selectedItems = selectedItems;
this.selectedItemsUpdated = true;
this.update();
}
get keyMods() {
if (this._quickNavigate) {
// Disable keyMods when quick navigate is enabled
// because in this model the interaction is purely
// keyboard driven and Ctrl/Alt are typically
// pressed and hold during this interaction.
return NO_KEY_MODS;
}
return this.ui.keyMods;
}
set valueSelection(valueSelection) {
this._valueSelection = valueSelection;
this.valueSelectionUpdated = true;
this.update();
}
get customButton() {
return this._customButton;
}
set customButton(showCustomButton) {
this._customButton = showCustomButton;
this.update();
}
get customLabel() {
return this._customButtonLabel;
}
set customLabel(label) {
this._customButtonLabel = label;
this.update();
}
get customHover() {
return this._customButtonHover;
}
set customHover(hover) {
this._customButtonHover = hover;
this.update();
}
get ok() {
return this._ok;
}
set ok(showOkButton) {
this._ok = showOkButton;
this.update();
}
inputHasFocus() {
return this.visible ? this.ui.inputBox.hasFocus() : false;
}
focusOnInput() {
this.ui.inputBox.setFocus();
}
get hideInput() {
return !!this._hideInput;
}
set hideInput(hideInput) {
this._hideInput = hideInput;
this.update();
}
get hideCheckAll() {
return !!this._hideCheckAll;
}
set hideCheckAll(hideCheckAll) {
this._hideCheckAll = hideCheckAll;
this.update();
}
onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;
onDidTriggerSeparatorButton = this.onDidTriggerSeparatorButtonEmitter.event;
trySelectFirst() {
if (this.autoFocusOnList) {
if (!this.canSelectMany) {
this.ui.list.focus(QuickInputListFocus.First);
}
}
}
show() {
if (!this.visible) {
this.visibleDisposables.add(this.ui.inputBox.onDidChange(value => {
this.doSetValue(value, true /* skip update since this originates from the UI */);
}));
this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => {
if (!this.autoFocusOnList) {
this.ui.list.clearFocus();
}
}));
this.visibleDisposables.add((this._hideInput ? this.ui.list : this.ui.inputBox).onKeyDown((event) => {
switch (event.keyCode) {
case 18 /* KeyCode.DownArrow */:
this.ui.list.focus(QuickInputListFocus.Next);
if (this.canSelectMany) {
this.ui.list.domFocus();
}
dom.EventHelper.stop(event, true);
break;
case 16 /* KeyCode.UpArrow */:
if (this.ui.list.getFocusedElements().length) {
this.ui.list.focus(QuickInputListFocus.Previous);
}
else {
this.ui.list.focus(QuickInputListFocus.Last);
}
if (this.canSelectMany) {
this.ui.list.domFocus();
}
dom.EventHelper.stop(event, true);
break;
case 12 /* KeyCode.PageDown */:
this.ui.list.focus(QuickInputListFocus.NextPage);
if (this.canSelectMany) {
this.ui.list.domFocus();
}
dom.EventHelper.stop(event, true);
break;
case 11 /* KeyCode.PageUp */:
this.ui.list.focus(QuickInputListFocus.PreviousPage);
if (this.canSelectMany) {
this.ui.list.domFocus();
}
dom.EventHelper.stop(event, true);
break;
case 17 /* KeyCode.RightArrow */:
if (!this._canAcceptInBackground) {
return; // needs to be enabled
}
if (!this.ui.inputBox.isSelectionAtEnd()) {
return; // ensure input box selection at end
}
if (this.activeItems[0]) {
this._selectedItems = [this.activeItems[0]];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
this.handleAccept(true);
}
break;
case 14 /* KeyCode.Home */:
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
this.ui.list.focus(QuickInputListFocus.First);
dom.EventHelper.stop(event, true);
}
break;
case 13 /* KeyCode.End */:
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
this.ui.list.focus(QuickInputListFocus.Last);
dom.EventHelper.stop(event, true);
}
break;
}
}));
this.visibleDisposables.add(this.ui.onDidAccept(() => {
if (this.canSelectMany) {
// if there are no checked elements, it means that an onDidChangeSelection never fired to overwrite
// `_selectedItems`. In that case, we should emit one with an empty array to ensure that
// `.selectedItems` is up to date.
if (!this.ui.list.getCheckedElements().length) {
this._selectedItems = [];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
}
}
else if (this.activeItems[0]) {
// For single-select, we set `selectedItems` to the item that was accepted.
this._selectedItems = [this.activeItems[0]];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
}
this.handleAccept(false);
}));
this.visibleDisposables.add(this.ui.onDidCustom(() => {
this.onDidCustomEmitter.fire();
}));
this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => {
if (this.activeItemsUpdated) {
return; // Expect another event.
}
if (this.activeItemsToConfirm !== this._activeItems && equals(focusedItems, this._activeItems, (a, b) => a === b)) {
return;
}
this._activeItems = focusedItems;
this.onDidChangeActiveEmitter.fire(focusedItems);
}));
this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => {
if (this.canSelectMany) {
if (selectedItems.length) {
this.ui.list.setSelectedElements([]);
}
return;
}
if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) {
return;
}
this._selectedItems = selectedItems;
this.onDidChangeSelectionEmitter.fire(selectedItems);
if (selectedItems.length) {
this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */);
}
}));
this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => {
if (!this.canSelectMany) {
return;
}
if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) {
return;
}
this._selectedItems = checkedItems;
this.onDidChangeSelectionEmitter.fire(checkedItems);
}));
this.visibleDisposables.add(this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event)));
this.visibleDisposables.add(this.ui.list.onSeparatorButtonTriggered(event => this.onDidTriggerSeparatorButtonEmitter.fire(event)));
this.visibleDisposables.add(this.registerQuickNavigation());
this.valueSelectionUpdated = true;
}
super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.)
}
handleAccept(inBackground) {
// Figure out veto via `onWillAccept` event
let veto = false;
this.onWillAcceptEmitter.fire({ veto: () => veto = true });
// Continue with `onDidAccept` if no veto
if (!veto) {
this.onDidAcceptEmitter.fire({ inBackground });
}
}
registerQuickNavigation() {
return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
if (this.canSelectMany || !this._quickNavigate) {
return;
}
const keyboardEvent = new StandardKeyboardEvent(e);
const keyCode = keyboardEvent.keyCode;
// Select element when keys are pressed that signal it
const quickNavKeys = this._quickNavigate.keybindings;
const wasTriggerKeyPressed = quickNavKeys.some(k => {
const [firstChord, secondChord] = k.getChords(); // TODO@chords
if (secondChord) {
return false;
}
if (firstChord.shiftKey && keyCode === 4 /* KeyCode.Shift */) {
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
return false; // this is an optimistic check for the shift key being used to navigate back in quick input
}
return true;
}
if (firstChord.altKey && keyCode === 6 /* KeyCode.Alt */) {
return true;
}
if (firstChord.ctrlKey && keyCode === 5 /* KeyCode.Ctrl */) {
return true;
}
if (firstChord.metaKey && keyCode === 57 /* KeyCode.Meta */) {
return true;
}
return false;
});
if (wasTriggerKeyPressed) {
if (this.activeItems[0]) {
this._selectedItems = [this.activeItems[0]];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
this.handleAccept(false);
}
// Unset quick navigate after press. It is only valid once
// and should not result in any behaviour change afterwards
// if the picker remains open because there was no active item
this._quickNavigate = undefined;
}
});
}
update() {
if (!this.visible) {
return;
}
// store the scrollTop before it is reset
const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0;
const hasDescription = !!this.description;
const visibilities = {
title: !!this.title || !!this.step || !!this.buttons.length,
description: hasDescription,
checkAll: this.canSelectMany && !this._hideCheckAll,
checkBox: this.canSelectMany,
inputBox: !this._hideInput,
progressBar: !this._hideInput || hasDescription,
visibleCount: true,
count: this.canSelectMany,
ok: this.ok === 'default' ? this.canSelectMany : this.ok,
list: true,
message: !!this.validationMessage,
customButton: this.customButton
};
this.ui.setVisibilities(visibilities);
super.update();
if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value;
}
if (this.valueSelectionUpdated) {
this.valueSelectionUpdated = false;
this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
}
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
this.ui.inputBox.placeholder = (this.placeholder || '');
}
let ariaLabel = this.ariaLabel;
if (!ariaLabel) {
ariaLabel = this.placeholder || QuickPick.DEFAULT_ARIA_LABEL;
// If we have a title, include it in the aria label.
if (this.title) {
ariaLabel += ` - ${this.title}`;
}
}
if (this.ui.inputBox.ariaLabel !== ariaLabel) {
this.ui.inputBox.ariaLabel = ariaLabel;
}
this.ui.list.matchOnDescription = this.matchOnDescription;
this.ui.list.matchOnDetail = this.matchOnDetail;
this.ui.list.matchOnLabel = this.matchOnLabel;
this.ui.list.matchOnLabelMode = this.matchOnLabelMode;
this.ui.list.sortByLabel = this.sortByLabel;
if (this.itemsUpdated) {
this.itemsUpdated = false;
this.ui.list.setElements(this.items);
this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
this.ui.count.setCount(this.ui.list.getCheckedCount());
switch (this._itemActivation) {
case ItemActivation.NONE:
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
break;
case ItemActivation.SECOND:
this.ui.list.focus(QuickInputListFocus.Second);
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
break;
case ItemActivation.LAST:
this.ui.list.focus(QuickInputListFocus.Last);
this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
break;
default:
this.trySelectFirst();
break;
}
}
if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
if (this.canSelectMany) {
this.ui.list.clearFocus();
}
else {
this.trySelectFirst();
}
}
if (this.activeItemsUpdated) {
this.activeItemsUpdated = false;
this.activeItemsToConfirm = this._activeItems;
this.ui.list.setFocusedElements(this.activeItems);
if (this.activeItemsToConfirm === this._activeItems) {
this.activeItemsToConfirm = null;
}
}
if (this.selectedItemsUpdated) {
this.selectedItemsUpdated = false;
this.selectedItemsToConfirm = this._selectedItems;
if (this.canSelectMany) {
this.ui.list.setCheckedElements(this.selectedItems);
}
else {
this.ui.list.setSelectedElements(this.selectedItems);
}
if (this.selectedItemsToConfirm === this._selectedItems) {
this.selectedItemsToConfirm = null;
}
}
this.ui.customButton.label = this.customLabel || '';
this.ui.customButton.element.title = this.customHover || '';
this.ui.setComboboxAccessibility(true);
if (!visibilities.inputBox) {
// we need to move focus into the tree to detect keybindings
// properly when the input box is not visible (quick nav)
this.ui.list.domFocus();
// Focus the first element in the list if multiselect is enabled
if (this.canSelectMany) {
this.ui.list.focus(QuickInputListFocus.First);
}
}
// Set the scroll position to what it was before updating the items
if (this.keepScrollPosition) {
this.scrollTop = scrollTopBefore;
}
}
}
class InputBox extends QuickInput {
_value = '';
_valueSelection;
valueSelectionUpdated = true;
_placeholder;
_password = false;
_prompt;
onDidValueChangeEmitter = this._register(new Emitter());
onDidAcceptEmitter = this._register(new Emitter());
get value() {
return this._value;
}
set value(value) {
this._value = value || '';
this.update();
}
set valueSelection(valueSelection) {
this._valueSelection = valueSelection;
this.valueSelectionUpdated = true;
this.update();
}
get placeholder() {
return this._placeholder;
}
set placeholder(placeholder) {
this._placeholder = placeholder;
this.update();
}
get password() {
return this._password;
}
set password(password) {
this._password = password;
this.update();
}
get prompt() {
return this._prompt;
}
set prompt(prompt) {
this._prompt = prompt;
this.noValidationMessage = prompt
? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt)
: QuickInput.noPromptMessage;
this.update();
}
onDidChangeValue = this.onDidValueChangeEmitter.event;
onDidAccept = this.onDidAcceptEmitter.event;
show() {
if (!this.visible) {
this.visibleDisposables.add(this.ui.inputBox.onDidChange(value => {
if (value === this.value) {
return;
}
this._value = value;
this.onDidValueChangeEmitter.fire(value);
}));
this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()));
this.valueSelectionUpdated = true;
}
super.show();
}
update() {
if (!this.visible) {
return;
}
this.ui.container.classList.remove('hidden-input');
const visibilities = {
title: !!this.title || !!this.step || !!this.buttons.length,
description: !!this.description || !!this.step,
inputBox: true, message: true
};
this.ui.setVisibilities(visibilities);
super.update();
if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value;
}
if (this.valueSelectionUpdated) {
this.valueSelectionUpdated = false;
this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
}
if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
this.ui.inputBox.placeholder = (this.placeholder || '');
}
if (this.ui.inputBox.password !== this.password) {
this.ui.inputBox.password = this.password;
}
}
}
export class QuickInputController extends Disposable {
options;
static MAX_WIDTH = 600; // Max total width of quick input widget
idPrefix;
ui;
dimension;
titleBarOffset;
comboboxAccessibility = false;
enabled = true;
onDidAcceptEmitter = this._register(new Emitter());
onDidCustomEmitter = this._register(new Emitter());
onDidTriggerButtonEmitter = this._register(new Emitter());
keyMods = { ctrlCmd: false, alt: false };
controller = null;
parentElement;
styles;
onShowEmitter = this._register(new Emitter());
onShow = this.onShowEmitter.event;
onHideEmitter = this._register(new Emitter());
onHide = this.onHideEmitter.event;
previousFocusElement;
constructor(options) {
super();
this.options = options;
this.idPrefix = options.idPrefix;
this.parentElement = options.container;
this.styles = options.styles;
this.registerKeyModsListeners();
}
registerKeyModsListeners() {
const listener = (e) => {
this.keyMods.ctrlCmd = e.ctrlKey || e.metaKey;
this.keyMods.alt = e.altKey;
};
this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, listener, true));
this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, listener, true));
this._register(dom.addDisposableListener(window, dom.EventType.MOUSE_DOWN, listener, true));
}
getUI() {
if (this.ui) {
return this.ui;
}
const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons'));
container.tabIndex = -1;
container.style.display = 'none';
const styleSheet = dom.createStyleSheet(container);
const titleBar = dom.append(container, $('.quick-input-titlebar'));
const leftActionBar = this._register(new ActionBar(titleBar));
leftActionBar.domNode.classList.add('quick-input-left-action-bar');
const title = dom.append(titleBar, $('.quick-input-title'));
const rightActionBar = this._register(new ActionBar(titleBar));
rightActionBar.domNode.classList.add('quick-input-right-action-bar');
const description1 = dom.append(container, $('.quick-input-description'));
const headerContainer = dom.append(container, $('.quick-input-header'));
const checkAll = dom.append(headerContainer, $('input.quick-input-check-all'));
checkAll.type = 'checkbox';
checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes"));
this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => {
const checked = checkAll.checked;
list.setAllVisibleChecked(checked);
}));
this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => {
if (e.x || e.y) { // Avoid 'click' triggered by 'space'...
inputBox.setFocus();
}
}));
const description2 = dom.append(headerContainer, $('.quick-input-description'));
const extraContainer = dom.append(headerContainer, $('.quick-input-and-message'));
const filterContainer = dom.append(extraContainer, $('.quick-input-filter'));
const inputBox = this._register(new QuickInputBox(filterContainer, this.styles.inputBox, this.styles.toggle));
inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`);
const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count'));
visibleCountContainer.setAttribute('aria-live', 'polite');
visibleCountContainer.setAttribute('aria-atomic', 'true');
const visibleCount = new CountBadge(visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") }, this.styles.countBadge);
const countContainer = dom.append(filterContainer, $('.quick-input-count'));
countContainer.setAttribute('aria-live', 'polite');
const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }, this.styles.countBadge);
const okContainer = dom.append(headerContainer, $('.quick-input-action'));
const ok = new Button(okContainer, this.styles.button);
ok.label = localize('ok', "OK");
this._register(ok.onDidClick(e => {
this.onDidAcceptEmitter.fire();
}));
const customButtonContainer = dom.append(headerContainer, $('.quick-input-action'));
const customButton = new Button(customButtonContainer, this.styles.button);
customButton.label = localize('custom', "Custom");
this._register(customButton.onDidClick(e => {
this.onDidCustomEmitter.fire();
}));
const message = dom.append(extraContainer, $(`#${this.idPrefix}message.quick-input-message`));
const progressBar = new ProgressBar(container, this.styles.progressBar);
progressBar.getContainer().classList.add('quick-input-progress');
const list = this._register(new QuickInputList(container, this.idPrefix + 'list', this.options));
this._register(list.onChangedAllVisibleChecked(checked => {
checkAll.checked = checked;
}));
this._register(list.onChangedVisibleCount(c => {
visibleCount.setCount(c);
}));
this._register(list.onChangedCheckedCount(c => {
count.setCount(c);
}));
this._register(list.onLeave(() => {
// Defer to avoid the input field reacting to the triggering key.
setTimeout(() => {
inputBox.setFocus();
if (this.controller instanceof QuickPick && this.controller.canSelectMany) {
list.clearFocus();
}
}, 0);
}));
this._register(list.onDidChangeFocus(() => {
if (this.comboboxAccessibility) {
this.getUI().inputBox.setAttribute('aria-activedescendant', this.getUI().list.getActiveDescendant() || '');
}
}));
const focusTracker = dom.trackFocus(container);
this._register(focusTracker);
this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => {
this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined;
}, true));
this._register(focusTracker.onDidBlur(() => {
if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) {
this.hide(QuickInputHideReason.Blur);
}
this.previousFocusElement = undefined;
}));
this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e) => {
inputBox.setFocus();
}));
this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e) => {
const event = new StandardKeyboardEvent(e);
switch (event.keyCode) {
case 3 /* KeyCode.Enter */:
dom.EventHelper.stop(e, true);
if (this.enabled) {
this.onDidAcceptEmitter.fire();
}
break;
case 9 /* KeyCode.Escape */:
dom.EventHelper.stop(e, true);
this.hide(QuickInputHideReason.Gesture);
break;
case 2 /* KeyCode.Tab */:
if (!event.altKey && !event.ctrlKey && !event.metaKey) {
const selectors = ['.action-label.codicon'];
if (container.classList.contains('show-checkboxes')) {
selectors.push('input');
}
else {
selectors.push('input[type=text]');
}
if (this.getUI().list.isDisplayed()) {
selectors.push('.monaco-list');
}
const stops = container.querySelectorAll(selectors.join(', '));
if (event.shiftKey && event.target === stops[0]) {
dom.EventHelper.stop(e, true);
stops[stops.length - 1].focus();
}
else if (!event.shiftKey && event.target === stops[stops.length - 1]) {
dom.EventHelper.stop(e, true);
stops[0].focus();
}
}
break;
}
}));
this.ui = {
container,
styleSheet,
leftActionBar,
titleBar,
title,
description1,
description2,
rightActionBar,
checkAll,
filterContainer,
inputBox,
visibleCountContainer,
visibleCount,
countContainer,
count,
okContainer,
ok,
message,
customButtonContainer,
customButton,
list,
progressBar,
onDidAccept: this.onDidAcceptEmitter.event,
onDidCustom: this.onDidCustomEmitter.event,
onDidTriggerButton: this.onDidTriggerButtonEmitter.event,
ignoreFocusOut: false,
keyMods: this.keyMods,
isScreenReaderOptimized: () => this.options.isScreenReaderOptimized(),
show: controller => this.show(controller),
hide: () => this.hide(),
setVisibilities: visibilities => this.setVisibilities(visibilities),
setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled),
setEnabled: enabled => this.setEnabled(enabled),
setContextKey: contextKey => this.options.setContextKey(contextKey),
};
this.updateStyles();
return this.ui;
}
pick(picks, options = {}, token = CancellationToken.None) {
return new Promise((doResolve, reject) => {
let resolve = (result) => {
resolve = doResolve;
options.onKeyMods?.(input.keyMods);
doResolve(result);
};
if (token.isCancellationRequested) {
resolve(undefined);
return;
}
const input = this.createQuickPick();
let activeItem;
const disposables = [
input,
input.onDidAccept(() => {
if (input.canSelectMany) {
resolve(input.selectedItems.slice());
input.hide();
}
else {
const result = input.activeItems[0];
if (result) {
resolve(result);
input.hide();
}
}
}),
input.onDidChangeActive(items => {
const focused = items[0];
if (focused && options.onDidFocus) {
options.onDidFocus(focused);
}
}),
input.onDidChangeSelection(items => {
if (!input.canSelectMany) {
const result = items[0];
if (result) {
resolve(result);
input.hide();
}
}
}),
input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({
...event,
removeItem: () => {
const index = input.items.indexOf(event.item);
if (index !== -1) {
const items = input.items.slice();
const removed = items.splice(index, 1);
const activeItems = input.activeItems.filter(activeItem => activeItem !== removed[0]);
const keepScrollPositionBefore = input.keepScrollPosition;
input.keepScrollPosition = true;
input.items = items;
if (activeItems) {
input.activeItems = activeItems;
}
input.keepScrollPosition = keepScrollPositionBefore;
}
}
})),