/*--------------------------------------------------------------------------------------------- * 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 } from 'vs/base/common/iterator'; import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; // Exported only for test reasons, do not use directly export interface ICompressedTreeElement extends ITreeElement { readonly children?: Iterator> | ICompressedTreeElement[]; readonly incompressible?: boolean; } // Exported only for test reasons, do not use directly export interface ICompressedTreeNode { readonly elements: T[]; readonly incompressible: boolean; } function noCompress(element: ICompressedTreeElement): ITreeElement> { const elements = [element.element]; const incompressible = element.incompressible || false; return { element: { elements, incompressible }, children: Iterator.map(Iterator.from(element.children), noCompress), collapsible: element.collapsible, collapsed: element.collapsed }; } // Exported only for test reasons, do not use directly 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), collapsible: element.collapsible, collapsed: element.collapsed }; } 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, collapsible: element.collapsible, collapsed: element.collapsed }; } return { element: element.element.elements[index], children, collapsible: element.collapsible, collapsed: element.collapsed }; } // Exported only for test reasons, do not use directly export function decompress(element: ITreeElement>): ICompressedTreeElement { return _decompress(element, 0); } 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)) }; } interface ICompressedObjectTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } // Exported only for test reasons, do not use directly export class CompressedObjectTreeModel, 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>(); private enabled: boolean = true; get size(): number { return this.nodes.size; } constructor( private user: string, list: ISpliceable, TFilterData>>, options: ICompressedObjectTreeModelOptions = {} ) { this.model = new ObjectTreeModel(user, list, options); } setChildren( element: T | null, children: ISequence> | undefined ): void { if (element === null) { const compressedChildren = Iterator.map(Iterator.from(children), this.enabled ? compress : noCompress); this._setChildren(null, compressedChildren); return; } const compressedNode = this.nodes.get(element); if (!compressedNode) { throw new Error('Unknown compressed tree node'); } const node = this.model.getNode(compressedNode) as ITreeNode, TFilterData>; const compressedParentNode = this.model.getParentNodeLocation(compressedNode); const parent = this.model.getNode(compressedParentNode) as ITreeNode, TFilterData>; const decompressedElement = decompress(node); const splicedElement = splice(decompressedElement, element, Iterator.from(children)); const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement); const parentChildren = parent.children .map(child => child === node ? recompressedElement : child); this._setChildren(parent.element, parentChildren); } isCompressionEnabled(): boolean { return this.enabled; } setCompressionEnabled(enabled: boolean): void { if (enabled === this.enabled) { return; } this.enabled = enabled; const root = this.model.getNode(); const rootChildren = Iterator.from((root.children as unknown) as ITreeNode>[]); // {{SQL CARBON EDIT}} strict-null-checks const decompressedRootChildren = Iterator.map(rootChildren, decompress); const recompressedRootChildren = Iterator.map(decompressedRootChildren, enabled ? compress : noCompress); this._setChildren(null, recompressedRootChildren); } private _setChildren( node: ICompressedTreeNode | null, children: ISequence>> | undefined ): void { 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); } }; const _onDidDeleteNode = (node: ITreeNode, TFilterData>) => { for (const element of node.element.elements) { if (!insertedElements.has(element)) { this.nodes.delete(element); } } }; this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode); } 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]; } 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); } 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; } } // Compressible Object Tree export type ElementMapper = (elements: T[]) => T; export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; export type CompressedNodeUnwrapper = (node: ICompressedTreeNode) => T; type CompressedNodeWeakMapper = WeakMapper | null, TFilterData>, ITreeNode>; class CompressedTreeNodeWrapper implements ITreeNode { get element(): T | null { return this.node.element === null ? null : this.unwrapper(this.node.element); } get children(): ITreeNode[] { return this.node.children.map(node => new CompressedTreeNodeWrapper(this.unwrapper, node)); } get depth(): number { return this.node.depth; } get visibleChildrenCount(): number { return this.node.visibleChildrenCount; } get visibleChildIndex(): number { return this.node.visibleChildIndex; } get collapsible(): boolean { return this.node.collapsible; } get collapsed(): boolean { return this.node.collapsed; } get visible(): boolean { return this.node.visible; } get filterData(): TFilterData | undefined { return this.node.filterData; } constructor( private unwrapper: CompressedNodeUnwrapper, private node: ITreeNode | null, TFilterData> ) { } } function mapList(nodeMapper: CompressedNodeWeakMapper, list: ISpliceable>): ISpliceable, TFilterData>> { return { splice(start: number, deleteCount: number, toInsert: ITreeNode, TFilterData>[]): void { list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node)) as ITreeNode[]); } }; } function mapOptions(compressedNodeUnwrapper: CompressedNodeUnwrapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { return { ...options, sorter: options.sorter && { compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { return options.sorter!.compare(node.elements[0], otherNode.elements[0]); } }, identityProvider: options.identityProvider && { getId(node: ICompressedTreeNode): { toString(): string; } { return options.identityProvider!.getId(compressedNodeUnwrapper(node)); } }, filter: options.filter && { filter(node: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility); } } }; } export interface ICompressibleObjectTreeModelOptions extends IObjectTreeModelOptions { readonly elementMapper?: ElementMapper; } export class CompressibleObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { readonly rootRef = null; get onDidSplice(): Event> { return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({ insertedNodes: insertedNodes.map(node => this.nodeMapper.map(node)), deletedNodes: deletedNodes.map(node => this.nodeMapper.map(node)), })); } get onDidChangeCollapseState(): Event> { return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({ node: this.nodeMapper.map(node), deep })); } get onDidChangeRenderNodeCount(): Event> { return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node)); } private elementMapper: ElementMapper; private nodeMapper: CompressedNodeWeakMapper; private model: CompressedObjectTreeModel; constructor( user: string, list: ISpliceable>, options: ICompressibleObjectTreeModelOptions = {} ) { this.elementMapper = options.elementMapper || DefaultElementMapper; const compressedNodeUnwrapper: CompressedNodeUnwrapper = node => this.elementMapper(node.elements); this.nodeMapper = new WeakMapper(node => new CompressedTreeNodeWrapper(compressedNodeUnwrapper, node)); this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options)); } setChildren(element: T | null, children?: ISequence>): void { this.model.setChildren(element, children); } isCompressionEnabled(): boolean { return this.model.isCompressionEnabled(); } setCompressionEnabled(enabled: boolean): void { this.model.setCompressionEnabled(enabled); } 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.nodeMapper.map(this.model.getNode(location)); } getNodeLocation(node: ITreeNode): T | null { return node.element; } getParentNodeLocation(location: T | null): T | null { return this.model.getParentNodeLocation(location); } getFirstElementChild(location: T | null): T | null | undefined { const result = this.model.getFirstElementChild(location); if (result === null || typeof result === 'undefined') { return null; // {{SQL CARBON EDIT}} strict-null-check } return this.elementMapper(result.elements); } getLastElementAncestor(location?: T | null | undefined): T | null | undefined { const result = this.model.getLastElementAncestor(location); if (result === null || typeof result === 'undefined') { return null; // {{SQL CARBON EDIT}} strict-null-check } return this.elementMapper(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); } getCompressedTreeNode(element: T): ITreeNode, TFilterData> { return this.model.getNode(element) as ITreeNode, TFilterData>; } }