/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; import { IndexTreeModel, IIndexTreeModelOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { mergeSort } from 'vs/base/common/arrays'; export type ITreeNodeCallback = (node: ITreeNode) => void; export interface IObjectTreeModel, TFilterData extends NonNullable = void> extends ITreeModel { setChildren(element: T | null, children: Iterable> | undefined): void; resort(element?: T | null, recursive?: boolean): void; updateElementHeight(element: T, height: number): void; } export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { readonly sorter?: ITreeSorter; readonly identityProvider?: IIdentityProvider; } export class ObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { readonly rootRef = null; private model: IndexTreeModel; private nodes = new Map>(); private readonly nodesByIdentity = new Map>(); private readonly identityProvider?: IIdentityProvider; private sorter?: ITreeSorter<{ element: T; }>; readonly onDidSplice: Event>; readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; get size(): number { return this.nodes.size; } constructor( private user: string, list: IList>, options: IObjectTreeModelOptions = {} ) { this.model = new IndexTreeModel(user, list, null, options); this.onDidSplice = this.model.onDidSplice; this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event>; this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event>; if (options.sorter) { this.sorter = { compare(a, b) { return options.sorter!.compare(a.element, b.element); } }; } this.identityProvider = options.identityProvider; } setChildren( element: T | null, children: Iterable> = Iterable.empty(), onDidCreateNode?: ITreeNodeCallback, onDidDeleteNode?: ITreeNodeCallback ): void { const location = this.getElementLocation(element); this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode); } private _setChildren( location: number[], children: Iterable> = Iterable.empty(), onDidCreateNode?: ITreeNodeCallback, onDidDeleteNode?: ITreeNodeCallback ): void { const insertedElements = new Set(); const insertedElementIds = new Set(); const _onDidCreateNode = (node: ITreeNode) => { if (node.element === null) { return; } const tnode = node as ITreeNode; insertedElements.add(tnode.element); this.nodes.set(tnode.element, tnode); if (this.identityProvider) { const id = this.identityProvider.getId(tnode.element).toString(); insertedElementIds.add(id); this.nodesByIdentity.set(id, tnode); } if (onDidCreateNode) { onDidCreateNode(tnode); } }; const _onDidDeleteNode = (node: ITreeNode) => { if (node.element === null) { return; } const tnode = node as ITreeNode; if (!insertedElements.has(tnode.element)) { this.nodes.delete(tnode.element); } if (this.identityProvider) { const id = this.identityProvider.getId(tnode.element).toString(); if (!insertedElementIds.has(id)) { this.nodesByIdentity.delete(id); } } if (onDidDeleteNode) { onDidDeleteNode(tnode); } }; this.model.splice( [...location, 0], Number.MAX_VALUE, children, _onDidCreateNode, _onDidDeleteNode ); } private preserveCollapseState(elements: Iterable> = Iterable.empty()): Iterable> { if (this.sorter) { elements = mergeSort([...elements], this.sorter.compare.bind(this.sorter)); } return Iterable.map(elements, treeElement => { let node = this.nodes.get(treeElement.element); if (!node && this.identityProvider) { const id = this.identityProvider.getId(treeElement.element).toString(); node = this.nodesByIdentity.get(id); } if (!node) { return { ...treeElement, children: this.preserveCollapseState(treeElement.children) }; } const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible; const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : node.collapsed; return { ...treeElement, collapsible, collapsed, children: this.preserveCollapseState(treeElement.children) }; }); } rerender(element: T | null): void { const location = this.getElementLocation(element); this.model.rerender(location); } updateElementHeight(element: T, height: number): void { const location = this.getElementLocation(element); this.model.updateElementHeight(location, height); } resort(element: T | null = null, recursive = true): void { if (!this.sorter) { return; } const location = this.getElementLocation(element); const node = this.model.getNode(location); this._setChildren(location, this.resortChildren(node, recursive)); } private resortChildren(node: ITreeNode, recursive: boolean, first = true): Iterable> { let childrenNodes = [...node.children] as ITreeNode[]; if (recursive || first) { childrenNodes = mergeSort(childrenNodes, this.sorter!.compare.bind(this.sorter)) as ITreeNode[]; } return Iterable.map, ITreeElement>(childrenNodes, node => ({ element: node.element as T, collapsible: node.collapsible, collapsed: node.collapsed, children: this.resortChildren(node, recursive, false) })); } getFirstElementChild(ref: T | null = null): T | null | undefined { const location = this.getElementLocation(ref); return this.model.getFirstElementChild(location); } getLastElementAncestor(ref: T | null = null): T | null | undefined { const location = this.getElementLocation(ref); return this.model.getLastElementAncestor(location); } has(element: T | null): boolean { return this.nodes.has(element); } getListIndex(element: T | null): number { const location = this.getElementLocation(element); return this.model.getListIndex(location); } getListRenderCount(element: T | null): number { const location = this.getElementLocation(element); return this.model.getListRenderCount(location); } isCollapsible(element: T | null): boolean { const location = this.getElementLocation(element); return this.model.isCollapsible(location); } setCollapsible(element: T | null, collapsible?: boolean): boolean { const location = this.getElementLocation(element); return this.model.setCollapsible(location, collapsible); } isCollapsed(element: T | null): boolean { const location = this.getElementLocation(element); return this.model.isCollapsed(location); } setCollapsed(element: T | null, collapsed?: boolean, recursive?: boolean): boolean { const location = this.getElementLocation(element); return this.model.setCollapsed(location, collapsed, recursive); } expandTo(element: T | null): void { const location = this.getElementLocation(element); this.model.expandTo(location); } refilter(): void { this.model.refilter(); } getNode(element: T | null = null): ITreeNode { if (element === null) { return this.model.getNode(this.model.rootRef); } const node = this.nodes.get(element); if (!node) { throw new TreeError(this.user, `Tree element not found: ${element}`); } return node; } getNodeLocation(node: ITreeNode): T | null { return node.element; } getParentNodeLocation(element: T | null): T | null { if (element === null) { throw new TreeError(this.user, `Invalid getParentNodeLocation call`); } const node = this.nodes.get(element); if (!node) { throw new TreeError(this.user, `Tree element not found: ${element}`); } const location = this.model.getNodeLocation(node); const parentLocation = this.model.getParentNodeLocation(location); const parent = this.model.getNode(parentLocation); return parent.element; } private getElementLocation(element: T | null): number[] { if (element === null) { return []; } const node = this.nodes.get(element); if (!node) { throw new TreeError(this.user, `Tree element not found: ${element}`); } return this.model.getNodeLocation(node); } }