diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 1ff4a7f6dc0..036b4ddf543 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -8,7 +8,7 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/l import { IListOptions, List, IListStyles, mightProducePrintableCharacter, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass } from 'vs/base/browser/dom'; -import { Event, Relay, Emitter } from 'vs/base/common/event'; +import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; @@ -22,6 +22,7 @@ import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTr import { localize } from 'vs/nls'; import { disposableTimeout } from 'vs/base/common/async'; import { isMacintosh } from 'vs/base/common/platform'; +import { values } from 'vs/base/common/map'; function asTreeDragAndDropData(data: IDragAndDropData): IDragAndDropData { if (data instanceof ElementsDragAndDropData) { @@ -641,12 +642,135 @@ export interface IAbstractTreeOptions extends IAbstractTr readonly autoExpandSingleChildren?: boolean; } +/** + * The trait concept needs to exist at the tree level, because collapsed + * tree nodes will not be known by the list. + */ +class Trait { + + private nodes: ITreeNode[] = []; + private elements: T[] | undefined; + + private _onDidChange = new Emitter>(); + readonly onDidChange = this._onDidChange.event; + + private _nodeSet: Set> | undefined; + private get nodeSet(): Set> { + if (!this._nodeSet) { + this._nodeSet = new Set(); + + for (const node of this.nodes) { + this._nodeSet.add(node); + } + } + + return this._nodeSet; + } + + set(nodes: ITreeNode[], browserEvent?: UIEvent): void { + this.nodes = [...nodes]; + this.elements = undefined; + this._nodeSet = undefined; + + const that = this; + this._onDidChange.fire({ get elements() { return that.get(); }, browserEvent }); + } + + get(): T[] { + if (!this.elements) { + this.elements = this.nodes.map(node => node.element); + } + + return [...this.elements]; + } + + has(node: ITreeNode): boolean { + return this.nodeSet.has(node); + } + + remove(nodes: ITreeNode[]): void { + const set = this.nodeSet; + + for (const node of nodes) { + set.delete(node); + } + + this.set(values(set)); + } +} + +/** + * We use this List subclass to restore selection and focus as nodes + * get rendered in the list, possibly due to a node expand() call. + */ +class TreeNodeList extends List> { + + constructor( + container: HTMLElement, + virtualDelegate: IListVirtualDelegate>, + renderers: IListRenderer[], + private focusTrait: Trait, + private selectionTrait: Trait, + options?: IListOptions> + ) { + super(container, virtualDelegate, renderers, options); + } + + splice(start: number, deleteCount: number, elements: ITreeNode[] = []): void { + super.splice(start, deleteCount, elements); + + if (elements.length === 0) { + return; + } + + const additionalFocus: number[] = []; + const additionalSelection: number[] = []; + + elements.forEach((node, index) => { + if (this.selectionTrait.has(node)) { + additionalFocus.push(start + index); + } + + if (this.selectionTrait.has(node)) { + additionalSelection.push(start + index); + } + }); + + if (additionalFocus.length > 0) { + super.setFocus([...super.getFocus(), ...additionalFocus]); + } + + if (additionalSelection.length > 0) { + super.setSelection([...super.getSelection(), ...additionalSelection]); + } + } + + setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void { + super.setFocus(indexes, browserEvent); + + if (!fromAPI) { + this.focusTrait.set(indexes.map(i => this.element(i)), browserEvent); + } + } + + setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void { + super.setSelection(indexes, browserEvent); + + if (!fromAPI) { + this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent); + } + } +} + export abstract class AbstractTree implements IDisposable { - private view: List>; + private view: TreeNodeList; private renderers: TreeRenderer[]; private focusNavigationFilter: ((node: ITreeNode) => boolean) | undefined; protected model: ITreeModel; + private focus = new Trait(); + private selection = new Trait(); + private eventBufferer = new EventBufferer(); protected disposables: IDisposable[] = []; private _onDidUpdateOptions = new Emitter>(); @@ -654,8 +778,8 @@ export abstract class AbstractTree implements IDisposable get onDidScroll(): Event { return this.view.onDidScroll; } - get onDidChangeFocus(): Event> { return Event.map(this.view.onFocusChange, asTreeEvent); } - get onDidChangeSelection(): Event> { return Event.map(this.view.onSelectionChange, asTreeEvent); } + readonly onDidChangeFocus: Event> = this.eventBufferer.wrapEvent(this.focus.onDidChange); + readonly onDidChangeSelection: Event> = this.eventBufferer.wrapEvent(this.selection.onDidChange); get onDidOpen(): Event> { return Event.map(this.view.onDidOpen, asTreeEvent); } get onMouseClick(): Event> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } @@ -697,11 +821,18 @@ export abstract class AbstractTree implements IDisposable this.disposables.push(filter); } - this.view = new List(container, treeDelegate, this.renderers, asListOptions(() => this.model, _options)); + this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, asListOptions(() => this.model, _options)); this.model = this.createModel(this.view, _options); onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; + this.model.onDidSplice(e => { + this.eventBufferer.bufferEvents(() => { + this.focus.remove(e.deletedNodes); + this.selection.remove(e.deletedNodes); + }); + }, null, this.disposables); + this.view.onTap(this.reactOnMouseClick, this, this.disposables); this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables); @@ -853,18 +984,23 @@ export abstract class AbstractTree implements IDisposable } setSelection(elements: TRef[], browserEvent?: UIEvent): void { - const indexes = elements.map(e => this.model.getListIndex(e)); - this.view.setSelection(indexes, browserEvent); + const nodes = elements.map(e => this.model.getNode(e)); + this.selection.set(nodes, browserEvent); + + const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1); + this.view.setSelection(indexes, browserEvent, true); } getSelection(): T[] { - const nodes = this.view.getSelectedElements(); - return nodes.map(n => n.element); + return this.selection.get(); } setFocus(elements: TRef[], browserEvent?: UIEvent): void { - const indexes = elements.map(e => this.model.getListIndex(e)); - this.view.setFocus(indexes, browserEvent); + const nodes = elements.map(e => this.model.getNode(e)); + this.focus.set(nodes, browserEvent); + + const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1); + this.view.setFocus(indexes, browserEvent, true); } focusNext(n = 1, loop = false, browserEvent?: UIEvent): void { @@ -892,8 +1028,7 @@ export abstract class AbstractTree implements IDisposable } getFocus(): T[] { - const nodes = this.view.getFocusedElements(); - return nodes.map(n => n.element); + return this.focus.get(); } open(elements: TRef[]): void { diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index bea50064994..cc6555950aa 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree'; import { tail2 } from 'vs/base/common/arrays'; import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; import { ISequence, Iterator } from 'vs/base/common/iterator'; @@ -61,7 +61,7 @@ export class IndexTreeModel, TFilterData = voi private filter?: ITreeFilter; private autoExpandSingleChildren: boolean; - private _onDidSplice = new Emitter(); + private _onDidSplice = new Emitter>(); readonly onDidSplice = this._onDidSplice.event; constructor(private list: ISpliceable>, rootElement: T, options: IIndexTreeModelOptions = {}) { @@ -127,7 +127,7 @@ export class IndexTreeModel, TFilterData = voi } const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement); - this._onDidSplice.fire(undefined); + this._onDidSplice.fire({ deletedNodes }); return result; } @@ -144,8 +144,8 @@ export class IndexTreeModel, TFilterData = voi } getListIndex(location: number[]): number { - const { listIndex, visible } = this.getTreeNodeWithListIndex(location); - return visible ? listIndex : -1; + const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location); + return visible && revealed ? listIndex : -1; } getListRenderCount(location: number[]): number { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 467a7c76e2f..3169271298e 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -7,7 +7,7 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator'; import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel'; import { Event } from 'vs/base/common/event'; -import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree'; export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { readonly sorter?: ITreeSorter; @@ -21,7 +21,7 @@ export class ObjectTreeModel, TFilterData extends Non private nodes = new Map>(); private sorter?: ITreeSorter>; - readonly onDidSplice: Event; + readonly onDidSplice: Event>; readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 7cf94dbb023..4880293c269 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -95,10 +95,14 @@ export interface ICollapseStateChangeEvent { deep: boolean; } +export interface ITreeModelSpliceEvent { + deletedNodes: ITreeNode[]; +} + export interface ITreeModel { readonly rootRef: TRef; - readonly onDidSplice: Event; + readonly onDidSplice: Event>; readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>;