From cabd0e4885e44ceae1d1d782fa2de26b016832f7 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 29 Nov 2018 14:47:41 +0100 Subject: [PATCH] async data tree: preserve collapse state given identityProvider --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 161 +++++++++++++------ src/vs/platform/list/browser/listService.ts | 5 - test/tree/public/index.html | 20 ++- 3 files changed, 128 insertions(+), 58 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index fa4893dc150..04c2fd8111d 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -5,14 +5,14 @@ import { ComposedTreeDelegate, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter, Event, mapEvent } from 'vs/base/common/event'; import { timeout, always } from 'vs/base/common/async'; -import { ISequence } from 'vs/base/common/iterator'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { toggleClass } from 'vs/base/browser/dom'; +import { Iterator } from 'vs/base/common/iterator'; export interface IDataSource> { hasChildren(element: T | null): boolean; @@ -29,6 +29,8 @@ enum AsyncDataTreeNodeState { interface IAsyncDataTreeNode> { readonly element: T | null; readonly parent: IAsyncDataTreeNode | null; + readonly id?: string | null; + readonly children?: IAsyncDataTreeNode[]; state: AsyncDataTreeNodeState; } @@ -152,24 +154,34 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOptions extends IAbstractTreeOptions { } +function asTreeElement(node: IAsyncDataTreeNode): ITreeElement> { + return { + element: node, + children: Iterator.map(Iterator.fromArray(node.children!), asTreeElement) + }; +} + +export interface IAsyncDataTreeOptions extends IAbstractTreeOptions { + identityProvider?: IIdentityProvider; +} export class AsyncDataTree, TFilterData = void> implements IDisposable { - private tree: ObjectTree, TFilterData>; - private root: IAsyncDataTreeNode; - private nodes = new Map>(); - private refreshPromises = new Map, Thenable>(); + private readonly tree: ObjectTree, TFilterData>; + private readonly root: IAsyncDataTreeNode; + private readonly nodes = new Map>(); + private readonly refreshPromises = new Map, Thenable>(); + private readonly identityProvider?: IIdentityProvider; - private _onDidChangeNodeState = new Emitter>(); + private readonly _onDidChangeNodeState = new Emitter>(); - protected disposables: IDisposable[] = []; + protected readonly disposables: IDisposable[] = []; get onDidChangeFocus(): Event> { return mapEvent(this.tree.onDidChangeFocus, asTreeEvent); } get onDidChangeSelection(): Event> { return mapEvent(this.tree.onDidChangeSelection, asTreeEvent); } get onDidChangeCollapseState(): Event { return mapEvent(this.tree.onDidChangeCollapseState, e => e.element!.element!); } - private _onDidResolveChildren = new Emitter>(); + private readonly _onDidResolveChildren = new Emitter>(); readonly onDidResolveChildren: Event> = this._onDidResolveChildren.event; get onMouseClick(): Event> { return mapEvent(this.tree.onMouseClick, asTreeMouseEvent); } @@ -187,18 +199,29 @@ export class AsyncDataTree, TFilterData = void> imple private dataSource: IDataSource, options?: IAsyncDataTreeOptions ) { + this.identityProvider = options && options.identityProvider; + const objectTreeDelegate = new ComposedTreeDelegate>(delegate); const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event)); const objectTreeOptions = asObjectTreeOptions(options) || {}; objectTreeOptions.collapseByDefault = true; this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); + this.root = { element: null, parent: null, state: AsyncDataTreeNodeState.Uninitialized, }; + if (this.identityProvider) { + this.root = { + ...this.root, + id: null, + children: [], + }; + } + this.nodes.set(null, this.root); this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables); @@ -236,8 +259,8 @@ export class AsyncDataTree, TFilterData = void> imple // Data Tree - refresh(element: T | null): Thenable { - return this.refreshNode(this.getDataNode(element), ChildrenResolutionReason.Refresh); + refresh(element: T | null, recursive = false): Thenable { + return this.refreshNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh); } // Tree @@ -262,7 +285,7 @@ export class AsyncDataTree, TFilterData = void> imple this.tree.expand(node); if (node.element!.state === AsyncDataTreeNodeState.Uninitialized) { - await this.refreshNode(node, ChildrenResolutionReason.Expand); + await this.refreshNode(node, false, ChildrenResolutionReason.Expand); } return true; @@ -352,13 +375,13 @@ export class AsyncDataTree, TFilterData = void> imple return node && node.element; } - getFirstElementChild(element: T | null = null): T | null { + getFirstElementChild(element: T | null = null): T | null | undefined { const dataNode = this.getDataNode(element); const node = this.tree.getFirstElementChild(dataNode === this.root ? null : dataNode); return node && node.element; } - getLastElementAncestor(element: T | null = null): T | null { + getLastElementAncestor(element: T | null = null): T | null | undefined { const dataNode = this.getDataNode(element); const node = this.tree.getLastElementAncestor(dataNode === this.root ? null : dataNode); return node && node.element; @@ -382,23 +405,23 @@ export class AsyncDataTree, TFilterData = void> imple return node; } - private refreshNode(node: IAsyncDataTreeNode, reason: ChildrenResolutionReason): Thenable { + private refreshNode(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Thenable { let result = this.refreshPromises.get(node); if (result) { return result; } - result = this.doRefresh(node, reason); + result = this.doRefresh(node, recursive, reason); this.refreshPromises.set(node, result); return always(result, () => this.refreshPromises.delete(node)); } - private doRefresh(node: IAsyncDataTreeNode, reason: ChildrenResolutionReason): Thenable { + private doRefresh(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Thenable { const hasChildren = this.dataSource.hasChildren(node.element); if (!hasChildren) { - this.setChildren(node); + this.setChildren(node, [], recursive); return Promise.resolve(); } else { node.state = AsyncDataTreeNodeState.Loading; @@ -417,21 +440,7 @@ export class AsyncDataTree, TFilterData = void> imple node.state = AsyncDataTreeNodeState.Loaded; this._onDidChangeNodeState.fire(node); - const createTreeElement = (element: T): ITreeElement> => { - const collapsible = this.dataSource.hasChildren(element); - - return { - element: { - element: element, - state: AsyncDataTreeNodeState.Uninitialized, - parent: node - }, - collapsible - }; - }; - - const nodeChildren = children.map>>(createTreeElement); - this.setChildren(node, nodeChildren); + this.setChildren(node, children, recursive); this._onDidResolveChildren.fire({ element: node.element, reason }); }, err => { slowTimeout.cancel(); @@ -449,32 +458,92 @@ export class AsyncDataTree, TFilterData = void> imple private _onDidChangeCollapseState(treeNode: ITreeNode, any>): void { if (!treeNode.collapsed && treeNode.element.state === AsyncDataTreeNodeState.Uninitialized) { - this.refreshNode(treeNode.element, ChildrenResolutionReason.Expand); + this.refreshNode(treeNode.element, false, ChildrenResolutionReason.Expand); } } - private setChildren(element: IAsyncDataTreeNode, children?: ISequence>>): void { + private setChildren(node: IAsyncDataTreeNode, childrenElements: T[], recursive: boolean): void { + const children = childrenElements.map>>(element => { + if (!this.identityProvider) { + return { + element: { + element, + parent: node, + state: AsyncDataTreeNodeState.Uninitialized + }, + collapsible: this.dataSource.hasChildren(element), + collapsed: true + }; + } + + const nodeChildren = new Map>(); + + for (const child of node.children!) { + nodeChildren.set(child.id!, child); + } + + const id = this.identityProvider.getId(element).toString(); + const asyncDataTreeNode = nodeChildren.get(id); + + if (!asyncDataTreeNode) { + return { + element: { + element, + parent: node, + id, + children: [], + state: AsyncDataTreeNodeState.Uninitialized + }, + collapsible: this.dataSource.hasChildren(element), + collapsed: true + }; + } + + // TODO + // if (recursive) { + // asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized; + + // if (this.tree.isCollapsed(asyncDataTreeNode)) { + // asyncDataTreeNode.children!.length = 0; + + // return { + // element: asyncDataTreeNode, + // collapsible: this.dataSource.hasChildren(element), + // }; + // } + // } + + return { + element: asyncDataTreeNode, + children: Iterator.map(Iterator.fromArray(asyncDataTreeNode.children!), asTreeElement) + }; + }); + const insertedElements = new Set(); - const onDidCreateNode = (node: ITreeNode, TFilterData>) => { - if (node.element.element) { - insertedElements.add(node.element.element); - this.nodes.set(node.element.element, node.element); + const onDidCreateNode = (treeNode: ITreeNode, TFilterData>) => { + if (treeNode.element.element) { + insertedElements.add(treeNode.element.element); + this.nodes.set(treeNode.element.element, treeNode.element); } }; - const onDidDeleteNode = (node: ITreeNode, TFilterData>) => { - if (node.element.element) { - if (!insertedElements.has(node.element.element)) { - this.nodes.delete(node.element.element); + const onDidDeleteNode = (treeNode: ITreeNode, TFilterData>) => { + if (treeNode.element.element) { + if (!insertedElements.has(treeNode.element.element)) { + this.nodes.delete(treeNode.element.element); } } }; - this.tree.setChildren(element === this.root ? null : element, children, onDidCreateNode, onDidDeleteNode); + this.tree.setChildren(node === this.root ? null : node, children, onDidCreateNode, onDidDeleteNode); + + if (this.identityProvider) { + node.children!.splice(0, node.children!.length, ...children.map(c => c.element)); + } } dispose(): void { - this.disposables = dispose(this.disposables); + dispose(this.disposables); } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index e01c4c7611c..d947e106f4e 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -987,11 +987,6 @@ export class WorkbenchAsyncDataTree, TFilterData = vo }) ); } - - dispose(): void { - super.dispose(); - this.disposables = dispose(this.disposables); - } } const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); diff --git a/test/tree/public/index.html b/test/tree/public/index.html index 09807177b52..0338fae23fd 100644 --- a/test/tree/public/index.html +++ b/test/tree/public/index.html @@ -138,13 +138,13 @@ }; const dataSource = new class { - hasChildren(node) { - return node === null || node.element.type === 'dir'; + hasChildren(element) { + return element === null || element.element.type === 'dir'; } - getChildren(node) { + getChildren(element) { return new Promise((c, e) => { const xhr = new XMLHttpRequest(); - xhr.open('GET', node ? `/api/readdir?path=${node.element.path}` : '/api/readdir'); + xhr.open('GET', element ? `/api/readdir?path=${element.element.path}` : '/api/readdir'); xhr.send(); xhr.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { @@ -153,7 +153,7 @@ collapsible: element.type === 'dir' })); - if (node && /\//.test(node.element.path)) { + if (element) { setTimeout(() => c(els), 2500); } else { c(els); @@ -164,7 +164,13 @@ } } - const tree = new AsyncDataTree(container, delegate, [renderer], dataSource, { filter: treeFilter }); + const identityProvider = { + getId(node) { + return node.element.path; + } + }; + + const tree = new AsyncDataTree(container, delegate, [renderer], dataSource, { filter: treeFilter, identityProvider }); return { tree, treeFilter }; } @@ -195,7 +201,7 @@ collapseall.onclick = () => perf('collapse all', () => tree.collapseAll()); renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random())); - refresh.onclick = () => perf('refresh', () => tree.refresh(null)); + refresh.onclick = () => perf('refresh', () => tree.refresh(null, true)); tree.refresh(null);