diff options
Diffstat (limited to 'src/vs/base/browser/ui/tree/abstractTree.ts')
-rw-r--r-- | src/vs/base/browser/ui/tree/abstractTree.ts | 550 |
1 files changed, 278 insertions, 272 deletions
diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 124d926541f..95032ae5ee4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -3,27 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DragAndDropData, IDragAndDropData, StaticDND } from 'vs/base/browser/dnd'; -import { $, addDisposableListener, append, clearNode, createStyleSheet, getDomNodePagePosition, hasParentWithClass } from 'vs/base/browser/dom'; +import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { $, append, clearNode, createStyleSheet, h, hasParentWithClass } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; +import { IMessage, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; -import { DefaultKeyboardNavigationDelegate, IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController } from 'vs/base/browser/ui/list/listWidget'; +import { IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeError, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { Action } from 'vs/base/common/actions'; import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { disposableTimeout, timeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { SetMap } from 'vs/base/common/collections'; +import { Color } from 'vs/base/common/color'; import { Emitter, Event, EventBufferer, Relay } from 'vs/base/common/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; -import { isMacintosh } from 'vs/base/common/platform'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { ISpliceable } from 'vs/base/common/sequence'; +import { isNumber } from 'vs/base/common/types'; import 'vs/css!./media/tree'; import { localize } from 'vs/nls'; @@ -194,8 +201,7 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, getKeyboardNavigationLabel(node) { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(node.element); } - }, - enableKeyboardNavigation: options.simpleKeyboardNavigation + } }; } @@ -563,7 +569,7 @@ class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer export type LabelFuzzyScore = { label: string; score: FuzzyScore }; -class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDisposable { +class FindFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDisposable { private _totalCount = 0; get totalCount(): number { return this._totalCount; } private _matchCount = 0; @@ -590,10 +596,6 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi if (this._filter) { const result = this._filter.filter(element, parentVisibility); - if (this.tree.options.simpleKeyboardNavigation) { - return result; - } - let visibility: TreeVisibility; if (typeof result === 'boolean') { @@ -611,7 +613,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi this._totalCount++; - if (this.tree.options.simpleKeyboardNavigation || !this._pattern) { + if (!this._pattern) { this._matchCount++; return { data: FuzzyScore.Default, visibility: true }; } @@ -634,7 +636,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi } } - if (this.tree.options.filterOnType) { + if (this.tree.findMode === TreeFindMode.Filter) { return TreeVisibility.Recurse; } else { return { data: FuzzyScore.Default, visibility: true }; @@ -651,170 +653,259 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi } } -class TypeFilterController<T, TFilterData> implements IDisposable { +export interface ICaseSensitiveToggleOpts { + readonly isChecked: boolean; + readonly inputActiveOptionBorder?: Color; + readonly inputActiveOptionForeground?: Color; + readonly inputActiveOptionBackground?: Color; +} - private _enabled = false; - get enabled(): boolean { return this._enabled; } +export class ModeToggle extends Toggle { + constructor(opts?: ICaseSensitiveToggleOpts) { + super({ + icon: Codicon.filter, + title: localize('filter', "Filter"), + isChecked: opts?.isChecked ?? false, + inputActiveOptionBorder: opts?.inputActiveOptionBorder, + inputActiveOptionForeground: opts?.inputActiveOptionForeground, + inputActiveOptionBackground: opts?.inputActiveOptionBackground + }); + } +} - private _pattern = ''; - get pattern(): string { return this._pattern; } +export interface IFindWidgetStyles extends IFindInputStyles, IListStyles { } - private _filterOnType: boolean; - get filterOnType(): boolean { return this._filterOnType; } +export interface IFindWidgetOpts extends IFindWidgetStyles { } - private _empty: boolean = false; - get empty(): boolean { return this._empty; } +export enum TreeFindMode { + Highlight, + Filter +} - private readonly _onDidChangeEmptyState = new Emitter<boolean>(); - readonly onDidChangeEmptyState: Event<boolean> = Event.latch(this._onDidChangeEmptyState.event); +class FindWidget<T, TFilterData> extends Disposable { - private positionClassName = 'ne'; - private domNode: HTMLElement; - private messageDomNode: HTMLElement; - private labelDomNode: HTMLElement; - private filterOnTypeDomNode: HTMLInputElement; - private clearDomNode: HTMLElement; - private keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter; + private readonly elements = h('.monaco-tree-type-filter', [ + h('.monaco-tree-type-filter-grab.codicon.codicon-debug-gripper@grab'), + h('.monaco-tree-type-filter-input@findInput'), + h('.monaco-tree-type-filter-actionbar@actionbar'), + ]); - private automaticKeyboardNavigation = true; - private triggered = false; + set mode(mode: TreeFindMode) { + this.modeToggle.checked = mode === TreeFindMode.Filter; + this.findInput.inputBox.setPlaceHolder(mode === TreeFindMode.Filter ? localize('type to filter', "Type to filter") : localize('type to search', "Type to search")); + } - private readonly _onDidChangePattern = new Emitter<string>(); - readonly onDidChangePattern = this._onDidChangePattern.event; + private readonly modeToggle: ModeToggle; + private readonly findInput: FindInput; + private readonly actionbar: ActionBar; + private width = 0; + private right = 4; - private readonly enabledDisposables = new DisposableStore(); - private readonly disposables = new DisposableStore(); + readonly _onDidDisable = new Emitter<void>(); + readonly onDidDisable = this._onDidDisable.event; + readonly onDidChangeValue: Event<string>; + readonly onDidChangeMode: Event<TreeFindMode>; constructor( + container: HTMLElement, private tree: AbstractTree<T, TFilterData, any>, - model: ITreeModel<T, TFilterData, any>, - private view: List<ITreeNode<T, TFilterData>>, - private filter: TypeFilter<T>, - private keyboardNavigationDelegate: IKeyboardNavigationDelegate + contextViewProvider: IContextViewProvider, + mode: TreeFindMode, + options?: IFindWidgetOpts ) { - this.domNode = $(`.monaco-list-type-filter.${this.positionClassName}`); - this.domNode.draggable = true; - this.disposables.add(addDisposableListener(this.domNode, 'dragstart', () => this.onDragStart())); + super(); - this.messageDomNode = append(view.getHTMLElement(), $(`.monaco-list-type-filter-message`)); + container.appendChild(this.elements.root); + this._register(toDisposable(() => container.removeChild(this.elements.root))); - this.labelDomNode = append(this.domNode, $('span.label')); - const controls = append(this.domNode, $('.controls')); + this.modeToggle = this._register(new ModeToggle({ ...options, isChecked: mode === TreeFindMode.Filter })); + this.onDidChangeMode = Event.map(this.modeToggle.onChange, () => this.modeToggle.checked ? TreeFindMode.Filter : TreeFindMode.Highlight, this._store); - this._filterOnType = !!tree.options.filterOnType; - this.filterOnTypeDomNode = append(controls, $<HTMLInputElement>('input.filter')); - this.filterOnTypeDomNode.type = 'checkbox'; - this.filterOnTypeDomNode.checked = this._filterOnType; - this.filterOnTypeDomNode.tabIndex = -1; - this.updateFilterOnTypeTitleAndIcon(); - this.disposables.add(addDisposableListener(this.filterOnTypeDomNode, 'input', () => this.onDidChangeFilterOnType())); + this.findInput = this._register(new FindInput(this.elements.findInput, contextViewProvider, false, { + label: localize('type to search', "Type to search"), + additionalToggles: [this.modeToggle] + })); - this.clearDomNode = append(controls, $<HTMLInputElement>('button.clear' + Codicon.treeFilterClear.cssSelector)); - this.clearDomNode.tabIndex = -1; - this.clearDomNode.title = localize('clear', "Clear"); + this.actionbar = this._register(new ActionBar(this.elements.actionbar)); + this.mode = mode; - this.keyboardNavigationEventFilter = tree.options.keyboardNavigationEventFilter; + const emitter = this._register(new DomEmitter(this.findInput.inputBox.inputElement, 'keydown')); + const onKeyDown = this._register(Event.chain(emitter.event)) + .map(e => new StandardKeyboardEvent(e)) + .event; - model.onDidSplice(this.onDidSpliceModel, this, this.disposables); - this.updateOptions(tree.options); + this._register(onKeyDown((e): any => { + switch (e.keyCode) { + case KeyCode.DownArrow: + e.preventDefault(); + e.stopPropagation(); + this.tree.domFocus(); + return; + } + })); + + const closeAction = this._register(new Action('close', localize('close', "Close"), 'codicon codicon-close', true, () => this.dispose())); + this.actionbar.push(closeAction, { icon: true, label: false }); + + const onGrabMouseDown = this._register(new DomEmitter(this.elements.grab, 'mousedown')); + + this._register(onGrabMouseDown.event(e => { + const disposables = new DisposableStore(); + const onWindowMouseMove = disposables.add(new DomEmitter(window, 'mousemove')); + const onWindowMouseUp = disposables.add(new DomEmitter(window, 'mouseup')); + + const startRight = this.right; + const startX = e.pageX; + this.elements.grab.classList.add('grabbing'); + + const update = (e: MouseEvent) => { + const deltaX = e.pageX - startX; + this.right = startRight - deltaX; + this.layout(); + }; + + disposables.add(onWindowMouseMove.event(update)); + disposables.add(onWindowMouseUp.event(e => { + update(e); + this.elements.grab.classList.remove('grabbing'); + disposables.dispose(); + })); + })); + + + this.onDidChangeValue = this.findInput.onDidChange; + this.style(options ?? {}); } - updateOptions(options: IAbstractTreeOptions<T, TFilterData>): void { - if (options.simpleKeyboardNavigation) { - this.disable(); - } else { - this.enable(); - } + style(styles: IFindWidgetStyles): void { + this.findInput.style(styles); - if (typeof options.filterOnType !== 'undefined') { - this._filterOnType = !!options.filterOnType; - this.filterOnTypeDomNode.checked = this._filterOnType; - this.updateFilterOnTypeTitleAndIcon(); + if (styles.listFilterWidgetBackground) { + this.elements.root.style.backgroundColor = styles.listFilterWidgetBackground.toString(); } - if (typeof options.automaticKeyboardNavigation !== 'undefined') { - this.automaticKeyboardNavigation = options.automaticKeyboardNavigation; + if (styles.listFilterWidgetShadow) { + this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; } + } - this.tree.refilter(); - this.render(); + focus() { + this.findInput.focus(); + } - if (!this.automaticKeyboardNavigation) { - this.onEventOrInput(''); - } + select() { + this.findInput.select(); } - toggle(): void { - this.triggered = !this.triggered; + layout(width: number = this.width): void { + this.width = width; + this.right = Math.min(Math.max(20, this.right), Math.max(20, width - 170)); + this.elements.root.style.right = `${this.right}px`; + } - if (!this.triggered) { - this.onEventOrInput(''); - } + showMessage(message: IMessage): void { + this.findInput.showMessage(message); } - private enable(): void { - if (this._enabled) { + clearMessage(): void { + this.findInput.clearMessage(); + } + + override async dispose(): Promise<void> { + this._onDidDisable.fire(); + this.elements.root.classList.add('disabled'); + await timeout(300); + super.dispose(); + } +} + +class FindController<T, TFilterData> implements IDisposable { + + private _pattern = ''; + get pattern(): string { return this._pattern; } + + private _mode: TreeFindMode; + get mode(): TreeFindMode { return this._mode; } + set mode(mode: TreeFindMode) { + if (mode === this._mode) { return; } - const onRawKeyDown = this.enabledDisposables.add(new DomEmitter(this.view.getHTMLElement(), 'keydown')); - const onKeyDown = Event.chain(onRawKeyDown.event) - .filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode) - .filter(e => e.key !== 'Dead' && !/^Media/.test(e.key)) - .map(e => new StandardKeyboardEvent(e)) - .filter(this.keyboardNavigationEventFilter || (() => true)) - .filter(() => this.automaticKeyboardNavigation || this.triggered) - .filter(e => (this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) && !(e.keyCode === KeyCode.DownArrow || e.keyCode === KeyCode.UpArrow || e.keyCode === KeyCode.LeftArrow || e.keyCode === KeyCode.RightArrow)) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey))) - .forEach(e => { e.stopPropagation(); e.preventDefault(); }) - .event; - - const onClearClick = this.enabledDisposables.add(new DomEmitter(this.clearDomNode, 'click')); + this._mode = mode; - Event.chain(Event.any<MouseEvent | StandardKeyboardEvent>(onKeyDown, onClearClick.event)) - .event(this.onEventOrInput, this, this.enabledDisposables); + if (this.widget) { + this.widget.mode = this._mode; + } - this.filter.pattern = ''; this.tree.refilter(); this.render(); - this._enabled = true; - this.triggered = false; + this._onDidChangeMode.fire(mode); + } + + private widget: FindWidget<T, TFilterData> | undefined; + private styles: IFindWidgetStyles | undefined; + private width = 0; + + private readonly _onDidChangeMode = new Emitter<TreeFindMode>(); + readonly onDidChangeMode = this._onDidChangeMode.event; + + private readonly _onDidChangePattern = new Emitter<string>(); + readonly onDidChangePattern = this._onDidChangePattern.event; + + private readonly _onDidChangeOpenState = new Emitter<boolean>(); + readonly onDidChangeOpenState = this._onDidChangeOpenState.event; + + private enabledDisposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); + + constructor( + private tree: AbstractTree<T, TFilterData, any>, + model: ITreeModel<T, TFilterData, any>, + private view: List<ITreeNode<T, TFilterData>>, + private filter: FindFilter<T>, + private readonly contextViewProvider: IContextViewProvider + ) { + this._mode = tree.options.defaultFindMode ?? TreeFindMode.Highlight; + model.onDidSplice(this.onDidSpliceModel, this, this.disposables); } - private disable(): void { - if (!this._enabled) { + open(): void { + if (this.widget) { + this.widget.focus(); + this.widget.select(); return; } - this.domNode.remove(); - this.enabledDisposables.clear(); - this.tree.refilter(); - this.render(); - this._enabled = false; - this.triggered = false; + this.widget = new FindWidget(this.view.getHTMLElement(), this.tree, this.contextViewProvider, this._mode, this.styles); + this.enabledDisposables.add(this.widget); + + this.widget.onDidChangeValue(this.onDidChangeValue, this, this.enabledDisposables); + this.widget.onDidChangeMode(mode => this.mode = mode, undefined, this.enabledDisposables); + this.widget.onDidDisable(this.close, this, this.enabledDisposables); + + this.widget.layout(this.width); + this.widget.focus(); + + this._onDidChangeOpenState.fire(true); } - private onEventOrInput(e: MouseEvent | StandardKeyboardEvent | string): void { - if (typeof e === 'string') { - this.onInput(e); - } else if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) { - this.onInput(''); - } else if (e.keyCode === KeyCode.Backspace) { - this.onInput(this.pattern.length === 0 ? '' : this.pattern.substr(0, this.pattern.length - 1)); - } else { - this.onInput(this.pattern + e.browserEvent.key); + close(): void { + if (!this.widget) { + return; } - } - private onInput(pattern: string): void { - const container = this.view.getHTMLElement(); + this.widget = undefined; - if (pattern && !this.domNode.parentElement) { - container.append(this.domNode); - } else if (!pattern && this.domNode.parentElement) { - this.domNode.remove(); - this.tree.domFocus(); - } + this.enabledDisposables.dispose(); + this.enabledDisposables = new DisposableStore(); + this.onDidChangeValue(''); + this.tree.domFocus(); + + this._onDidChangeOpenState.fire(false); + } + + private onDidChangeValue(pattern: string): void { this._pattern = pattern; this._onDidChangePattern.fire(pattern); @@ -836,75 +927,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable { } this.render(); - - if (!pattern) { - this.triggered = false; - } - } - - private onDragStart(): void { - const container = this.view.getHTMLElement(); - const { left } = getDomNodePagePosition(container); - const containerWidth = container.clientWidth; - const midContainerWidth = containerWidth / 2; - const width = this.domNode.clientWidth; - const disposables = new DisposableStore(); - let positionClassName = this.positionClassName; - - const updatePosition = () => { - switch (positionClassName) { - case 'nw': - this.domNode.style.top = `4px`; - this.domNode.style.left = `4px`; - break; - case 'ne': - this.domNode.style.top = `4px`; - this.domNode.style.left = `${containerWidth - width - 6}px`; - break; - } - }; - - const onDragOver = (event: DragEvent) => { - event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) - - const x = event.clientX - left; - if (event.dataTransfer) { - event.dataTransfer.dropEffect = 'none'; - } - - if (x < midContainerWidth) { - positionClassName = 'nw'; - } else { - positionClassName = 'ne'; - } - - updatePosition(); - }; - - const onDragEnd = () => { - this.positionClassName = positionClassName; - this.domNode.className = `monaco-list-type-filter ${this.positionClassName}`; - this.domNode.style.top = ''; - this.domNode.style.left = ''; - - dispose(disposables); - }; - - updatePosition(); - this.domNode.classList.remove(positionClassName); - - this.domNode.classList.add('dragging'); - disposables.add(toDisposable(() => this.domNode.classList.remove('dragging'))); - - disposables.add(addDisposableListener(document, 'dragover', e => onDragOver(e))); - disposables.add(addDisposableListener(this.domNode, 'dragend', () => onDragEnd())); - - StaticDND.CurrentDragAndDropData = new DragAndDropData('vscode-ui'); - disposables.add(toDisposable(() => StaticDND.CurrentDragAndDropData = undefined)); } private onDidSpliceModel(): void { - if (!this._enabled || this.pattern.length === 0) { + if (!this.widget || this.pattern.length === 0) { return; } @@ -912,46 +938,18 @@ class TypeFilterController<T, TFilterData> implements IDisposable { this.render(); } - private onDidChangeFilterOnType(): void { - this.tree.updateOptions({ filterOnType: this.filterOnTypeDomNode.checked }); - this.tree.refilter(); - this.tree.domFocus(); - this.render(); - this.updateFilterOnTypeTitleAndIcon(); - } - - private updateFilterOnTypeTitleAndIcon(): void { - if (this.filterOnType) { - this.filterOnTypeDomNode.classList.remove(...Codicon.treeFilterOnTypeOff.classNamesArray); - this.filterOnTypeDomNode.classList.add(...Codicon.treeFilterOnTypeOn.classNamesArray); - this.filterOnTypeDomNode.title = localize('disable filter on type', "Disable Filter on Type"); - } else { - this.filterOnTypeDomNode.classList.remove(...Codicon.treeFilterOnTypeOn.classNamesArray); - this.filterOnTypeDomNode.classList.add(...Codicon.treeFilterOnTypeOff.classNamesArray); - this.filterOnTypeDomNode.title = localize('enable filter on type', "Enable Filter on Type"); - } - } - private render(): void { const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0; - if (this.pattern && this.tree.options.filterOnType && noMatches) { - this.messageDomNode.textContent = localize('empty', "No elements found"); - this._empty = true; + if (this.pattern && noMatches) { + this.widget?.showMessage({ type: MessageType.WARNING, content: localize('not found', "No elements found.") }); } else { - this.messageDomNode.innerText = ''; - this._empty = false; + this.widget?.clearMessage(); } - - this.domNode.classList.toggle('no-matches', noMatches); - this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount); - this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern; - - this._onDidChangeEmptyState.fire(this._empty); } shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean { - if (!this.enabled || !this.pattern || this.filterOnType) { + if (!this.widget || !this.pattern || this._mode === TreeFindMode.Filter) { return true; } @@ -962,16 +960,20 @@ class TypeFilterController<T, TFilterData> implements IDisposable { return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); } - dispose() { - if (this._enabled) { - this.domNode.remove(); - this.enabledDisposables.dispose(); - this._enabled = false; - this.triggered = false; - } + style(styles: IFindWidgetStyles): void { + this.styles = styles; + this.widget?.style(styles); + } + + layout(width: number): void { + this.width = width; + this.widget?.layout(width); + } + dispose() { this._onDidChangePattern.dispose(); - dispose(this.disposables); + this.enabledDisposables.dispose(); + this.disposables.dispose(); } } @@ -982,6 +984,8 @@ function asTreeMouseEvent<T>(event: IListMouseEvent<ITreeNode<T, any>>): ITreeMo target = TreeMouseEventTarget.Twistie; } else if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tl-contents', 'monaco-tl-row')) { target = TreeMouseEventTarget.Element; + } else if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tree-type-filter', 'monaco-list')) { + target = TreeMouseEventTarget.Filter; } return { @@ -1005,9 +1009,9 @@ export interface IKeyboardNavigationEventFilter { export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly multipleSelectionSupport?: boolean; - readonly automaticKeyboardNavigation?: boolean; - readonly simpleKeyboardNavigation?: boolean; - readonly filterOnType?: boolean; + readonly typeNavigationEnabled?: boolean; + readonly typeNavigationMode?: TypeNavigationMode; + readonly defaultFindMode?: TreeFindMode; readonly smoothScrolling?: boolean; readonly horizontalScrolling?: boolean; readonly mouseWheelScrollSensitivity?: number; @@ -1017,6 +1021,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { } export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTreeOptionsUpdate, IListOptions<T> { + readonly contextViewProvider?: IContextViewProvider; readonly collapseByDefault?: boolean; // defaults to false readonly filter?: ITreeFilter<T, TFilterData>; readonly dnd?: ITreeDragAndDrop<T>; @@ -1318,7 +1323,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable private selection: Trait<T>; private anchor: Trait<T>; private eventBufferer = new EventBufferer(); - private typeFilterController?: TypeFilterController<T, TFilterData>; + private findController?: FindController<T, TFilterData>; + readonly onDidChangeFindOpenState: Event<boolean> = Event.None; private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined; private styleElement: HTMLStyleElement; protected readonly disposables = new DisposableStore(); @@ -1329,7 +1335,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable get onDidChangeSelection(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); } get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } - get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseDblClick, asTreeMouseEvent); } + get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.filter(Event.map(this.view.onMouseDblClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); } get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.view.onContextMenu, asTreeContextMenuEvent); } get onTap(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onTap, asTreeMouseEvent); } get onPointer(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onPointer, asTreeMouseEvent); } @@ -1348,8 +1354,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable private readonly _onWillRefilter = new Emitter<void>(); readonly onWillRefilter: Event<void> = this._onWillRefilter.event; - get filterOnType(): boolean { return !!this._options.filterOnType; } - get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; } + get findMode(): TreeFindMode { return this.findController?.mode ?? TreeFindMode.Highlight; } + set findMode(findMode: TreeFindMode) { if (this.findController) { this.findController.mode = findMode; } } + readonly onDidChangeFindMode: Event<TreeFindMode>; + + get onDidChangeFindPattern(): Event<string> { return this.findController ? this.findController.onDidChangePattern : Event.None; } get expandOnDoubleClick(): boolean { return typeof this._options.expandOnDoubleClick === 'undefined' ? true : this._options.expandOnDoubleClick; } get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? true : this._options.expandOnlyOnTwistieClick; } @@ -1376,10 +1385,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable this.disposables.add(r); } - let filter: TypeFilter<T> | undefined; + let filter: FindFilter<T> | undefined; if (_options.keyboardNavigationLabelProvider) { - filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>); + filter = new FindFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>); _options = { ..._options, filter: filter as ITreeFilter<T, TFilterData> }; // TODO need typescript help here this.disposables.add(filter); } @@ -1432,11 +1441,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); } - if (_options.keyboardNavigationLabelProvider) { - const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate; - this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, delegate); - this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node); - this.disposables.add(this.typeFilterController!); + if (_options.keyboardNavigationLabelProvider && _options.contextViewProvider) { + this.findController = new FindController(this, this.model, this.view, filter!, _options.contextViewProvider); + this.focusNavigationFilter = node => this.findController!.shouldAllowFocus(node); + this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState; + this.disposables.add(this.findController!); + this.onDidChangeFindMode = this.findController.onDidChangeMode; + } else { + this.onDidChangeFindMode = Event.None; } this.styleElement = createStyleSheet(this.view.getHTMLElement()); @@ -1450,13 +1462,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable renderer.updateOptions(optionsUpdate); } - this.view.updateOptions({ - ...this._options, - enableKeyboardNavigation: this._options.simpleKeyboardNavigation, - }); - - this.typeFilterController?.updateOptions(this._options); - + this.view.updateOptions(this._options); this._onDidUpdateOptions.fire(this._options); this.getHTMLElement().classList.toggle('always', this._options.renderIndentGuides === RenderIndentGuides.Always); @@ -1483,21 +1489,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable } get contentHeight(): number { - if (this.typeFilterController && this.typeFilterController.filterOnType && this.typeFilterController.empty) { - return 100; - } - return this.view.contentHeight; } get onDidChangeContentHeight(): Event<number> { - let result = this.view.onDidChangeContentHeight; - - if (this.typeFilterController) { - result = Event.any(result, Event.map(this.typeFilterController.onDidChangeEmptyState, () => this.contentHeight)); - } - - return result; + return this.view.onDidChangeContentHeight; } get scrollTop(): number { @@ -1559,6 +1555,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable layout(height?: number, width?: number): void { this.view.layout(height, width); + + if (isNumber(width)) { + this.findController?.layout(width); + } } style(styles: IListStyles): void { @@ -1571,6 +1571,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable } this.styleElement.textContent = content.join('\n'); + + this.findController?.style(styles); this.view.style(styles); } @@ -1624,12 +1626,16 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable return this.model.isCollapsed(location); } - toggleKeyboardNavigation(): void { - this.view.toggleKeyboardNavigation(); + triggerTypeNavigation(): void { + this.view.triggerTypeNavigation(); + } - if (this.typeFilterController) { - this.typeFilterController.toggle(); - } + openFind(): void { + this.findController?.open(); + } + + closeFind(): void { + this.findController?.close(); } refilter(): void { |