/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { readonly sorter?: ITreeSorter; readonly identityProvider?: IIdentityProvider; } export class ObjectTreeModel, TFilterData extends NonNullable = void> implements ITreeModel { 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(list: ISpliceable>, options: IObjectTreeModelOptions = {}) { this.model = new IndexTreeModel(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: ISequence> | undefined, onDidCreateNode?: (node: ITreeNode) => void, onDidDeleteNode?: (node: ITreeNode) => void ): Iterator> { const location = this.getElementLocation(element); return this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode); } private _setChildren( location: number[], children: ISequence> | undefined, onDidCreateNode?: (node: ITreeNode) => void, onDidDeleteNode?: (node: ITreeNode) => void ): Iterator> { const insertedElements = new Set(); const insertedElementIds = new Set(); const _onDidCreateNode = (node: ITreeNode) => { insertedElements.add(node.element); this.nodes.set(node.element, node); if (this.identityProvider) { const id = this.identityProvider.getId(node.element).toString(); insertedElementIds.add(id); this.nodesByIdentity.set(id, node); } if (onDidCreateNode) { onDidCreateNode(node); } }; const _onDidDeleteNode = (node: ITreeNode) => { if (!insertedElements.has(node.element)) { this.nodes.delete(node.element); } if (this.identityProvider) { const id = this.identityProvider.getId(node.element).toString(); if (!insertedElementIds.has(id)) { this.nodesByIdentity.delete(id); } } if (onDidDeleteNode) { onDidDeleteNode(node); } }; const result = this.model.splice( [...location, 0], Number.MAX_VALUE, children, _onDidCreateNode, _onDidDeleteNode ); return result as Iterator>; } private preserveCollapseState(elements: ISequence> | undefined): ISequence> { let iterator = elements ? getSequenceIterator(elements) : Iterator.empty>(); if (this.sorter) { iterator = Iterator.fromArray(Iterator.collect(iterator).sort(this.sorter.compare.bind(this.sorter))); } return Iterator.map(iterator, 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); } 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): ISequence> { let childrenNodes = Iterator.fromArray(node.children as ITreeNode[]); if (recursive || first) { childrenNodes = Iterator.fromArray(Iterator.collect(childrenNodes).sort(this.sorter!.compare.bind(this.sorter))); } return Iterator.map, ITreeElement>(childrenNodes, node => ({ element: node.element as T, collapsible: node.collapsible, collapsed: node.collapsed, children: this.resortChildren(node, recursive, false) })); } getParentElement(ref: T | null = null): T | null { const location = this.getElementLocation(ref); return this.model.getParentElement(location); } 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); } 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); } 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 Error(`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 Error(`Invalid getParentNodeLocation call`); } const node = this.nodes.get(element); if (!node) { throw new Error(`Tree element not found: ${element}`); } return node.parent!.element; } private getElementLocation(element: T | null): number[] { if (element === null) { return []; } const node = this.nodes.get(element); if (!node) { throw new Error(`Tree element not found: ${element}`); } return this.model.getNodeLocation(node); } }