/*--------------------------------------------------------------------------------------------- * 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 { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; export interface ICompressedTreeElement extends ITreeElement { readonly children?: Iterator> | ICompressedTreeElement[]; readonly incompressible?: boolean; } export interface ICompressedTreeNode { readonly elements: T[]; readonly incompressible: boolean; } export function compress(element: ICompressedTreeElement): ITreeElement> { const elements = [element.element]; const incompressible = element.incompressible || false; let childrenIterator: Iterator>; let children: ITreeElement[]; while (true) { childrenIterator = Iterator.from(element.children); children = Iterator.collect(childrenIterator, 2); if (children.length !== 1) { break; } element = children[0]; if (element.incompressible) { break; } elements.push(element.element); } return { element: { elements, incompressible }, children: Iterator.map(Iterator.concat(Iterator.fromArray(children), childrenIterator), compress) }; } export function _decompress(element: ITreeElement>, index = 0): ICompressedTreeElement { let children: Iterator>; if (index < element.element.elements.length - 1) { children = Iterator.single(_decompress(element, index + 1)); } else { children = Iterator.map(Iterator.from(element.children), el => _decompress(el, 0)); } if (index === 0 && element.element.incompressible) { return { element: element.element.elements[index], children, incompressible: true }; } return { element: element.element.elements[index], children }; } export function decompress(element: ITreeElement>): ICompressedTreeElement { return _decompress(element, 0); } export function splice(treeElement: ICompressedTreeElement, element: T, children: Iterator>): ICompressedTreeElement { if (treeElement.element === element) { return { element, children }; } return { ...treeElement, children: Iterator.map(Iterator.from(treeElement.children), e => splice(e, element, children)) }; } export interface ICompressedTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } export class CompressedTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { readonly rootRef = null; get onDidSplice(): Event | null, TFilterData>> { return this.model.onDidSplice; } get onDidChangeCollapseState(): Event, TFilterData>> { return this.model.onDidChangeCollapseState; } get onDidChangeRenderNodeCount(): Event, TFilterData>> { return this.model.onDidChangeRenderNodeCount; } private model: ObjectTreeModel, TFilterData>; private nodes = new Map>(); get size(): number { return this.nodes.size; } constructor( private user: string, list: ISpliceable, TFilterData>>, options: ICompressedTreeModelOptions = {} ) { this.model = new ObjectTreeModel(user, list, options); } setChildren( element: T | null, children: ISequence> | undefined, onDidCreateNode?: (node: ITreeNode, TFilterData>) => void, onDidDeleteNode?: (node: ITreeNode, TFilterData>) => void ): Iterator> { const insertedElements = new Set(); const _onDidCreateNode = (node: ITreeNode, TFilterData>) => { for (const element of node.element.elements) { insertedElements.add(element); this.nodes.set(element, node.element); } // 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, TFilterData>) => { for (const element of node.element.elements) { if (!insertedElements.has(element)) { this.nodes.delete(element); } } // if (this.identityProvider) { // const id = this.identityProvider.getId(node.element).toString(); // if (!insertedElementIds.has(id)) { // this.nodesByIdentity.delete(id); // } // } if (onDidDeleteNode) { onDidDeleteNode(node); } }; if (element === null) { const compressedChildren = Iterator.map(Iterator.from(children), compress); const result = this.model.setChildren(null, compressedChildren, _onDidCreateNode, _onDidDeleteNode); return Iterator.map(result, decompress); } const compressedNode = this.nodes.get(element); const node = this.model.getNode(compressedNode) as ITreeNode, TFilterData>; const parent = node.parent!; const decompressedElement = decompress(node); const splicedElement = splice(decompressedElement, element, Iterator.from(children)); const recompressedElement = compress(splicedElement); const parentChildren = parent.children .map(child => child === node ? recompressedElement : child); this.model.setChildren(parent.element, parentChildren, _onDidCreateNode, _onDidDeleteNode); // TODO return Iterator.empty(); } getListIndex(location: T | null): number { const node = this.getCompressedNode(location); return this.model.getListIndex(node); } getListRenderCount(location: T | null): number { const node = this.getCompressedNode(location); return this.model.getListRenderCount(node); } getNode(location?: T | null | undefined): ITreeNode | null, TFilterData> { if (typeof location === 'undefined') { return this.model.getNode(); } const node = this.getCompressedNode(location); return this.model.getNode(node); } // TODO: review this getNodeLocation(node: ITreeNode, TFilterData>): T | null { const compressedNode = this.model.getNodeLocation(node); if (compressedNode === null) { return null; } return compressedNode.elements[compressedNode.elements.length - 1]; } // TODO: review this getParentNodeLocation(location: T | null): T | null { const compressedNode = this.getCompressedNode(location); const parentNode = this.model.getParentNodeLocation(compressedNode); if (parentNode === null) { return null; } return parentNode.elements[parentNode.elements.length - 1]; } getParentElement(location: T | null): ICompressedTreeNode | null { const compressedNode = this.getCompressedNode(location); return this.model.getParentElement(compressedNode); } getFirstElementChild(location: T | null): ICompressedTreeNode | null | undefined { const compressedNode = this.getCompressedNode(location); return this.model.getFirstElementChild(compressedNode); } getLastElementAncestor(location?: T | null | undefined): ICompressedTreeNode | null | undefined { const compressedNode = typeof location === 'undefined' ? undefined : this.getCompressedNode(location); return this.model.getLastElementAncestor(compressedNode); } isCollapsible(location: T | null): boolean { const compressedNode = this.getCompressedNode(location); return this.model.isCollapsible(compressedNode); } setCollapsible(location: T | null, collapsible?: boolean): boolean { const compressedNode = this.getCompressedNode(location); return this.model.setCollapsible(compressedNode, collapsible); } isCollapsed(location: T | null): boolean { const compressedNode = this.getCompressedNode(location); return this.model.isCollapsed(compressedNode); } setCollapsed(location: T | null, collapsed?: boolean | undefined, recursive?: boolean | undefined): boolean { const compressedNode = this.getCompressedNode(location); return this.model.setCollapsed(compressedNode, collapsed, recursive); } expandTo(location: T | null): void { const compressedNode = this.getCompressedNode(location); this.model.expandTo(compressedNode); } rerender(location: T | null): void { const compressedNode = this.getCompressedNode(location); this.model.rerender(compressedNode); } refilter(): void { this.model.refilter(); } resort(location: T | null = null, recursive = true): void { const compressedNode = this.getCompressedNode(location); this.model.resort(compressedNode, recursive); } private getCompressedNode(element: T | null): ICompressedTreeNode | null { if (element === null) { return null; } const node = this.nodes.get(element); if (!node) { throw new TreeError(this.user, `Tree element not found: ${element}`); } return node; } } export type ElementMapper = (elements: T[]) => T; export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; export type NodeMapper = (node: ITreeNode | null, TFilterData>) => ITreeNode; function mapNode(elementMapper: ElementMapper, node: ITreeNode | null, TFilterData>): ITreeNode { return { ...node, element: node.element === null ? null : elementMapper(node.element.elements), children: node.children.map(child => mapNode(elementMapper, child)), parent: typeof node.parent === 'undefined' ? node.parent : mapNode(elementMapper, node.parent) }; } function createNodeMapper(elementMapper: ElementMapper): NodeMapper { return node => mapNode(elementMapper, node); } export interface ICompressedObjectTreeModelOptions extends ICompressedTreeModelOptions { readonly elementMapper?: ElementMapper; } export class CompressedObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { readonly rootRef = null; get onDidSplice(): Event> { return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({ insertedNodes: insertedNodes.map(this.mapNode), deletedNodes: deletedNodes.map(this.mapNode), })); } get onDidChangeCollapseState(): Event> { return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({ node: this.mapNode(node), deep })); } get onDidChangeRenderNodeCount(): Event> { return Event.map(this.model.onDidChangeRenderNodeCount, this.mapNode); } private mapElement: ElementMapper; private mapNode: NodeMapper; private model: CompressedTreeModel; constructor( user: string, list: ISpliceable, TFilterData>>, options: ICompressedObjectTreeModelOptions = {} ) { this.mapElement = options.elementMapper || DefaultElementMapper; this.mapNode = createNodeMapper(this.mapElement); this.model = new CompressedTreeModel(user, list, options); } setChildren( element: T | null, children: ISequence> | undefined ): Iterator> { this.model.setChildren(element, children); // TODO return Iterator.empty(); } getListIndex(location: T | null): number { return this.model.getListIndex(location); } getListRenderCount(location: T | null): number { return this.model.getListRenderCount(location); } getNode(location?: T | null | undefined): ITreeNode { return this.mapNode(this.model.getNode(location)); } getNodeLocation(node: ITreeNode): T | null { return node.element; } getParentNodeLocation(location: T | null): T | null { return this.model.getParentNodeLocation(location); } getParentElement(location: T | null): T | null { const result = this.model.getParentElement(location); if (result === null) { return result; } return this.mapElement(result.elements); } getFirstElementChild(location: T | null): T | null | undefined { const result = this.model.getFirstElementChild(location); if (result === null || typeof result === 'undefined') { return result; } return this.mapElement(result.elements); } getLastElementAncestor(location?: T | null | undefined): T | null | undefined { const result = this.model.getLastElementAncestor(location); if (result === null || typeof result === 'undefined') { return result; } return this.mapElement(result.elements); } isCollapsible(location: T | null): boolean { return this.model.isCollapsible(location); } setCollapsible(location: T | null, collapsed?: boolean): boolean { return this.model.setCollapsible(location, collapsed); } isCollapsed(location: T | null): boolean { return this.model.isCollapsed(location); } setCollapsed(location: T | null, collapsed?: boolean | undefined, recursive?: boolean | undefined): boolean { return this.model.setCollapsed(location, collapsed, recursive); } expandTo(location: T | null): void { return this.model.expandTo(location); } rerender(location: T | null): void { return this.model.rerender(location); } refilter(): void { return this.model.refilter(); } resort(element: T | null = null, recursive = true): void { return this.model.resort(element, recursive); } }