diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index ba7ce21e32f..4f8331c286f 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts @@ -139,9 +139,9 @@ suite('vscode API - quick input', function () { }; const quickPick = createQuickPick({ - events: ['active', 'selection', 'accept', 'active', 'selection', 'active', 'selection', 'accept', 'hide'], - activeItems: [['eins'], [], ['drei']], - selectionItems: [['eins'], [], ['drei']], + events: ['active', 'selection', 'accept', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], ['drei']], + selectionItems: [['eins'], ['drei']], acceptedItems: { active: [['eins'], ['drei']], selection: [['eins'], ['drei']], diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 87e8f52fbc6..00b1c0b7111 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -2438,6 +2438,8 @@ export abstract class AbstractTree implements IDisposable get onMouseClick(): Event> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } get onMouseDblClick(): Event> { return Event.filter(Event.map(this.view.onMouseDblClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); } + get onMouseOver(): Event> { return Event.map(this.view.onMouseOver, asTreeMouseEvent); } + get onMouseOut(): Event> { return Event.map(this.view.onMouseOut, asTreeMouseEvent); } get onContextMenu(): Event> { return Event.any(Event.filter(Event.map(this.view.onContextMenu, asTreeContextMenuEvent), e => !e.isStickyScroll), this.stickyScrollController?.onContextMenu ?? Event.None); } get onTap(): Event> { return Event.map(this.view.onTap, asTreeMouseEvent); } get onPointer(): Event> { return Event.map(this.view.onPointer, asTreeMouseEvent); } @@ -2876,27 +2878,27 @@ export abstract class AbstractTree implements IDisposable }); } - focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusNext(n, loop, browserEvent, filter); } - focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusPrevious(n, loop, browserEvent, filter); } - focusNextPage(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { + focusNextPage(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { return this.view.focusNextPage(browserEvent, filter); } - focusPreviousPage(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { + focusPreviousPage(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): Promise { return this.view.focusPreviousPage(browserEvent, filter, () => this.stickyScrollController?.height ?? 0); } - focusLast(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusLast(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusLast(browserEvent, filter); } - focusFirst(browserEvent?: UIEvent, filter = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { + focusFirst(browserEvent?: UIEvent, filter: ((node: ITreeNode) => boolean) | undefined = (isKeyboardEvent(browserEvent) && browserEvent.altKey) ? undefined : this.focusNavigationFilter): void { this.view.focusFirst(browserEvent, filter); } diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 929cb32d5a8..beaf0b894e6 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ContextView, ContextViewDOMPosition, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IContextViewDelegate, IContextViewService } from './contextView'; import { getWindow } from 'vs/base/browser/dom'; @@ -12,7 +12,7 @@ import { getWindow } from 'vs/base/browser/dom'; export class ContextViewHandler extends Disposable implements IContextViewProvider { - private currentViewDisposable: IDisposable = Disposable.None; + private currentViewDisposable = this._register(new MutableDisposable()); protected readonly contextView = this._register(new ContextView(this.layoutService.mainContainer, ContextViewDOMPosition.ABSOLUTE)); constructor( @@ -50,7 +50,7 @@ export class ContextViewHandler extends Disposable implements IContextViewProvid } }); - this.currentViewDisposable = disposable; + this.currentViewDisposable.value = disposable; return disposable; } @@ -61,13 +61,6 @@ export class ContextViewHandler extends Disposable implements IContextViewProvid hideContextView(data?: any): void { this.contextView.hide(data); } - - override dispose(): void { - super.dispose(); - - this.currentViewDisposable.dispose(); - this.currentViewDisposable = Disposable.None; - } } export class ContextViewService extends ContextViewHandler implements IContextViewService { diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 8756dc7c60a..8fa0b03bb7a 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -165,10 +165,6 @@ padding-bottom: 5px; } -.quick-input-list .monaco-scrollable-element { - padding: 0px 5px; -} - .quick-input-list .quick-input-list-entry { box-sizing: border-box; overflow: hidden; @@ -184,6 +180,7 @@ .quick-input-list .monaco-list-row { border-radius: 3px; + padding: 0px 5px; } .quick-input-list .monaco-list-row[data-index="0"] .quick-input-list-entry.quick-input-list-separator-border { @@ -319,3 +316,13 @@ font-weight: 600; font-size: 12px; } + +/* Hide border when the item becomes the sticky one */ +.quick-input-list .monaco-tree-sticky-row .quick-input-list-entry.quick-input-list-separator-as-item.quick-input-list-separator-border { + border-top-style: none; +} + +/* TODO: This seems to be the best way to do this... is there a better way? */ +.quick-input-list .monaco-tl-twistie { + display: none !important; +} diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index a3902157e53..65ee0c0bb82 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -11,8 +11,7 @@ import { CountBadge, ICountBadgeStyles } from 'vs/base/browser/ui/countBadge/cou import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; +import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IProgressBarStyles, ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { equals } from 'vs/base/common/arrays'; @@ -28,10 +27,10 @@ import 'vs/css!./media/quickInput'; import { localize } from 'vs/nls'; import { IInputBox, IKeyMods, IQuickInput, IQuickInputButton, IQuickInputHideEvent, IQuickInputToggle, IQuickNavigateConfiguration, IQuickPick, IQuickPickDidAcceptEvent, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, IQuickPickWillAcceptEvent, IQuickWidget, ItemActivation, NO_KEY_MODS, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from './quickInputBox'; -import { QuickInputList, QuickInputListFocus } from './quickInputList'; import { quickInputButtonToAction, renderQuickInputDescription } from './quickInputUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHoverOptions, IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; export interface IQuickInputOptions { idPrefix: string; @@ -41,13 +40,6 @@ export interface IQuickInputOptions { setContextKey(id?: string): void; linkOpenerDelegate(content: string): void; returnFocus(): void; - createList( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IListOptions, - ): List; /** * @todo With IHover in vs/editor, can we depend on the service directly * instead of passing it through a hover delegate? @@ -108,7 +100,7 @@ export interface QuickInputUI { customButtonContainer: HTMLElement; customButton: Button; progressBar: ProgressBar; - list: QuickInputList; + list: QuickInputTree; onDidAccept: Event; onDidCustom: Event; onDidTriggerButton: Event; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 67afe5550b3..e64440ba2a7 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -18,11 +18,11 @@ import { isString } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { QuickInputBox } from 'vs/platform/quickinput/browser/quickInputBox'; -import { QuickInputList, QuickInputListFocus } from 'vs/platform/quickinput/browser/quickInputList'; import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget } from 'vs/platform/quickinput/browser/quickInput'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { mainWindow } from 'vs/base/browser/window'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { QuickInputListFocus, QuickInputTree } from 'vs/platform/quickinput/browser/quickInputTree'; const $ = dom.$; @@ -54,9 +54,10 @@ export class QuickInputController extends Disposable { private previousFocusElement?: HTMLElement; - constructor(private options: IQuickInputOptions, - private readonly themeService: IThemeService, - private readonly layoutService: ILayoutService + constructor( + private options: IQuickInputOptions, + @ILayoutService private readonly layoutService: ILayoutService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.idPrefix = options.idPrefix; @@ -172,7 +173,7 @@ export class QuickInputController extends Disposable { const description1 = dom.append(container, $('.quick-input-description')); const listId = this.idPrefix + 'list'; - const list = this._register(new QuickInputList(container, listId, this.options, this.themeService)); + const list = this._register(this.instantiationService.createInstance(QuickInputTree, container, this.options.hoverDelegate, this.options.linkOpenerDelegate, listId)); inputBox.setAttribute('aria-controls', listId); this._register(list.onDidChangeFocus(() => { inputBox.setAttribute('aria-activedescendant', list.getActiveDescendant() ?? ''); diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index 2974eed8077..e23c1158e68 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -3,14 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { List } from 'vs/base/browser/ui/list/listWidget'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; @@ -82,23 +79,16 @@ export class QuickInputService extends Themable implements IQuickInputService { }); }, returnFocus: () => host.focus(), - createList: ( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IWorkbenchListOptions - ) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List, styles: this.computeStyles(), hoverDelegate: this._register(this.instantiationService.createInstance(QuickInputHoverDelegate)) }; - const controller = this._register(new QuickInputController({ - ...defaultOptions, - ...options - }, - this.themeService, - this.layoutService + const controller = this._register(this.instantiationService.createInstance( + QuickInputController, + { + ...defaultOptions, + ...options + } )); controller.layout(host.activeContainerDimension, host.activeContainerOffset.quickPickTop); diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts similarity index 52% rename from src/vs/platform/quickinput/browser/quickInputList.ts rename to src/vs/platform/quickinput/browser/quickInputTree.ts index f82c848d50e..5b3bbdb2828 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -4,56 +4,67 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { AriaRole } from 'vs/base/browser/ui/aria/aria'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; -import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListAccessibilityProvider, IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; -import { range } from 'vs/base/common/arrays'; -import { ThrottledDelayer } from 'vs/base/common/async'; -import { compareAnything } from 'vs/base/common/comparers'; -import { memoize } from 'vs/base/common/decorators'; -import { isCancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { IMatch } from 'vs/base/common/filters'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { getCodiconAriaLabel, IParsedLabelWithIcons, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import * as platform from 'vs/base/common/platform'; -import { ltrim } from 'vs/base/common/strings'; -import 'vs/css!./media/quickInput'; +import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IObjectTreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { localize } from 'vs/nls'; -import { IQuickInputOptions } from 'vs/platform/quickinput/browser/quickInput'; -import { quickInputButtonToAction } from 'vs/platform/quickinput/browser/quickInputUtils'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { Lazy } from 'vs/base/common/lazy'; -import { URI } from 'vs/base/common/uri'; -import { isDark } from 'vs/platform/theme/common/theme'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator, IQuickPickSeparatorButtonEvent, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMatch } from 'vs/base/common/filters'; +import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { OS, isMacintosh } from 'vs/base/common/platform'; +import { memoize } from 'vs/base/common/decorators'; +import { IIconLabelValueOptions, IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { isDark } from 'vs/platform/theme/common/theme'; +import { URI } from 'vs/base/common/uri'; import { IHoverWidget, ITooltipMarkdownString } from 'vs/base/browser/ui/hover/updatableHoverWidget'; +import { quickInputButtonToAction } from 'vs/platform/quickinput/browser/quickInputUtils'; +import { Lazy } from 'vs/base/common/lazy'; +import { IParsedLabelWithIcons, getCodiconAriaLabel, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { compareAnything } from 'vs/base/common/comparers'; +import { ltrim } from 'vs/base/common/strings'; +import { RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { isCancellationError } from 'vs/base/common/errors'; const $ = dom.$; -interface IListElementLazyParts { +export enum QuickInputListFocus { + First = 1, + Second, + Last, + Next, + Previous, + NextPage, + PreviousPage, + NextSeparator, + PreviousSeparator +} + +interface IQuickInputItemLazyParts { readonly saneLabel: string; readonly saneSortLabel: string; readonly saneAriaLabel: string; } -interface IListElement extends IListElementLazyParts { +interface IQuickPickElement extends IQuickInputItemLazyParts { readonly hasCheckbox: boolean; readonly index: number; readonly item?: IQuickPickItem; readonly saneDescription?: string; readonly saneDetail?: string; readonly saneTooltip?: string | IMarkdownString | HTMLElement; - readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; - readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void; readonly onChecked: Event; checked: boolean; hidden: boolean; @@ -64,62 +75,34 @@ interface IListElement extends IListElementLazyParts { separator?: IQuickPickSeparator; } -class ListElement implements IListElement { - private readonly _init: Lazy; +interface IQuickInputItemTemplateData { + entry: HTMLDivElement; + checkbox: HTMLInputElement; + icon: HTMLDivElement; + label: IconLabel; + keybinding: KeybindingLabel; + detail: IconLabel; + separator: HTMLDivElement; + actionBar: ActionBar; + element: IQuickPickElement; + toDisposeElement: DisposableStore; + toDisposeTemplate: DisposableStore; +} - readonly hasCheckbox: boolean; - readonly index: number; - readonly item?: IQuickPickItem; - readonly saneDescription?: string; - readonly saneDetail?: string; - readonly saneTooltip?: string | IMarkdownString | HTMLElement; - readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; - readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void; +class BaseQuickPickItemElement implements IQuickPickElement { + private readonly _init: Lazy; - // state will get updated later - private _checked: boolean = false; - private _hidden: boolean = false; - private _element?: HTMLElement; - private _labelHighlights?: IMatch[]; - private _descriptionHighlights?: IMatch[]; - private _detailHighlights?: IMatch[]; - private _separator?: IQuickPickSeparator; - - private readonly _onChecked: Emitter<{ listElement: IListElement; checked: boolean }>; - onChecked: Event; + readonly onChecked: Event; constructor( - mainItem: QuickPickItem, - previous: QuickPickItem | undefined, - index: number, - hasCheckbox: boolean, - fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void, - fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void, - onCheckedEmitter: Emitter<{ listElement: IListElement; checked: boolean }> + readonly index: number, + readonly hasCheckbox: boolean, + private _onChecked: Emitter<{ element: IQuickPickElement; checked: boolean }>, + mainItem: QuickPickItem ) { - this.hasCheckbox = hasCheckbox; - this.index = index; - this.fireButtonTriggered = fireButtonTriggered; - this.fireSeparatorButtonTriggered = fireSeparatorButtonTriggered; - this._onChecked = onCheckedEmitter; this.onChecked = hasCheckbox - ? Event.map(Event.filter<{ listElement: IListElement; checked: boolean }>(this._onChecked.event, e => e.listElement === this), e => e.checked) + ? Event.map(Event.filter<{ element: IQuickPickElement; checked: boolean }>(this._onChecked.event, e => e.element === this), e => e.checked) : Event.None; - - if (mainItem.type === 'separator') { - this._separator = mainItem; - } else { - this.item = mainItem; - if (previous && previous.type === 'separator' && !previous.buttons) { - this._separator = previous; - } - this.saneDescription = this.item.description; - this.saneDetail = this.item.detail; - this._labelHighlights = this.item.highlights?.label; - this._descriptionHighlights = this.item.highlights?.description; - this._detailHighlights = this.item.highlights?.detail; - this.saneTooltip = this.item.tooltip; - } this._init = new Lazy(() => { const saneLabel = mainItem.label ?? ''; const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim(); @@ -142,11 +125,9 @@ class ListElement implements IListElement { get saneLabel() { return this._init.value.saneLabel; } - get saneSortLabel() { return this._init.value.saneSortLabel; } - get saneAriaLabel() { return this._init.value.saneAriaLabel; } @@ -155,112 +136,204 @@ class ListElement implements IListElement { // #region Getters and Setters + private _element?: HTMLElement; get element() { return this._element; } - set element(value: HTMLElement | undefined) { this._element = value; } + private _hidden: boolean = false; get hidden() { return this._hidden; } - set hidden(value: boolean) { this._hidden = value; } + private _checked: boolean = false; get checked() { return this._checked; } - set checked(value: boolean) { if (value !== this._checked) { this._checked = value; - this._onChecked.fire({ listElement: this, checked: value }); + this._onChecked.fire({ element: this, checked: value }); } } - get separator() { - return this._separator; + protected _saneDescription?: string; + get saneDescription() { + return this._saneDescription; + } + set saneDescription(value: string | undefined) { + this._saneDescription = value; } - set separator(value: IQuickPickSeparator | undefined) { - this._separator = value; + protected _saneDetail?: string; + get saneDetail() { + return this._saneDetail; + } + set saneDetail(value: string | undefined) { + this._saneDetail = value; } + protected _saneTooltip?: string | IMarkdownString | HTMLElement; + get saneTooltip() { + return this._saneTooltip; + } + set saneTooltip(value: string | IMarkdownString | HTMLElement | undefined) { + this._saneTooltip = value; + } + + protected _labelHighlights?: IMatch[]; get labelHighlights() { return this._labelHighlights; } - set labelHighlights(value: IMatch[] | undefined) { this._labelHighlights = value; } + protected _descriptionHighlights?: IMatch[]; get descriptionHighlights() { return this._descriptionHighlights; } - set descriptionHighlights(value: IMatch[] | undefined) { this._descriptionHighlights = value; } + protected _detailHighlights?: IMatch[]; get detailHighlights() { return this._detailHighlights; } - set detailHighlights(value: IMatch[] | undefined) { this._detailHighlights = value; } - - // #endregion } -interface IListElementTemplateData { - entry: HTMLDivElement; - checkbox: HTMLInputElement; - icon: HTMLDivElement; - label: IconLabel; - keybinding: KeybindingLabel; - detail: IconLabel; - separator: HTMLDivElement; - actionBar: ActionBar; - element: IListElement; - toDisposeElement: IDisposable[]; - toDisposeTemplate: IDisposable[]; +class QuickPickItemElement extends BaseQuickPickItemElement { + constructor( + index: number, + hasCheckbox: boolean, + readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void, + onCheckedEmitter: Emitter<{ element: IQuickPickElement; checked: boolean }>, + readonly item: IQuickPickItem, + previous: QuickPickItem | undefined, + ) { + super(index, hasCheckbox, onCheckedEmitter, item); + this._saneDescription = item.description; + this._saneDetail = item.detail; + this._saneTooltip = this.item.tooltip; + this._labelHighlights = item.highlights?.label; + this._descriptionHighlights = item.highlights?.description; + this._detailHighlights = item.highlights?.detail; + + if (previous && previous.type === 'separator' && !previous.buttons) { + this._separator = previous; + } + } + + private _separator: IQuickPickSeparator | undefined; + get separator() { + return this._separator; + } + set separator(value: IQuickPickSeparator | undefined) { + this._separator = value; + } } -class ListElementRenderer implements IListRenderer { +class QuickPickSeparatorElement extends BaseQuickPickItemElement { + children = new Array(); + + constructor( + index: number, + readonly fireSeparatorButtonTriggered: (event: IQuickPickSeparatorButtonEvent) => void, + // TODO: remove this + onCheckedEmitter: Emitter<{ element: IQuickPickElement; checked: boolean }>, + readonly separator: IQuickPickSeparator, + ) { + super(index, false, onCheckedEmitter, separator); + } +} + +class QuickInputItemDelegate implements IListVirtualDelegate { + getHeight(element: IQuickPickElement): number { + + if (!element.item) { + // must be a separator + return 24; + } + return element.saneDetail ? 44 : 22; + } + + getTemplateId(element: IQuickPickElement): string { + return QuickInputListRenderer.ID; + } +} + +class QuickInputAccessibilityProvider implements IListAccessibilityProvider { + + getWidgetAriaLabel(): string { + return localize('quickInput', "Quick Input"); + } + + getAriaLabel(element: IQuickPickElement): string | null { + return element.separator?.label + ? `${element.saneAriaLabel}, ${element.separator.label}` + : element.saneAriaLabel; + } + + getWidgetRole(): AriaRole { + return 'listbox'; + } + + getRole(element: IQuickPickElement) { + return element.hasCheckbox ? 'checkbox' : 'option'; + } + + isChecked(element: IQuickPickElement) { + if (!element.hasCheckbox) { + return undefined; + } + + return { + value: element.checked, + onDidChange: element.onChecked + }; + } +} + +class QuickInputListRenderer implements ITreeRenderer { static readonly ID = 'listelement'; constructor( - private readonly themeService: IThemeService, private readonly hoverDelegate: IHoverDelegate | undefined, + @IThemeService private readonly themeService: IThemeService, ) { } get templateId() { - return ListElementRenderer.ID; + return QuickInputListRenderer.ID; } - renderTemplate(container: HTMLElement): IListElementTemplateData { - const data: IListElementTemplateData = Object.create(null); - data.toDisposeElement = []; - data.toDisposeTemplate = []; + renderTemplate(container: HTMLElement): IQuickInputItemTemplateData { + const data: IQuickInputItemTemplateData = 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.push(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => { + 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'; - data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { + data.toDisposeTemplate.add(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => { data.element.checked = data.checkbox.checked; })); @@ -271,18 +344,18 @@ class ListElementRenderer implements IListRendererdom.prepend(data.label.element, $('.quick-input-list-icon')); // Keybinding const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding')); - data.keybinding = new KeybindingLabel(keybindingContainer, platform.OS); - data.toDisposeTemplate.push(data.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.push(data.detail); + data.toDisposeTemplate.add(data.detail); // Separator data.separator = dom.append(data.entry, $('.quick-input-list-separator')); @@ -290,18 +363,18 @@ class ListElementRenderer implements IListRenderer, index: number, data: IQuickInputItemTemplateData): void { + const element = node.element; data.element = element; element.element = data.entry ?? undefined; const mainItem: QuickPickItem = element.item ? element.item : element.separator!; data.checkbox.checked = element.checked; - data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked)); + data.toDisposeElement.add(element.onChecked(checked => data.checkbox.checked = checked)); const { labelHighlights, descriptionHighlights, detailHighlights } = element; @@ -387,144 +460,235 @@ class ListElementRenderer implements IListRenderer quickInputButtonToAction( button, `id-${index}`, - () => mainItem.type !== 'separator' - ? element.fireButtonTriggered({ button, item: mainItem }) - : element.fireSeparatorButtonTriggered({ button, separator: mainItem }) + () => element instanceof QuickPickItemElement + ? element.fireButtonTriggered({ button, item: element.item }) + : (element as QuickPickSeparatorElement).fireSeparatorButtonTriggered({ button, separator: element.separator! }) )), { icon: true, label: false }); data.entry.classList.add('has-actions'); } else { data.entry.classList.remove('has-actions'); } } - - disposeElement(element: IListElement, index: number, data: IListElementTemplateData): void { - data.toDisposeElement = dispose(data.toDisposeElement); + disposeElement?(_element: ITreeNode, _index: number, data: IQuickInputItemTemplateData): void { + data.toDisposeElement.clear(); data.actionBar.clear(); } - - disposeTemplate(data: IListElementTemplateData): void { - data.toDisposeElement = dispose(data.toDisposeElement); - data.toDisposeTemplate = dispose(data.toDisposeTemplate); + disposeTemplate(data: IQuickInputItemTemplateData): void { + data.toDisposeElement.dispose(); + data.toDisposeTemplate.dispose(); } } -class ListElementDelegate implements IListVirtualDelegate { +export class QuickInputTree extends Disposable { - getHeight(element: IListElement): number { - if (!element.item) { - // must be a separator - return 24; - } - return element.saneDetail ? 44 : 22; - } + private readonly _onKeyDown = new Emitter(); + /** + * Event that is fired when the tree receives a keydown. + */ + readonly onKeyDown: Event = this._onKeyDown.event; - getTemplateId(element: IListElement): string { - return ListElementRenderer.ID; - } -} + private readonly _onLeave = new Emitter(); + /** + * Event that is fired when the tree would no longer have focus. + */ + readonly onLeave: Event = this._onLeave.event; -export enum QuickInputListFocus { - First = 1, - Second, - Last, - Next, - Previous, - NextPage, - PreviousPage, - NextSeparator, - PreviousSeparator -} - -export class QuickInputList { - - readonly id: string; - private container: HTMLElement; - private list: List; - private inputElements: Array = []; - private elements: IListElement[] = []; - private elementsToIndexes = new Map(); - matchOnDescription = false; - matchOnDetail = false; - matchOnLabel = true; - matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; - matchOnMeta = true; - sortByLabel = true; private readonly _onChangedAllVisibleChecked = new Emitter(); onChangedAllVisibleChecked: Event = this._onChangedAllVisibleChecked.event; + private readonly _onChangedCheckedCount = new Emitter(); onChangedCheckedCount: Event = this._onChangedCheckedCount.event; + private readonly _onChangedVisibleCount = new Emitter(); onChangedVisibleCount: Event = this._onChangedVisibleCount.event; + private readonly _onChangedCheckedElements = new Emitter(); onChangedCheckedElements: Event = this._onChangedCheckedElements.event; + private readonly _onButtonTriggered = new Emitter>(); onButtonTriggered = this._onButtonTriggered.event; + private readonly _onSeparatorButtonTriggered = new Emitter(); onSeparatorButtonTriggered = this._onSeparatorButtonTriggered.event; - private readonly _onKeyDown = new Emitter(); - onKeyDown: Event = this._onKeyDown.event; - private readonly _onLeave = new Emitter(); - onLeave: Event = this._onLeave.event; - private readonly _listElementChecked = new Emitter<{ listElement: IListElement; checked: boolean }>(); - private _fireCheckedEvents = true; - private elementDisposables: IDisposable[] = []; - private disposables: IDisposable[] = []; + + private readonly _container: HTMLElement; + private readonly _tree: WorkbenchObjectTree; + private readonly _elementChecked = new Emitter<{ element: IQuickPickElement; checked: boolean }>(); + private _inputElements = new Array(); + private _elements = new Array(); + private _itemElements = new Array(); + // Elements that apply to the current set of elements + private _elementDisposable = this._register(new DisposableStore()); private _lastHover: IHoverWidget | undefined; - private _toggleHover: IDisposable | undefined; constructor( private parent: HTMLElement, + private hoverDelegate: IHoverDelegate, + private linkOpenerDelegate: (content: string) => void, id: string, - private options: IQuickInputOptions, - themeService: IThemeService + @IInstantiationService instantiationService: IInstantiationService ) { - this.id = id; - this.container = dom.append(this.parent, $('.quick-input-list')); - const delegate = new ListElementDelegate(); - const accessibilityProvider = new QuickInputAccessibilityProvider(); - this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer(themeService, options.hoverDelegate)], { - identityProvider: { - getId: element => { - // always prefer item over separator because if item is defined, it must be the main item type - // always prefer a defined id if one was specified and use label as a fallback - return element.item?.id - ?? element.item?.label - ?? element.separator?.id - ?? element.separator?.label - ?? ''; - } - }, - setRowLineHeight: false, - multipleSelectionSupport: false, - horizontalScrolling: false, - accessibilityProvider - } as IListOptions); - this.list.getHTMLElement().id = id; - this.disposables.push(this.list); - // Keybindings for the list itself - this.disposables.push(this.list.onKeyDown(e => { + super(); + this._container = dom.append(this.parent, $('.quick-input-list')); + this._tree = this._register(instantiationService.createInstance( + WorkbenchObjectTree, + 'QuickInput', + this._container, + new QuickInputItemDelegate(), + [instantiationService.createInstance(QuickInputListRenderer, hoverDelegate)], + { + accessibilityProvider: new QuickInputAccessibilityProvider(), + setRowLineHeight: false, + multipleSelectionSupport: false, + hideTwistiesOfChildlessElements: true, + renderIndentGuides: RenderIndentGuides.None, + findWidgetEnabled: false, + indent: 0, + horizontalScrolling: false, + identityProvider: { + getId: element => { + // always prefer item over separator because if item is defined, it must be the main item type + // always prefer a defined id if one was specified and use label as a fallback + return element.item?.id + ?? element.item?.label + ?? element.separator?.id + ?? element.separator?.label + ?? ''; + } + }, + alwaysConsumeMouseWheel: true + } + )); + this._tree.getHTMLElement().id = id; + this._registerListeners(); + } + + //#region public getters/setters + + @memoize + get onDidChangeFocus() { + return Event.map( + this._tree.onDidChangeFocus, + e => e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item) + ); + } + + @memoize + get onDidChangeSelection() { + return Event.map( + this._tree.onDidChangeSelection, + e => ({ + items: e.elements.filter((e): e is QuickPickItemElement => e instanceof QuickPickItemElement).map(e => e.item), + event: e.browserEvent + })); + } + + get scrollTop() { + return this._tree.scrollTop; + } + + set scrollTop(scrollTop: number) { + this._tree.scrollTop = scrollTop; + } + + get ariaLabel() { + return this._tree.ariaLabel; + } + + set ariaLabel(label: string | null) { + this._tree.ariaLabel = label ?? ''; + } + + set enabled(value: boolean) { + this._tree.getHTMLElement().style.pointerEvents = value ? '' : 'none'; + } + + private _matchOnDescription = false; + get matchOnDescription() { + return this._matchOnDescription; + } + set matchOnDescription(value: boolean) { + this._matchOnDescription = value; + } + + private _matchOnDetail = false; + get matchOnDetail() { + return this._matchOnDetail; + } + set matchOnDetail(value: boolean) { + this._matchOnDetail = value; + } + + private _matchOnLabel = true; + get matchOnLabel() { + return this._matchOnLabel; + } + set matchOnLabel(value: boolean) { + this._matchOnLabel = value; + } + + private _matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy'; + get matchOnLabelMode() { + return this._matchOnLabelMode; + } + set matchOnLabelMode(value: 'fuzzy' | 'contiguous') { + this._matchOnLabelMode = value; + } + + private _matchOnMeta = true; + get matchOnMeta() { + return this._matchOnMeta; + } + set matchOnMeta(value: boolean) { + this._matchOnMeta = value; + } + + private _sortByLabel = true; + get sortByLabel() { + return this._sortByLabel; + } + set sortByLabel(value: boolean) { + this._sortByLabel = value; + } + + //#endregion + + //#region register listeners + + private _registerListeners() { + this._registerOnKeyDown(); + this._registerOnContainerClick(); + this._registerOnMouseMiddleClick(); + this._registerOnElementChecked(); + this._registerOnContextMenu(); + this._registerHoverListeners(); + } + + private _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 KeyCode.Space: this.toggleCheckbox(); break; case KeyCode.KeyA: - if (platform.isMacintosh ? e.metaKey : e.ctrlKey) { - this.list.setFocus(range(this.list.length)); + if (isMacintosh ? e.metaKey : e.ctrlKey) { + this._tree.setFocus(this._itemElements); } break; - // When we hit the top of the list, we fire the onLeave event. + // When we hit the top of the tree, we fire the onLeave event. case KeyCode.UpArrow: { - const focus1 = this.list.getFocus(); - if (focus1.length === 1 && focus1[0] === 0) { + const focus1 = this._tree.getFocus(); + if (focus1.length === 1 && focus1[0] === this._itemElements[0]) { this._onLeave.fire(); } break; } - // When we hit the bottom of the list, we fire the onLeave event. + // When we hit the bottom of the tree, we fire the onLeave event. case KeyCode.DownArrow: { - const focus2 = this.list.getFocus(); - if (focus2.length === 1 && focus2[0] === this.list.length - 1) { + const focus2 = this._tree.getFocus(); + if (focus2.length === 1 && focus2[0] === this._itemElements[this._itemElements.length - 1]) { this._onLeave.fire(); } break; @@ -533,22 +697,31 @@ export class QuickInputList { this._onKeyDown.fire(event); })); - this.disposables.push(this.list.onMouseDown(e => { - if (e.browserEvent.button !== 2) { - // Works around / fixes #64350. - e.browserEvent.preventDefault(); - } - })); - this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => { + } + + private _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(); } })); - this.disposables.push(this.list.onMouseMiddleClick(e => { - this._onLeave.fire(); + } + + private _registerOnMouseMiddleClick() { + this._register(dom.addDisposableListener(this._container, dom.EventType.AUXCLICK, e => { + if (e.button === 1) { + this._onLeave.fire(); + } })); - this.disposables.push(this.list.onContextMenu(e => { - if (typeof e.index === 'number') { + } + + private _registerOnElementChecked() { + this._register(this._elementChecked.event(_ => this._fireCheckedEvents())); + } + + private _registerOnContextMenu() { + this._register(this._tree.onContextMenu(e => { + if (e.element) { e.browserEvent.preventDefault(); // we want to treat a context menu event as @@ -556,14 +729,14 @@ export class QuickInputList { // since we do not have any context menu // this enables for example macOS to Ctrl- // click on an item to open it. - this.list.setSelection([e.index]); + this._tree.setSelection([e.element]); } })); + } - const delayer = new ThrottledDelayer(options.hoverDelegate.delay); - // onMouseOver triggers every time a new element has been moused over - // even if it's on the same list item. - this.disposables.push(this.list.onMouseOver(async e => { + private _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 (e.browserEvent.target instanceof HTMLAnchorElement) { @@ -580,7 +753,7 @@ export class QuickInputList { } try { await delayer.trigger(async () => { - if (e.element) { + if (e.element instanceof QuickPickItemElement) { this.showHover(e.element); } }); @@ -591,7 +764,7 @@ export class QuickInputList { } } })); - this.disposables.push(this.list.onMouseOut(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. @@ -600,246 +773,224 @@ export class QuickInputList { } delayer.cancel(); })); - this.disposables.push(delayer); - this.disposables.push(this._listElementChecked.event(_ => this.fireCheckedEvents())); - this.disposables.push( - this._onChangedAllVisibleChecked, - this._onChangedCheckedCount, - this._onChangedVisibleCount, - this._onChangedCheckedElements, - this._onButtonTriggered, - this._onSeparatorButtonTriggered, - this._onLeave, - this._onKeyDown - ); } - @memoize - get onDidChangeFocus() { - return Event.map(this.list.onDidChangeFocus, e => e.elements.map(e => e.item)); - } + //#endregion - @memoize - get onDidChangeSelection() { - return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent })); - } - - get scrollTop() { - return this.list.scrollTop; - } - - set scrollTop(scrollTop: number) { - this.list.scrollTop = scrollTop; - } - - get ariaLabel() { - return this.list.getHTMLElement().ariaLabel; - } - - set ariaLabel(label: string | null) { - this.list.getHTMLElement().ariaLabel = label; - } + //#region public methods getAllVisibleChecked() { - return this.allVisibleChecked(this.elements, false); - } - - private allVisibleChecked(elements: IListElement[], whenNoneVisible = true) { - for (let i = 0, n = elements.length; i < n; i++) { - const element = elements[i]; - if (!element.hidden) { - if (!element.checked) { - return false; - } else { - whenNoneVisible = true; - } - } - } - return whenNoneVisible; + return this._allVisibleChecked(this._itemElements, false); } getCheckedCount() { - let count = 0; - const elements = this.elements; - for (let i = 0, n = elements.length; i < n; i++) { - if (elements[i].checked) { - count++; - } - } - return count; + return this._itemElements.filter(element => element.checked).length; } getVisibleCount() { - let count = 0; - const elements = this.elements; - for (let i = 0, n = elements.length; i < n; i++) { - if (!elements[i].hidden) { - count++; - } - } - return count; + return this._itemElements.filter(e => !e.hidden).length; } setAllVisibleChecked(checked: boolean) { - try { - this._fireCheckedEvents = false; - this.elements.forEach(element => { - if (!element.hidden) { - element.checked = checked; - } - }); - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); - } + this._itemElements.forEach(element => { + if (!element.hidden) { + element.checked = checked; + } + }); + this._fireCheckedEvents(); } - setElements(inputElements: Array): void { - this.elementDisposables = dispose(this.elementDisposables); - const fireButtonTriggered = (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event); - const fireSeparatorButtonTriggered = (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event); - this.inputElements = inputElements; + setElements(inputElements: QuickPickItem[]): void { + this._elementDisposable.clear(); + this._inputElements = inputElements; const elementsToIndexes = new Map(); const hasCheckbox = this.parent.classList.contains('show-checkboxes'); - this.elements = inputElements.reduce((result, item, index) => { - const previous = index > 0 ? inputElements[index - 1] : undefined; + let currentSeparatorElement: QuickPickSeparatorElement | undefined; + this._itemElements = new Array(); + this._elements = inputElements.reduce((result, item, index) => { + let element: IQuickPickElement; 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, + (event: IQuickPickSeparatorButtonEvent) => this.fireSeparatorButtonTriggered(event), + this._elementChecked, + item + ); + element = currentSeparatorElement; + } else { + const previous = index > 0 ? inputElements[index - 1] : undefined; + const qpi = new QuickPickItemElement( + index, + hasCheckbox, + (event: IQuickPickItemButtonEvent) => this.fireButtonTriggered(event), + this._elementChecked, + item, + previous, + ); + this._itemElements.push(qpi); - const element = new ListElement( - item, - previous, - index, - hasCheckbox, - fireButtonTriggered, - fireSeparatorButtonTriggered, - this._listElementChecked - ); + if (currentSeparatorElement) { + currentSeparatorElement.children.push(qpi); + return result; + } + element = qpi; + } const resultIndex = result.length; result.push(element); elementsToIndexes.set(element.item ?? element.separator!, resultIndex); return result; - }, [] as IListElement[]); - this.elementsToIndexes = elementsToIndexes; - this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty. - this.list.splice(0, this.list.length, this.elements); - this._onChangedVisibleCount.fire(this.elements.length); + }, new Array()); + + // if we ever saw a separator item, we render "tree like" + if (currentSeparatorElement) { + const elements = new Array>(); + let visibleCount = 0; + for (const element of this._elements) { + if (element instanceof QuickPickSeparatorElement) { + elements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + visibleCount += element.children.length + 1; // +1 for the separator itself; + } else { + elements.push({ + element, + collapsible: false, + collapsed: false, + }); + visibleCount++; + } + } + this._tree.setChildren(null, elements); + this._onChangedVisibleCount.fire(visibleCount); + } else { + // All elements are items so we render "flat" + this._tree.setChildren( + null, + this._elements.map>(e => ({ + element: e, + collapsible: false, + collapsed: false, + })) + ); + this._onChangedVisibleCount.fire(this._elements.length); + } } getElementsCount(): number { - return this.inputElements.length; + return this._inputElements.length; } getFocusedElements() { - return this.list.getFocusedElements() - .map(e => e.item); + return this._tree.getFocus() + .filter((e): e is IQuickPickElement => !!e) + .map(e => e.item) + .filter((e): e is IQuickPickItem => !!e); } setFocusedElements(items: IQuickPickItem[]) { - this.list.setFocus(items - .filter(item => this.elementsToIndexes.has(item)) - .map(item => this.elementsToIndexes.get(item)!)); + const elements = items.map(item => this._itemElements.find(e => e.item === item)) + .filter((e): e is QuickPickItemElement => !!e); + this._tree.setFocus(elements); if (items.length > 0) { - const focused = this.list.getFocus()[0]; - if (typeof focused === 'number') { - this.list.reveal(focused); + const focused = this._tree.getFocus()[0]; + if (focused) { + this._tree.reveal(focused); } } } getActiveDescendant() { - return this.list.getHTMLElement().getAttribute('aria-activedescendant'); + return this._tree.getHTMLElement().getAttribute('aria-activedescendant'); } getSelectedElements() { - return this.list.getSelectedElements() + return this._tree.getSelection() + .filter((e): e is IQuickPickElement => !!e && !!(e as QuickPickItemElement).item) .map(e => e.item); } setSelectedElements(items: IQuickPickItem[]) { - this.list.setSelection(items - .filter(item => this.elementsToIndexes.has(item)) - .map(item => this.elementsToIndexes.get(item)!)); + const elements = items.map(item => this._itemElements.find(e => e.item === item)) + .filter((e): e is QuickPickItemElement => !!e); + this._tree.setSelection(elements); } getCheckedElements() { - return this.elements.filter(e => e.checked) - .map(e => e.item) - .filter(e => !!e) as IQuickPickItem[]; + return this._itemElements.filter(e => e.checked) + .map(e => e.item); } setCheckedElements(items: IQuickPickItem[]) { - try { - this._fireCheckedEvents = false; - const checked = new Set(); - for (const item of items) { - checked.add(item); - } - for (const element of this.elements) { - element.checked = checked.has(element.item); - } - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); + const checked = new Set(); + for (const item of items) { + checked.add(item); } - } - - set enabled(value: boolean) { - this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none'; + for (const element of this._itemElements) { + element.checked = checked.has(element.item); + } + this._fireCheckedEvents(); } focus(what: QuickInputListFocus): void { - if (!this.list.length) { + if (!this._itemElements.length) { return; } - if (what === QuickInputListFocus.Second && this.list.length < 2) { + if (what === QuickInputListFocus.Second && this._itemElements.length < 2) { what = QuickInputListFocus.First; } switch (what) { case QuickInputListFocus.First: - this.list.scrollTop = 0; - this.list.focusFirst(undefined, (e) => !!e.item); + this._tree.scrollTop = 0; + this._tree.focusFirst(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickInputListFocus.Second: - this.list.scrollTop = 0; - this.list.focusNth(1, undefined, (e) => !!e.item); + this._tree.scrollTop = 0; + this._tree.setFocus([this._itemElements[1]]); break; case QuickInputListFocus.Last: - this.list.scrollTop = this.list.scrollHeight; - this.list.focusLast(undefined, (e) => !!e.item); + this._tree.scrollTop = this._tree.scrollHeight; + this._tree.setFocus([this._itemElements[this._itemElements.length - 1]]); break; case QuickInputListFocus.Next: - this.list.focusNext(undefined, true, undefined, (e) => !!e.item); + this._tree.focusNext(undefined, true, undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickInputListFocus.Previous: - this.list.focusPrevious(undefined, true, undefined, (e) => !!e.item); + this._tree.focusPrevious(undefined, true, undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickInputListFocus.NextPage: - this.list.focusNextPage(undefined, (e) => !!e.item); + this._tree.focusNextPage(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickInputListFocus.PreviousPage: - this.list.focusPreviousPage(undefined, (e) => !!e.item); + this._tree.focusPreviousPage(undefined, (e) => e.element instanceof QuickPickItemElement); break; case QuickInputListFocus.NextSeparator: { let foundSeparatorAsItem = false; - this.list.focusNext(undefined, true, undefined, (e) => { + 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.separator) { - if (e.item) { + + if (e.element instanceof QuickPickSeparatorElement) { + foundSeparatorAsItem = true; + } else if (e.element instanceof QuickPickItemElement) { + if (e.element.separator) { return true; - } else { - foundSeparatorAsItem = true; } } return false; @@ -847,88 +998,66 @@ export class QuickInputList { break; } case QuickInputListFocus.PreviousSeparator: { - let foundSeparatorAsItem = false; - this.list.focusPrevious(undefined, true, undefined, (e) => { - if (foundSeparatorAsItem) { - // This should be the index right before the separator so it - // is the item we want to focus. - return true; - } - if (e.separator) { - if (e.item) { - // This would be an inline-separator so we should - // focus this item. - return true; + let focusElement: IQuickPickElement | undefined; + // 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) { + focusElement ??= e.element.children[0]; } else { - foundSeparatorAsItem = true; + foundSeparator = true; + } + } else if (e.element instanceof QuickPickItemElement) { + if (e.element.separator) { + focusElement ??= e.element; } } return false; }); + if (focusElement) { + this._tree.setFocus([focusElement]); + } break; } } - const focused = this.list.getFocus()[0]; - if (typeof focused === 'number') { - if (focused !== 0 && !this.elements[focused - 1].item && this.list.firstVisibleIndex > focused - 1) { - this.list.reveal(focused - 1); + const focused = this._tree.getFocus()[0]; + if (focused) { + // TODO: can this be improved? + const indexOfFocused = this._itemElements.indexOf(focused as QuickPickItemElement); + const indexOfFirstVisible = this._tree.firstVisibleElement ? this._itemElements.indexOf(this._tree.firstVisibleElement as QuickPickItemElement) : -1; + if (focused !== this._itemElements[0] && !this._itemElements[indexOfFocused - 1].item && indexOfFirstVisible > indexOfFocused - 1) { + this._tree.reveal(this._elements[indexOfFocused - 1]); } else { - this.list.reveal(focused); + this._tree.reveal(focused); } } } clearFocus() { - this.list.setFocus([]); + this._tree.setFocus([]); } domFocus() { - this.list.domFocus(); - } - - /** - * Disposes of the hover and shows a new one for the given index if it has a tooltip. - * @param element The element to show the hover for - */ - private showHover(element: IListElement): void { - if (this._lastHover && !this._lastHover.isDisposed) { - this.options.hoverDelegate.onDidHideHover?.(); - this._lastHover?.dispose(); - } - - if (!element.element || !element.saneTooltip) { - return; - } - this._lastHover = this.options.hoverDelegate.showHover({ - content: element.saneTooltip, - target: element.element, - linkHandler: (url) => { - this.options.linkOpenerDelegate(url); - }, - appearance: { - showPointer: true, - }, - container: this.container, - position: { - hoverPosition: HoverPosition.RIGHT - } - }, false); + this._tree.domFocus(); } layout(maxHeight?: number): void { - this.list.getHTMLElement().style.maxHeight = 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.list.layout(); + this._tree.layout(); } filter(query: string): boolean { - if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { - this.list.layout(); + if (!(this._sortByLabel || this._matchOnLabel || this._matchOnDescription || this._matchOnDetail)) { + this._tree.layout(); return false; } @@ -937,12 +1066,12 @@ export class QuickInputList { // Reset filtering if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { - this.elements.forEach(element => { + 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]; + const previous = element.index && this._inputElements[element.index - 1]; if (element.item) { element.separator = previous && previous.type === 'separator' && !previous.buttons ? previous : undefined; } @@ -952,7 +1081,7 @@ export class QuickInputList { // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) else { let currentSeparator: IQuickPickSeparator | undefined; - this.elements.forEach(element => { + this._elements.forEach(element => { let labelHighlights: IMatch[] | undefined; if (this.matchOnLabelMode === 'fuzzy') { labelHighlights = this.matchOnLabel ? matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel)) ?? undefined : undefined; @@ -983,7 +1112,7 @@ export class QuickInputList { // we can show the separator unless the list gets sorted by match if (!this.sortByLabel) { - const previous = element.index && this.inputElements[element.index - 1]; + const previous = element.index && this._inputElements[element.index - 1]; currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; if (currentSeparator && !element.hidden) { element.separator = currentSeparator; @@ -993,7 +1122,7 @@ export class QuickInputList { }); } - const shownElements = this.elements.filter(element => !element.hidden); + const shownElements = this._elements.filter(element => !element.hidden); // Sort by value if (this.sortByLabel && query) { @@ -1003,13 +1132,59 @@ export class QuickInputList { }); } - this.elementsToIndexes = shownElements.reduce((map, element, index) => { - map.set(element.item ?? element.separator!, index); - return map; - }, new Map()); - this.list.splice(0, this.list.length, shownElements); - this.list.setFocus([]); - this.list.layout(); + let currentSeparator: QuickPickSeparatorElement | undefined; + const finalElements = shownElements.reduce((result, element, index) => { + if (element instanceof QuickPickItemElement) { + if (currentSeparator) { + currentSeparator.children.push(element); + } else { + result.push(element); + } + } else if (element instanceof QuickPickSeparatorElement) { + element.children = []; + currentSeparator = element; + result.push(element); + } + return result; + }, new Array()); + + // if we ever saw a separator item, we render "tree like" + if (currentSeparator) { + const elements = new Array>(); + for (const element of finalElements) { + if (element instanceof QuickPickSeparatorElement) { + elements.push({ + element, + collapsible: false, + collapsed: false, + children: element.children.map(e => ({ + element: e, + collapsible: false, + collapsed: false, + })), + }); + } else { + elements.push({ + element, + collapsible: false, + collapsed: false, + }); + } + } + this._tree.setChildren(null, elements); + } else { + // All elements are items so we render "flat" + this._tree.setChildren( + null, + finalElements.map>(e => ({ + element: e, + collapsible: false, + collapsed: false, + })) + ); + } + + this._tree.layout(); this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); this._onChangedVisibleCount.fire(shownElements.length); @@ -1018,55 +1193,29 @@ export class QuickInputList { } toggleCheckbox() { - try { - this._fireCheckedEvents = false; - const elements = this.list.getFocusedElements(); - const allChecked = this.allVisibleChecked(elements); - for (const element of elements) { - element.checked = !allChecked; - } - } finally { - this._fireCheckedEvents = true; - this.fireCheckedEvents(); + const elements = this._tree.getFocus().filter((e): e is IQuickPickElement => !!e); + const allChecked = this._allVisibleChecked(elements); + for (const element of elements) { + element.checked = !allChecked; } + this._fireCheckedEvents(); } display(display: boolean) { - this.container.style.display = display ? '' : 'none'; + this._container.style.display = display ? '' : 'none'; } isDisplayed() { - return this.container.style.display !== 'none'; - } - - dispose() { - this.elementDisposables = dispose(this.elementDisposables); - this.disposables = dispose(this.disposables); - } - - private fireCheckedEvents() { - if (this._fireCheckedEvents) { - this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); - this._onChangedCheckedCount.fire(this.getCheckedCount()); - this._onChangedCheckedElements.fire(this.getCheckedElements()); - } - } - - private fireButtonTriggered(event: IQuickPickItemButtonEvent) { - this._onButtonTriggered.fire(event); - } - - private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { - this._onSeparatorButtonTriggered.fire(event); + return this._container.style.display !== 'none'; } style(styles: IListStyles) { - this.list.style(styles); + this._tree.style(styles); } toggleHover() { - const element: IListElement | undefined = this.list.getFocusedElements()[0]; - if (!element?.saneTooltip) { + const focused: IQuickPickElement | null = this._tree.getFocus()[0]; + if (!focused?.saneTooltip || !(focused instanceof QuickPickItemElement)) { return; } @@ -1077,22 +1226,78 @@ export class QuickInputList { } // If there is no hover, show it (toggle on) - const focused = this.list.getFocusedElements()[0]; - if (!focused) { - return; - } this.showHover(focused); const store = new DisposableStore(); - store.add(this.list.onDidChangeFocus(e => { - if (e.indexes.length) { + store.add(this._tree.onDidChangeFocus(e => { + if (e.elements[0] instanceof QuickPickItemElement) { this.showHover(e.elements[0]); } })); if (this._lastHover) { store.add(this._lastHover); } - this._toggleHover = store; - this.elementDisposables.push(this._toggleHover); + this._elementDisposable.add(store); + } + + //#endregion + + //#region private methods + + private _allVisibleChecked(elements: IQuickPickElement[], whenNoneVisible = true) { + for (let i = 0, n = elements.length; i < n; i++) { + const element = elements[i]; + if (!element.hidden) { + if (!element.checked) { + return false; + } else { + whenNoneVisible = true; + } + } + } + return whenNoneVisible; + } + + private _fireCheckedEvents() { + this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); + this._onChangedCheckedCount.fire(this.getCheckedCount()); + this._onChangedCheckedElements.fire(this.getCheckedElements()); + } + + private fireButtonTriggered(event: IQuickPickItemButtonEvent) { + this._onButtonTriggered.fire(event); + } + + private fireSeparatorButtonTriggered(event: IQuickPickSeparatorButtonEvent) { + this._onSeparatorButtonTriggered.fire(event); + } + + /** + * Disposes of the hover and shows a new one for the given index if it has a tooltip. + * @param element The element to show the hover for + */ + private showHover(element: QuickPickItemElement): void { + if (this._lastHover && !this._lastHover.isDisposed) { + this.hoverDelegate.onDidHideHover?.(); + this._lastHover?.dispose(); + } + + if (!element.element || !element.saneTooltip) { + return; + } + this._lastHover = this.hoverDelegate.showHover({ + content: element.saneTooltip, + target: element.element, + linkHandler: (url) => { + this.linkOpenerDelegate(url); + }, + appearance: { + showPointer: true, + }, + container: this._container, + position: { + hoverPosition: HoverPosition.RIGHT + } + }, false); } } @@ -1133,7 +1338,7 @@ function matchesContiguous(word: string, wordToMatchAgainst: string): IMatch[] | return null; } -function compareEntries(elementA: IListElement, elementB: IListElement, lookFor: string): number { +function compareEntries(elementA: IQuickPickElement, elementB: IQuickPickElement, lookFor: string): number { const labelHighlightsA = elementA.labelHighlights || []; const labelHighlightsB = elementB.labelHighlights || []; @@ -1151,35 +1356,3 @@ function compareEntries(elementA: IListElement, elementB: IListElement, lookFor: return compareAnything(elementA.saneSortLabel, elementB.saneSortLabel, lookFor); } - -class QuickInputAccessibilityProvider implements IListAccessibilityProvider { - - getWidgetAriaLabel(): string { - return localize('quickInput', "Quick Input"); - } - - getAriaLabel(element: IListElement): string | null { - return element.separator?.label - ? `${element.saneAriaLabel}, ${element.separator.label}` - : element.saneAriaLabel; - } - - getWidgetRole(): AriaRole { - return 'listbox'; - } - - getRole(element: IListElement) { - return element.hasCheckbox ? 'checkbox' : 'option'; - } - - isChecked(element: IListElement) { - if (!element.hasCheckbox) { - return undefined; - } - - return { - value: element.checked, - onDidChange: element.onChecked - }; - } -} diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index f2a71af5553..3cd5b67a6af 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; -import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IListOptions, List, unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { unthemedListStyles } from 'vs/base/browser/ui/list/listWidget'; import { unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; +import { Event } from 'vs/base/common/event'; import { raceTimeout } from 'vs/base/common/async'; import { unthemedCountStyles } from 'vs/base/browser/ui/countBadge/countBadge'; import { unthemedKeybindingLabelOptions } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; @@ -20,6 +20,18 @@ import { toDisposable } from 'vs/base/common/lifecycle'; import { mainWindow } from 'vs/base/browser/window'; import { QuickPick } from 'vs/platform/quickinput/browser/quickInput'; import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IListService, ListService } from 'vs/platform/list/browser/listService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; +import { NoMatchingKb } from 'vs/platform/keybinding/common/keybindingResolver'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; // Sets up an `onShow` listener to allow us to wait until the quick pick is shown (useful when triggering an `accept()` right after launching a quick pick) // kick this off before you launch the picker and then await the promise returned after you launch the picker. @@ -45,50 +57,58 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 mainWindow.document.body.appendChild(fixture); store.add(toDisposable(() => mainWindow.document.body.removeChild(fixture))); - controller = store.add(new QuickInputController({ - container: fixture, - idPrefix: 'testQuickInput', - ignoreFocusOut() { return true; }, - returnFocus() { }, - backKeybindingLabel() { return undefined; }, - setContextKey() { return undefined; }, - linkOpenerDelegate(content) { }, - createList: ( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate, - renderers: IListRenderer[], - options: IListOptions, - ) => new List(user, container, delegate, renderers, options), - hoverDelegate: { - showHover(options, focus) { - return undefined; + const instantiationService = new TestInstantiationService(); + + // Stub the services the quick input controller needs to function + instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IListService, store.add(new ListService())); + instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); + instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); + instantiationService.stub(IContextKeyService, store.add(instantiationService.createInstance(ContextKeyService))); + instantiationService.stub(IKeybindingService, { + mightProducePrintableCharacter() { return false; }, + softDispatch() { return NoMatchingKb; }, + }); + + controller = store.add(instantiationService.createInstance( + QuickInputController, + { + container: fixture, + idPrefix: 'testQuickInput', + ignoreFocusOut() { return true; }, + returnFocus() { }, + backKeybindingLabel() { return undefined; }, + setContextKey() { return undefined; }, + linkOpenerDelegate(content) { }, + hoverDelegate: { + showHover(options, focus) { + return undefined; + }, + delay: 200 }, - delay: 200 - }, - styles: { - button: unthemedButtonStyles, - countBadge: unthemedCountStyles, - inputBox: unthemedInboxStyles, - toggle: unthemedToggleStyles, - keybindingLabel: unthemedKeybindingLabelOptions, - list: unthemedListStyles, - progressBar: unthemedProgressBarOptions, - widget: { - quickInputBackground: undefined, - quickInputForeground: undefined, - quickInputTitleBackground: undefined, - widgetBorder: undefined, - widgetShadow: undefined, - }, - pickerGroup: { - pickerGroupBorder: undefined, - pickerGroupForeground: undefined, + styles: { + button: unthemedButtonStyles, + countBadge: unthemedCountStyles, + inputBox: unthemedInboxStyles, + toggle: unthemedToggleStyles, + keybindingLabel: unthemedKeybindingLabelOptions, + list: unthemedListStyles, + progressBar: unthemedProgressBarOptions, + widget: { + quickInputBackground: undefined, + quickInputForeground: undefined, + quickInputTitleBackground: undefined, + widgetBorder: undefined, + widgetShadow: undefined, + }, + pickerGroup: { + pickerGroupBorder: undefined, + pickerGroupForeground: undefined, + } } } - }, - new TestThemeService(), - { activeContainer: fixture } as any)); + )); // initial layout controller.layout({ height: 20, width: 40 }, 0); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index fd003921c47..8808bd0a350 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -171,6 +171,8 @@ import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; import { IEditorPaneService } from 'vs/workbench/services/editor/common/editorPaneService'; import { EditorPaneService } from 'vs/workbench/services/editor/browser/editorPaneService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -332,6 +334,7 @@ export function workbenchInstantiationService( instantiationService.stub(ICodeEditorService, disposables.add(new CodeEditorService(editorService, themeService, configService))); instantiationService.stub(IPaneCompositePartService, disposables.add(new TestPaneCompositeService())); instantiationService.stub(IListService, new TestListService()); + instantiationService.stub(IContextViewService, disposables.add(instantiationService.createInstance(ContextViewService))); instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, layoutService))); instantiationService.stub(IWorkspacesService, new TestWorkspacesService()); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService()));