/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator, ISequence } from 'vs/base/common/iterator'; import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; import { tail2 } from 'vs/base/common/arrays'; import { ITreeFilterDataResult, TreeVisibility, ITreeFilter, ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; interface IMutableTreeNode extends ITreeNode { readonly parent: IMutableTreeNode | undefined; readonly children: IMutableTreeNode[]; collapsible: boolean; collapsed: boolean; renderNodeCount: number; visible: boolean; filterData: TFilterData | undefined; } function isFilterResult(obj: any): obj is ITreeFilterDataResult { return typeof obj === 'object' && 'visibility' in obj && 'data' in obj; } function treeNodeToElement(node: IMutableTreeNode): ITreeElement { const { element, collapsed } = node; const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement); return { element, children, collapsed }; } function getVisibleState(visibility: boolean | TreeVisibility): TreeVisibility { switch (visibility) { case true: return TreeVisibility.Visible; case false: return TreeVisibility.Hidden; default: return visibility; } } export interface IIndexTreeModelOptions { collapseByDefault?: boolean; // defaults to false filter?: ITreeFilter; } export class IndexTreeModel, TFilterData = void> implements ITreeModel { readonly rootRef = []; private root: IMutableTreeNode; private eventBufferer = new EventBufferer(); private _onDidChangeCollapseState = new Emitter>(); readonly onDidChangeCollapseState: Event> = this.eventBufferer.wrapEvent(this._onDidChangeCollapseState.event); private _onDidChangeRenderNodeCount = new Emitter>(); readonly onDidChangeRenderNodeCount: Event> = this.eventBufferer.wrapEvent(this._onDidChangeRenderNodeCount.event); private collapseByDefault: boolean; private filter?: ITreeFilter; constructor(private list: ISpliceable>, rootElement: T, options: IIndexTreeModelOptions = {}) { this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault; this.filter = options.filter; // this.onDidChangeCollapseState(node => console.log(node.collapsed, node)); this.root = { parent: undefined, element: rootElement, children: [], depth: 0, collapsible: false, collapsed: false, renderNodeCount: 0, visible: true, filterData: undefined }; } splice( location: number[], deleteCount: number, toInsert?: ISequence>, onDidCreateNode?: (node: ITreeNode) => void, onDidDeleteNode?: (node: ITreeNode) => void ): Iterator> { if (location.length === 0) { throw new Error('Invalid tree location'); } const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location); const treeListElementsToInsert: ITreeNode[] = []; const nodesToInsertIterator = Iterator.map(Iterator.from(toInsert), el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode)); const nodesToInsert: IMutableTreeNode[] = []; let renderNodeCount = 0; Iterator.forEach(nodesToInsertIterator, node => { nodesToInsert.push(node); renderNodeCount += node.renderNodeCount; }); const lastIndex = location[location.length - 1]; const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert); if (revealed) { const visibleDeleteCount = deletedNodes.reduce((r, node) => r + node.renderNodeCount, 0); this._updateAncestorsRenderNodeCount(parentNode, renderNodeCount - visibleDeleteCount); this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert); } if (deletedNodes.length > 0 && onDidDeleteNode) { const visit = (node: ITreeNode) => { onDidDeleteNode(node); node.children.forEach(visit); }; deletedNodes.forEach(visit); } return Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement); } getListIndex(location: number[]): number { return this.getTreeNodeWithListIndex(location).listIndex; } isCollapsible(location: number[]): boolean { return this.getTreeNode(location).collapsible; } isCollapsed(location: number[]): boolean { return this.getTreeNode(location).collapsed; } setCollapsed(location: number[], collapsed?: boolean, recursive?: boolean): boolean { const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location); if (typeof collapsed === 'undefined') { collapsed = !node.collapsed; } return this.eventBufferer.bufferEvents(() => { return this._setCollapsed(node, listIndex, revealed, collapsed!, recursive || false); }); } private _setCollapsed(node: IMutableTreeNode, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean { const result = this._setNodeCollapsed(node, collapsed, recursive, false); if (!revealed || !node.visible) { return result; } const previousRenderNodeCount = node.renderNodeCount; const toInsert = this.updateNodeAfterCollapseChange(node); const deleteCount = previousRenderNodeCount - (listIndex === -1 ? 0 : 1); this.list.splice(listIndex + 1, deleteCount, toInsert.slice(1)); return result; } private _setNodeCollapsed(node: IMutableTreeNode, collapsed: boolean, recursive: boolean, deep: boolean): boolean { let result = node.collapsible && node.collapsed !== collapsed; if (node.collapsible) { node.collapsed = collapsed; if (result) { this._onDidChangeCollapseState.fire({ node, deep }); } } if (recursive) { for (const child of node.children) { result = this._setNodeCollapsed(child, collapsed, true, true) || result; } } return result; } refilter(): void { const previousRenderNodeCount = this.root.renderNodeCount; const toInsert = this.updateNodeAfterFilterChange(this.root); this.list.splice(0, previousRenderNodeCount, toInsert); } private createTreeNode( treeElement: ITreeElement, parent: IMutableTreeNode, parentVisibility: TreeVisibility, revealed: boolean, treeListElements: ITreeNode[], onDidCreateNode?: (node: ITreeNode) => void ): IMutableTreeNode { const node: IMutableTreeNode = { parent, element: treeElement.element, children: [], depth: parent.depth + 1, collapsible: typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'), collapsed: typeof treeElement.collapsed === 'undefined' ? this.collapseByDefault : treeElement.collapsed, renderNodeCount: 1, visible: true, filterData: undefined }; const visibility = this._filterNode(node, parentVisibility); if (revealed) { treeListElements.push(node); } const childElements = Iterator.from(treeElement.children); const childRevealed = revealed && visibility !== TreeVisibility.Hidden && !node.collapsed; const childNodes = Iterator.map(childElements, el => this.createTreeNode(el, node, visibility, childRevealed, treeListElements, onDidCreateNode)); let hasVisibleDescendants = false; let renderNodeCount = 1; Iterator.forEach(childNodes, child => { node.children.push(child); hasVisibleDescendants = hasVisibleDescendants || child.visible; renderNodeCount += child.renderNodeCount; }); node.collapsible = node.collapsible || node.children.length > 0; node.visible = visibility === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility === TreeVisibility.Visible); if (!node.visible) { node.renderNodeCount = 0; if (revealed) { treeListElements.pop(); } } else if (!node.collapsed) { node.renderNodeCount = renderNodeCount; } if (onDidCreateNode) { onDidCreateNode(node); } return node; } private updateNodeAfterCollapseChange(node: IMutableTreeNode): ITreeNode[] { const previousRenderNodeCount = node.renderNodeCount; const result: ITreeNode[] = []; this._updateNodeAfterCollapseChange(node, result); this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount); return result; } private _updateNodeAfterCollapseChange(node: IMutableTreeNode, result: ITreeNode[]): number { if (node.visible === false) { return 0; } result.push(node); node.renderNodeCount = 1; if (!node.collapsed) { for (const child of node.children) { node.renderNodeCount += this._updateNodeAfterCollapseChange(child, result); } } this._onDidChangeRenderNodeCount.fire(node); return node.renderNodeCount; } private updateNodeAfterFilterChange(node: IMutableTreeNode): ITreeNode[] { const previousRenderNodeCount = node.renderNodeCount; const result: ITreeNode[] = []; this._updateNodeAfterFilterChange(node, node.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, result); this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount); return result; } private _updateNodeAfterFilterChange(node: IMutableTreeNode, parentVisibility: TreeVisibility, result: ITreeNode[], revealed = true): boolean { let visibility: TreeVisibility; if (node !== this.root) { visibility = this._filterNode(node, parentVisibility); if (visibility === TreeVisibility.Hidden) { node.visible = false; return false; } if (revealed) { result.push(node); } } const resultStartLength = result.length; node.renderNodeCount = node === this.root ? 0 : 1; let hasVisibleDescendants = false; if (!node.collapsed || visibility! !== TreeVisibility.Hidden) { for (const child of node.children) { hasVisibleDescendants = this._updateNodeAfterFilterChange(child, visibility!, result, revealed && !node.collapsed) || hasVisibleDescendants; } } if (node !== this.root) { node.visible = visibility! === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility! === TreeVisibility.Visible); } if (!node.visible) { node.renderNodeCount = 0; if (revealed) { result.pop(); } } else if (!node.collapsed) { node.renderNodeCount += result.length - resultStartLength; } this._onDidChangeRenderNodeCount.fire(node); return node.visible; } private _updateAncestorsRenderNodeCount(node: IMutableTreeNode | undefined, diff: number): void { if (diff === 0) { return; } while (node) { node.renderNodeCount += diff; this._onDidChangeRenderNodeCount.fire(node); node = node.parent; } } private _filterNode(node: IMutableTreeNode, parentVisibility: TreeVisibility): TreeVisibility { const result = this.filter ? this.filter.filter(node.element, parentVisibility) : TreeVisibility.Visible; if (typeof result === 'boolean') { node.filterData = undefined; return result ? TreeVisibility.Visible : TreeVisibility.Hidden; } else if (isFilterResult(result)) { node.filterData = result.data; return getVisibleState(result.visibility); } else { node.filterData = undefined; return getVisibleState(result); } } // cheap private getTreeNode(location: number[], node: IMutableTreeNode = this.root): IMutableTreeNode { if (!location || location.length === 0) { return node; } const [index, ...rest] = location; if (index < 0 || index > node.children.length) { throw new Error('Invalid tree location'); } return this.getTreeNode(rest, node.children[index]); } // expensive private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode, listIndex: number, revealed: boolean } { if (location.length === 0) { return { node: this.root, listIndex: -1, revealed: true }; } const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location); const index = location[location.length - 1]; if (index < 0 || index > parentNode.children.length) { throw new Error('Invalid tree location'); } const node = parentNode.children[index]; return { node, listIndex, revealed }; } private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; } { const [index, ...rest] = location; if (index < 0 || index > node.children.length) { throw new Error('Invalid tree location'); } // TODO@joao perf! for (let i = 0; i < index; i++) { listIndex += node.children[i].renderNodeCount; } revealed = revealed && !node.collapsed; if (rest.length === 0) { return { parentNode: node, listIndex, revealed }; } return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed); } getNode(location: number[] = []): ITreeNode { return this.getTreeNode(location); } // TODO@joao perf! getNodeLocation(node: ITreeNode): number[] { const location: number[] = []; while (node.parent) { location.push(node.parent.children.indexOf(node)); node = node.parent; } return location.reverse(); } getParentNodeLocation(location: number[]): number[] { if (location.length <= 1) { return []; } return tail2(location)[0]; } getParentElement(location: number[]): T { const parentLocation = this.getParentNodeLocation(location); const node = this.getTreeNode(parentLocation); return node.element; } getFirstElementChild(location: number[]): T | undefined { const node = this.getTreeNode(location); if (node.children.length === 0) { return undefined; } return node.children[0].element; } getLastElementAncestor(location: number[] = []): T | undefined { const node = this.getTreeNode(location); if (node.children.length === 0) { return undefined; } return this._getLastElementAncestor(node); } private _getLastElementAncestor(node: ITreeNode): T { if (node.children.length === 0) { return node.element; } return this._getLastElementAncestor(node.children[node.children.length - 1]); } }