From c0753cb9a9d766cc9d8e59b21423dabf1fd8bf57 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 10:43:59 +0200 Subject: [PATCH 01/17] introduce CompressibleObjectTreeModel --- .../browser/ui/tree/compressedObjectTree.ts | 56 ------------ .../ui/tree/compressedObjectTreeModel.ts | 91 +++++++++++++------ .../ui/tree/compressedObjectTreeModel.test.ts | 10 +- 3 files changed, 66 insertions(+), 91 deletions(-) delete mode 100644 src/vs/base/browser/ui/tree/compressedObjectTree.ts diff --git a/src/vs/base/browser/ui/tree/compressedObjectTree.ts b/src/vs/base/browser/ui/tree/compressedObjectTree.ts deleted file mode 100644 index 5d1b80462c512..0000000000000 --- a/src/vs/base/browser/ui/tree/compressedObjectTree.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Iterator, ISequence } from 'vs/base/common/iterator'; -import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; -import { ISpliceable } from 'vs/base/common/sequence'; -import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { Event } from 'vs/base/common/event'; -import { CompressedTreeModel, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; - -export interface IObjectTreeOptions extends IAbstractTreeOptions { - sorter?: ITreeSorter; -} - -export class CompressedObjectTree, TFilterData = void> extends AbstractTree | null, TFilterData, T | null> { - - protected model: CompressedTreeModel; - - get onDidChangeCollapseState(): Event | null, TFilterData>> { return this.model.onDidChangeCollapseState; } - - constructor( - container: HTMLElement, - delegate: IListVirtualDelegate>, - renderers: ITreeRenderer, TFilterData, any>[], - options: IObjectTreeOptions, TFilterData> = {} - ) { - super(container, delegate, renderers, options); - } - - setChildren( - element: T | null, - children?: ISequence> - ): Iterator> { - return this.model.setChildren(element, children); - } - - rerender(element?: T): void { - if (element === undefined) { - this.view.rerender(); - return; - } - - this.model.rerender(element); - } - - resort(element: T, recursive = true): void { - this.model.resort(element, recursive); - } - - protected createModel(view: ISpliceable, TFilterData>>, options: IObjectTreeOptions, TFilterData>): ITreeModel | null, TFilterData, T | null> { - return new CompressedTreeModel(view, options); - } -} diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 339dfdbbe9e94..1932c5a70f123 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -6,7 +6,7 @@ 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 } from 'vs/base/browser/ui/tree/tree'; +import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; export interface ICompressedTreeElement extends ITreeElement { @@ -80,9 +80,10 @@ export function splice(treeElement: ICompressedTreeElement, element: T, ch }; } -export interface ICompressedTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } +export interface ICompressedObjectTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } -export class CompressedTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { +// This model is exported only for test reasons +export class CompressedObjectTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { readonly rootRef = null; @@ -95,7 +96,7 @@ export class CompressedTreeModel, TFilterData extends get size(): number { return this.nodes.size; } - constructor(list: ISpliceable, TFilterData>>, options: ICompressedTreeModelOptions = {}) { + constructor(list: ISpliceable, TFilterData>>, options: ICompressedObjectTreeModelOptions = {}) { this.model = new ObjectTreeModel(list, options); } @@ -271,61 +272,91 @@ export class CompressedTreeModel, TFilterData extends } } -export type ElementMapper = (elements: T[]) => T; -export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; +// Compressible Object Tree +export type ElementMapper = (elements: T[]) => T; +export type CompressedNodeMapper = (node: ICompressedTreeNode) => T; export type NodeMapper = (node: ITreeNode | null, TFilterData>) => ITreeNode; -function mapNode(elementMapper: ElementMapper, node: ITreeNode | null, TFilterData>): ITreeNode { +export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; + +function mapNode(compressedNodeMapper: CompressedNodeMapper, 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) + element: node.element === null ? null : compressedNodeMapper(node.element), + children: node.children.map(child => mapNode(compressedNodeMapper, child)), + parent: typeof node.parent === 'undefined' ? node.parent : mapNode(compressedNodeMapper, node.parent) }; } -function createNodeMapper(elementMapper: ElementMapper): NodeMapper { - return node => mapNode(elementMapper, node); +function mapList(nodeMapper: NodeMapper, list: ISpliceable>): ISpliceable, TFilterData>> { + return { + splice(start: number, deleteCount: number, toInsert: ITreeNode, TFilterData>[]): void { + list.splice(start, deleteCount, toInsert.map(nodeMapper) as ITreeNode[]); + } + }; } -export interface ICompressedObjectTreeModelOptions extends ICompressedTreeModelOptions { +function mapOptions(compressedNodeMapper: CompressedNodeMapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { + return { + ...options, + sorter: options.sorter && { + compare(element: ICompressedTreeNode, otherElement: ICompressedTreeNode): number { + return options.sorter!.compare(compressedNodeMapper(element), compressedNodeMapper(otherElement)); + } + }, + identityProvider: options.identityProvider && { + getId(element: ICompressedTreeNode): { toString(): string; } { + return options.identityProvider!.getId(compressedNodeMapper(element)); + } + }, + filter: options.filter && { + filter(element: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { + return options.filter!.filter(compressedNodeMapper(element), parentVisibility); + } + } + }; +} + +export interface ICompressibleObjectTreeModelOptions extends IObjectTreeModelOptions { readonly elementMapper?: ElementMapper; } -export class CompressedObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { +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(this.mapNode), - deletedNodes: deletedNodes.map(this.mapNode), + insertedNodes: insertedNodes.map(this.nodeMapper), + deletedNodes: deletedNodes.map(this.nodeMapper), })); } get onDidChangeCollapseState(): Event> { return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({ - node: this.mapNode(node), + node: this.nodeMapper(node), deep })); } get onDidChangeRenderNodeCount(): Event> { - return Event.map(this.model.onDidChangeRenderNodeCount, this.mapNode); + return Event.map(this.model.onDidChangeRenderNodeCount, this.nodeMapper); } - private mapElement: ElementMapper; - private mapNode: NodeMapper; - private model: CompressedTreeModel; + private elementMapper: ElementMapper; + private nodeMapper: NodeMapper; + private model: CompressedObjectTreeModel; constructor( - list: ISpliceable, TFilterData>>, - options: ICompressedObjectTreeModelOptions = {} + list: ISpliceable>, + options: ICompressibleObjectTreeModelOptions = {} ) { - this.mapElement = options.elementMapper || DefaultElementMapper; - this.mapNode = createNodeMapper(this.mapElement); - this.model = new CompressedTreeModel(list, options); + this.elementMapper = options.elementMapper || DefaultElementMapper; + const compressedNodeMapper: CompressedNodeMapper = node => this.elementMapper(node.elements); + this.nodeMapper = node => mapNode(compressedNodeMapper, node); + + this.model = new CompressedObjectTreeModel(mapList(this.nodeMapper, list), mapOptions(compressedNodeMapper, options)); } setChildren( @@ -347,7 +378,7 @@ export class CompressedObjectTreeModel, TFilterData e } getNode(location?: T | null | undefined): ITreeNode { - return this.mapNode(this.model.getNode(location)); + return this.nodeMapper(this.model.getNode(location)); } getNodeLocation(node: ITreeNode): T | null { @@ -365,7 +396,7 @@ export class CompressedObjectTreeModel, TFilterData e return result; } - return this.mapElement(result.elements); + return this.elementMapper(result.elements); } getFirstElementChild(location: T | null): T | null | undefined { @@ -375,7 +406,7 @@ export class CompressedObjectTreeModel, TFilterData e return result; } - return this.mapElement(result.elements); + return this.elementMapper(result.elements); } getLastElementAncestor(location?: T | null | undefined): T | null | undefined { @@ -385,7 +416,7 @@ export class CompressedObjectTreeModel, TFilterData e return result; } - return this.mapElement(result.elements); + return this.elementMapper(result.elements); } isCollapsible(location: T | null): boolean { diff --git a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts index 4d03b49a0a806..e952c56fdf9ae 100644 --- a/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/compressedObjectTreeModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { compress, ICompressedTreeElement, ICompressedTreeNode, decompress, CompressedTreeModel } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { compress, ICompressedTreeElement, ICompressedTreeNode, decompress, CompressedObjectTreeModel } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { Iterator } from 'vs/base/common/iterator'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; @@ -305,7 +305,7 @@ suite('CompressedObjectTree', function () { test('ctor', () => { const list: ITreeNode>[] = []; - const model = new CompressedTreeModel(toSpliceable(list)); + const model = new CompressedObjectTreeModel(toSpliceable(list)); assert(model); assert.equal(list.length, 0); assert.equal(model.size, 0); @@ -313,7 +313,7 @@ suite('CompressedObjectTree', function () { test('flat', () => { const list: ITreeNode>[] = []; - const model = new CompressedTreeModel(toSpliceable(list)); + const model = new CompressedObjectTreeModel(toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { element: 0 }, @@ -340,7 +340,7 @@ suite('CompressedObjectTree', function () { test('nested', () => { const list: ITreeNode>[] = []; - const model = new CompressedTreeModel(toSpliceable(list)); + const model = new CompressedObjectTreeModel(toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { @@ -376,7 +376,7 @@ suite('CompressedObjectTree', function () { test('compressed', () => { const list: ITreeNode>[] = []; - const model = new CompressedTreeModel(toSpliceable(list)); + const model = new CompressedObjectTreeModel(toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { From 6e091c40f944ce663e5305443aed4e8f0200156e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 14:18:50 +0200 Subject: [PATCH 02/17] CompressibleObjectTree --- .../ui/tree/compressedObjectTreeModel.ts | 18 +++- src/vs/base/browser/ui/tree/objectTree.ts | 95 +++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 1932c5a70f123..7244a8a10d12c 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -9,16 +9,19 @@ import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeFilterResult, TreeVisibility } 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; } +// Exported only for test reasons, do not use directly export function compress(element: ICompressedTreeElement): ITreeElement> { const elements = [element.element]; const incompressible = element.incompressible || false; @@ -49,7 +52,7 @@ export function compress(element: ICompressedTreeElement): ITreeElement(element: ITreeElement>, index = 0): ICompressedTreeElement { +function _decompress(element: ITreeElement>, index = 0): ICompressedTreeElement { let children: Iterator>; if (index < element.element.elements.length - 1) { @@ -65,11 +68,12 @@ export function _decompress(element: ITreeElement>, in return { element: element.element.elements[index], children }; } +// Exported only for test reasons, do not use directly export function decompress(element: ITreeElement>): ICompressedTreeElement { return _decompress(element, 0); } -export function splice(treeElement: ICompressedTreeElement, element: T, children: Iterator>): ICompressedTreeElement { +function splice(treeElement: ICompressedTreeElement, element: T, children: Iterator>): ICompressedTreeElement { if (treeElement.element === element) { return { element, children }; } @@ -80,9 +84,9 @@ export function splice(treeElement: ICompressedTreeElement, element: T, ch }; } -export interface ICompressedObjectTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } +interface ICompressedObjectTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } -// This model is exported only for test reasons +// 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; @@ -257,7 +261,7 @@ export class CompressedObjectTreeModel, TFilterData e this.model.resort(compressedNode, recursive); } - private getCompressedNode(element: T | null): ICompressedTreeNode | null { + getCompressedNode(element: T | null): ICompressedTreeNode | null { if (element === null) { return null; } @@ -446,4 +450,8 @@ export class CompressibleObjectTreeModel, TFilterData resort(element: T | null = null, recursive = true): void { return this.model.resort(element, recursive); } + + getCompressedNode(element: T | null): ICompressedTreeNode | null { + return this.model.getCompressedNode(element); + } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 5187346295969..1b2c44d8cb822 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -10,6 +10,7 @@ import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IColla import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; +import { CompressibleObjectTreeModel, ElementMapper } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { sorter?: ITreeSorter; @@ -54,3 +55,97 @@ export class ObjectTree, TFilterData = void> extends return new ObjectTreeModel(view, options); } } + +interface ICompressedElementsCollectionProvider { + getCompressedElements(element: T): T[]; +} + +export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { + readonly elementMapper?: ElementMapper; +} + +export interface ICompressibleTreeRenderer extends ITreeRenderer { + renderCompressedElements(elements: T[], index: number, templateData: TTemplateData, height: number | undefined): void; + disposeCompressedElements?(elements: T[], index: number, templateData: TTemplateData, height: number | undefined): void; +} + +interface CompressibleTemplateData { + compressedElements: T[] | undefined; + readonly data: TTemplateData; +} + +class CompressibleRenderer implements ITreeRenderer> { + + readonly templateId: string; + readonly onDidChangeTwistieState: Event | undefined; + + constructor(private renderer: ICompressibleTreeRenderer, private compressedElementsCollectionProvider: ICompressedElementsCollectionProvider) { + this.templateId = renderer.templateId; + + if (renderer.onDidChangeTwistieState) { + this.onDidChangeTwistieState = renderer.onDidChangeTwistieState; + } + } + + renderTemplate(container: HTMLElement): CompressibleTemplateData { + const data = this.renderer.renderTemplate(container); + return { compressedElements: undefined, data }; + } + + renderElement(element: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + const elements = this.compressedElementsCollectionProvider.getCompressedElements(element.element); + + if (elements.length === 1) { + templateData.compressedElements = undefined; + this.renderer.renderElement(element, index, templateData.data, height); + } else { + templateData.compressedElements = elements; + this.renderer.renderCompressedElements(elements, index, templateData.data, height); + } + } + + disposeElement(element: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + if (templateData.compressedElements) { + if (this.renderer.disposeCompressedElements) { + this.renderer.disposeCompressedElements(templateData.compressedElements, index, templateData.data, height); + } + } else { + if (this.renderer.disposeElement) { + this.renderer.disposeElement(element, index, templateData.data, height); + } + } + } + + disposeTemplate(templateData: CompressibleTemplateData): void { + this.renderer.disposeTemplate(templateData.data); + } + + renderTwistie?(element: T, twistieElement: HTMLElement): void { + if (this.renderer.renderTwistie) { + this.renderer.renderTwistie(element, twistieElement); + } + } +} + +export class CompressibleObjectTree, TFilterData = void> extends ObjectTree { + + protected model: CompressibleObjectTreeModel; + + constructor( + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + options: IObjectTreeOptions = {} + ) { + const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r, this)); + super(container, delegate, compressibleRenderers, options); + } + + protected createModel(view: ISpliceable>, options: ICompressibleObjectTreeOptions): ITreeModel { + return new CompressibleObjectTreeModel(view, options); + } + + getCompressedElements(element: T): T[] { + return this.model.getCompressedNode(element)!.elements; + } +} From 0a3fc51dcb204ade81c361326f1f131c63d4bd1c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 14:41:59 +0200 Subject: [PATCH 03/17] relax splice/setChildren return value --- .../ui/tree/compressedObjectTreeModel.ts | 17 ++++------------- src/vs/base/browser/ui/tree/indexTree.ts | 4 ++-- src/vs/base/browser/ui/tree/indexTreeModel.ts | 11 +---------- src/vs/base/browser/ui/tree/objectTree.ts | 9 +++------ src/vs/base/browser/ui/tree/objectTreeModel.ts | 12 +++++------- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 7244a8a10d12c..18ea6519ff050 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -109,7 +109,7 @@ export class CompressedObjectTreeModel, TFilterData e children: ISequence> | undefined, onDidCreateNode?: (node: ITreeNode, TFilterData>) => void, onDidDeleteNode?: (node: ITreeNode, TFilterData>) => void - ): Iterator> { + ): void { const insertedElements = new Set(); const _onDidCreateNode = (node: ITreeNode, TFilterData>) => { for (const element of node.element.elements) { @@ -149,8 +149,8 @@ export class CompressedObjectTreeModel, TFilterData e 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); + this.model.setChildren(null, compressedChildren, _onDidCreateNode, _onDidDeleteNode); + return; } const compressedNode = this.nodes.get(element); @@ -165,9 +165,6 @@ export class CompressedObjectTreeModel, TFilterData e .map(child => child === node ? recompressedElement : child); this.model.setChildren(parent.element, parentChildren, _onDidCreateNode, _onDidDeleteNode); - - // TODO - return Iterator.empty(); } getListIndex(location: T | null): number { @@ -363,14 +360,8 @@ export class CompressibleObjectTreeModel, TFilterData this.model = new CompressedObjectTreeModel(mapList(this.nodeMapper, list), mapOptions(compressedNodeMapper, options)); } - setChildren( - element: T | null, - children: ISequence> | undefined - ): Iterator> { + setChildren(element: T | null, children?: ISequence>): void { this.model.setChildren(element, children); - - // TODO - return Iterator.empty(); } getListIndex(location: T | null): number { diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts index 4378ffd3e1a43..2af61d77b50c0 100644 --- a/src/vs/base/browser/ui/tree/indexTree.ts +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -27,8 +27,8 @@ export class IndexTree extends AbstractTree> = Iterator.empty()): Iterator> { - return this.model.splice(location, deleteCount, toInsert); + splice(location: number[], deleteCount: number, toInsert: ISequence> = Iterator.empty()): void { + this.model.splice(location, deleteCount, toInsert); } rerender(location?: number[]): void { diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index a568881e7c2ff..76e32717fc721 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -33,13 +33,6 @@ export function getVisibleState(visibility: boolean | TreeVisibility): TreeVisib } } -function treeNodeToElement(node: IMutableTreeNode): ITreeElement { - const { element, collapsed } = node; - const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement); - - return { element, children, collapsed }; -} - export interface IIndexTreeModelOptions { readonly collapseByDefault?: boolean; // defaults to false readonly filter?: ITreeFilter; @@ -92,7 +85,7 @@ export class IndexTreeModel, TFilterData = voi toInsert?: ISequence>, onDidCreateNode?: (node: ITreeNode) => void, onDidDeleteNode?: (node: ITreeNode) => void - ): Iterator> { + ): void { if (location.length === 0) { throw new Error('Invalid tree location'); } @@ -170,9 +163,7 @@ export class IndexTreeModel, TFilterData = voi deletedNodes.forEach(visit); } - const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement); this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes }); - return result; } rerender(location: number[]): void { diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 1b2c44d8cb822..1669a80b17753 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { ISequence } from 'vs/base/common/iterator'; import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; import { ISpliceable } from 'vs/base/common/sequence'; import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; @@ -31,11 +31,8 @@ export class ObjectTree, TFilterData = void> extends super(container, delegate, renderers, options); } - setChildren( - element: T | null, - children?: ISequence> - ): Iterator> { - return this.model.setChildren(element, children); + setChildren(element: T | null, children?: ISequence>): void { + this.model.setChildren(element, children); } rerender(element?: T): void { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 626d1264daa82..ce5a69e94ff51 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -13,7 +13,7 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; export type ITreeNodeCallback = (node: ITreeNode) => void; export interface IObjectTreeModel, TFilterData extends NonNullable = void> extends ITreeModel { - setChildren(element: T | null, children: ISequence> | undefined): Iterator>; + setChildren(element: T | null, children: ISequence> | undefined): void; resort(element?: T | null, recursive?: boolean): void; } @@ -60,9 +60,9 @@ export class ObjectTreeModel, TFilterData extends Non children: ISequence> | undefined, onDidCreateNode?: ITreeNodeCallback, onDidDeleteNode?: ITreeNodeCallback - ): Iterator> { + ): void { const location = this.getElementLocation(element); - return this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode); + this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode); } private _setChildren( @@ -70,7 +70,7 @@ export class ObjectTreeModel, TFilterData extends Non children: ISequence> | undefined, onDidCreateNode?: ITreeNodeCallback, onDidDeleteNode?: ITreeNodeCallback - ): Iterator> { + ): void { const insertedElements = new Set(); const insertedElementIds = new Set(); @@ -106,15 +106,13 @@ export class ObjectTreeModel, TFilterData extends Non } }; - const result = this.model.splice( + this.model.splice( [...location, 0], Number.MAX_VALUE, children, _onDidCreateNode, _onDidDeleteNode ); - - return result as Iterator>; } private preserveCollapseState(elements: ISequence> | undefined): ISequence> { From f46a73325b404f8d405a9fa1cf703f1e5cfb8efb Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 15:19:00 +0200 Subject: [PATCH 04/17] CompressibleObjectTree: getting ready for testing --- .../ui/tree/compressedObjectTreeModel.ts | 4 +- src/vs/base/browser/ui/tree/objectTree.ts | 55 ++++++++++--------- .../test/browser/ui/tree/objectTree.test.ts | 43 ++++++++++++++- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 18ea6519ff050..d29597cd0cddd 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -442,7 +442,7 @@ export class CompressibleObjectTreeModel, TFilterData return this.model.resort(element, recursive); } - getCompressedNode(element: T | null): ICompressedTreeNode | null { - return this.model.getCompressedNode(element); + getCompressedTreeNode(element: T): ITreeNode, TFilterData> { + return this.model.getNode(element) as ITreeNode, TFilterData>; } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 1669a80b17753..b7cb458d7560f 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -10,7 +10,7 @@ import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IColla import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; -import { CompressibleObjectTreeModel, ElementMapper } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { sorter?: ITreeSorter; @@ -53,8 +53,8 @@ export class ObjectTree, TFilterData = void> extends } } -interface ICompressedElementsCollectionProvider { - getCompressedElements(element: T): T[]; +interface ICompressedTreeNodeProvider { + getCompressedTreeNode(element: T): ITreeNode, TFilterData>; } export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { @@ -62,21 +62,23 @@ export interface ICompressibleObjectTreeOptions extends I } export interface ICompressibleTreeRenderer extends ITreeRenderer { - renderCompressedElements(elements: T[], index: number, templateData: TTemplateData, height: number | undefined): void; - disposeCompressedElements?(elements: T[], index: number, templateData: TTemplateData, height: number | undefined): void; + renderCompressedElements(node: ITreeNode, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void; + disposeCompressedElements?(node: ITreeNode, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void; } -interface CompressibleTemplateData { - compressedElements: T[] | undefined; +interface CompressibleTemplateData { + compressedTreeNode: ITreeNode, TFilterData> | undefined; readonly data: TTemplateData; } -class CompressibleRenderer implements ITreeRenderer> { +class CompressibleRenderer implements ITreeRenderer> { readonly templateId: string; readonly onDidChangeTwistieState: Event | undefined; - constructor(private renderer: ICompressibleTreeRenderer, private compressedElementsCollectionProvider: ICompressedElementsCollectionProvider) { + compressedTreeNodeProvider: ICompressedTreeNodeProvider; + + constructor(private renderer: ICompressibleTreeRenderer) { this.templateId = renderer.templateId; if (renderer.onDidChangeTwistieState) { @@ -84,36 +86,36 @@ class CompressibleRenderer implements ITreeRender } } - renderTemplate(container: HTMLElement): CompressibleTemplateData { + renderTemplate(container: HTMLElement): CompressibleTemplateData { const data = this.renderer.renderTemplate(container); - return { compressedElements: undefined, data }; + return { compressedTreeNode: undefined, data }; } - renderElement(element: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { - const elements = this.compressedElementsCollectionProvider.getCompressedElements(element.element); + renderElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element); - if (elements.length === 1) { - templateData.compressedElements = undefined; - this.renderer.renderElement(element, index, templateData.data, height); + if (compressedTreeNode.element.elements.length === 1) { + templateData.compressedTreeNode = undefined; + this.renderer.renderElement(node, index, templateData.data, height); } else { - templateData.compressedElements = elements; - this.renderer.renderCompressedElements(elements, index, templateData.data, height); + templateData.compressedTreeNode = compressedTreeNode; + this.renderer.renderCompressedElements(compressedTreeNode, index, templateData.data, height); } } - disposeElement(element: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { - if (templateData.compressedElements) { + disposeElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + if (templateData.compressedTreeNode) { if (this.renderer.disposeCompressedElements) { - this.renderer.disposeCompressedElements(templateData.compressedElements, index, templateData.data, height); + this.renderer.disposeCompressedElements(templateData.compressedTreeNode, index, templateData.data, height); } } else { if (this.renderer.disposeElement) { - this.renderer.disposeElement(element, index, templateData.data, height); + this.renderer.disposeElement(node, index, templateData.data, height); } } } - disposeTemplate(templateData: CompressibleTemplateData): void { + disposeTemplate(templateData: CompressibleTemplateData): void { this.renderer.disposeTemplate(templateData.data); } @@ -134,15 +136,16 @@ export class CompressibleObjectTree, TFilterData = vo renderers: ICompressibleTreeRenderer[], options: IObjectTreeOptions = {} ) { - const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r, this)); + const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r)); super(container, delegate, compressibleRenderers, options); + compressibleRenderers.forEach(r => r.compressedTreeNodeProvider = this); } protected createModel(view: ISpliceable>, options: ICompressibleObjectTreeOptions): ITreeModel { return new CompressibleObjectTreeModel(view, options); } - getCompressedElements(element: T): T[] { - return this.model.getCompressedNode(element)!.elements; + getCompressedTreeNode(element: T): ITreeNode, TFilterData> { + return this.model.getCompressedTreeNode(element)!; } } diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 46e03f8884dbf..1aed1573903ed 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -6,8 +6,9 @@ import * as assert from 'assert'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, CompressibleObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; suite('ObjectTree', function () { suite('TreeNavigator', function () { @@ -224,4 +225,44 @@ suite('ObjectTree', function () { tree.setChildren(null, [{ element: 100 }, { element: 101 }, { element: 102 }, { element: 103 }]); assert.deepStrictEqual(tree.getFocus(), [101]); }); +}); + +function toArray(list: NodeList): Node[] { + const result: Node[] = []; + list.forEach(node => result.push(node)); + return result; +} + +suite('CompressibleObjectTree', function () { + + class Delegate implements IListVirtualDelegate { + getHeight() { return 20; } + getTemplateId(): string { return 'default'; } + } + + class Renderer implements ICompressibleTreeRenderer { + readonly templateId = 'default'; + renderTemplate(container: HTMLElement): HTMLElement { + return container; + } + renderElement(node: ITreeNode, _: number, templateData: HTMLElement): void { + templateData.textContent = `regular: ${node.element}`; + } + renderCompressedElements(node: ITreeNode, void>, _: number, templateData: HTMLElement): void { + templateData.textContent = `compressed: ${node.element.elements.join('/')}`; + } + disposeTemplate(): void { } + } + + test('empty', function () { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); + tree.layout(200); + + const rows = toArray(container.querySelectorAll('.monaco-tl-contents')); + assert.equal(rows.length, 0); + }); }); \ No newline at end of file From 678141f84f5829d76a4af43d5bbfdb82782cc15c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 16:21:15 +0200 Subject: [PATCH 05/17] remove ITreeNode.parent and ITreeModel.getParentElement --- src/vs/base/browser/ui/tree/abstractTree.ts | 63 ++++++++++--------- src/vs/base/browser/ui/tree/asyncDataTree.ts | 1 - .../ui/tree/compressedObjectTreeModel.ts | 26 +++----- src/vs/base/browser/ui/tree/indexTreeModel.ts | 62 +++++++++--------- .../base/browser/ui/tree/objectTreeModel.ts | 16 ++--- src/vs/base/browser/ui/tree/tree.ts | 5 +- .../browser/ui/tree/indexTreeModel.test.ts | 6 +- .../test/browser/ui/tree/objectTree.test.ts | 32 +++++++--- .../workbench/browser/actions/listCommands.ts | 9 +-- .../contrib/search/browser/searchActions.ts | 8 +-- .../search/test/browser/mockSearchTree.ts | 4 -- 11 files changed, 112 insertions(+), 120 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 7624d5b1fbe54..24a9b2837aa4b 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -98,9 +98,11 @@ class TreeNodeListDragAndDrop implements IListDragAndDrop< } if (result.bubble === TreeDragOverBubble.Up) { - const parentNode = targetNode.parent; const model = this.modelProvider(); - const parentIndex = parentNode && model.getListIndex(model.getNodeLocation(parentNode)); + const ref = model.getNodeLocation(targetNode); + const parentRef = model.getParentNodeLocation(ref); + const parentNode = model.getNode(parentRef); + const parentIndex = parentRef && model.getListIndex(parentRef); return this.onDragOver(data, parentNode, parentIndex, originalEvent, false); } @@ -155,7 +157,12 @@ function asListOptions(modelProvider: () => ITreeModel implements Collection { } } -class TreeRenderer implements IListRenderer, ITreeListTemplateData> { +class TreeRenderer implements IListRenderer, ITreeListTemplateData> { private static DefaultIndent = 8; @@ -251,6 +258,7 @@ class TreeRenderer implements IListRenderer, + private modelProvider: () => ITreeModel, onDidChangeCollapseState: Event>, private activeNodes: Collection>, options: ITreeRendererOptions = {} @@ -380,10 +388,19 @@ class TreeRenderer implements IListRenderer('.indent-guide', { style: `width: ${this.indent}px` }); if (this.activeParentNodes.has(parent)) { @@ -411,10 +428,14 @@ class TreeRenderer implements IListRenderer>(); + const model = this.modelProvider(); nodes.forEach(node => { - if (node.parent) { - set.add(node.parent); + const ref = model.getNodeLocation(node); + const parentRef = model.getParentNodeLocation(ref); + + if (parentRef) { + set.add(model.getNode(parentRef)); } }); @@ -1136,7 +1157,7 @@ class TreeNodeList extends List> export abstract class AbstractTree implements IDisposable { protected view: TreeNodeList; - private renderers: TreeRenderer[]; + private renderers: TreeRenderer[]; protected model: ITreeModel; private focus: Trait; private selection: Trait; @@ -1193,7 +1214,7 @@ export abstract class AbstractTree implements IDisposable const activeNodes = new EventCollection(onDidChangeActiveNodes.event); this.disposables.push(activeNodes); - this.renderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event, activeNodes, _options)); + this.renderers = renderers.map(r => new TreeRenderer(r, () => this.model, onDidChangeCollapseStateRelay.event, activeNodes, _options)); this.disposables.push(...this.renderers); let filter: TypeFilter | undefined; @@ -1357,7 +1378,9 @@ export abstract class AbstractTree implements IDisposable // Tree navigation getParentElement(location: TRef): T { - return this.model.getParentElement(location); + const parentRef = this.model.getParentNodeLocation(location); + const parentNode = this.model.getNode(parentRef); + return parentNode.element; } getFirstElementChild(location: TRef): T | undefined { @@ -1509,7 +1532,7 @@ export abstract class AbstractTree implements IDisposable if (!didChange) { const parentLocation = this.model.getParentNodeLocation(location); - if (parentLocation === null) { + if (!parentLocation) { return; } @@ -1611,22 +1634,6 @@ class TreeNavigator, TFilterData, TRef> implements IT return this.current(); } - parent(): T | null { - if (this.index < 0 || this.index >= this.view.length) { - return null; - } - - const node = this.view.element(this.index); - - if (!node.parent) { - this.index = -1; - return this.current(); - } - - this.index = this.model.getListIndex(this.model.getNodeLocation(node.parent)); - return this.current(); - } - first(): T | null { this.index = 0; return this.current(); diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 4421bce8de12d..9caccc3319337 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -69,7 +69,6 @@ interface IDataTreeListTemplateData { class AsyncDataTreeNodeWrapper implements ITreeNode { get element(): T { return this.node.element!.element as T; } - get parent(): ITreeNode | undefined { return this.node.parent && new AsyncDataTreeNodeWrapper(this.node.parent); } get children(): ITreeNode[] { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); } get depth(): number { return this.node.depth; } get visibleChildrenCount(): number { return this.node.visibleChildrenCount; } diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index d29597cd0cddd..e26e9310f248e 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -154,8 +154,14 @@ export class CompressedObjectTreeModel, TFilterData e } 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 parent = node.parent!; + 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)); @@ -209,11 +215,6 @@ export class CompressedObjectTreeModel, TFilterData e 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); @@ -285,8 +286,7 @@ function mapNode(compressedNodeMapper: CompressedNodeMapper, return { ...node, element: node.element === null ? null : compressedNodeMapper(node.element), - children: node.children.map(child => mapNode(compressedNodeMapper, child)), - parent: typeof node.parent === 'undefined' ? node.parent : mapNode(compressedNodeMapper, node.parent) + children: node.children.map(child => mapNode(compressedNodeMapper, child)) }; } @@ -384,16 +384,6 @@ export class CompressibleObjectTreeModel, TFilterData return this.model.getParentNodeLocation(location); } - getParentElement(location: T | null): T | null { - const result = this.model.getParentElement(location); - - if (result === null) { - return result; - } - - return this.elementMapper(result.elements); - } - getFirstElementChild(location: T | null): T | null | undefined { const result = this.model.getFirstElementChild(location); diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 76e32717fc721..f5df16496ceb0 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -9,9 +9,10 @@ import { Emitter, Event, EventBufferer } from 'vs/base/common/event'; import { ISequence, Iterator } from 'vs/base/common/iterator'; import { ISpliceable } from 'vs/base/common/sequence'; -interface IMutableTreeNode extends ITreeNode { - readonly parent: IMutableTreeNode | undefined; - readonly children: IMutableTreeNode[]; +// Exported for tests +export interface IIndexTreeNode extends ITreeNode { + readonly parent: IIndexTreeNode | undefined; + readonly children: IIndexTreeNode[]; visibleChildrenCount: number; visibleChildIndex: number; collapsible: boolean; @@ -43,7 +44,7 @@ export class IndexTreeModel, TFilterData = voi readonly rootRef = []; - private root: IMutableTreeNode; + private root: IIndexTreeNode; private eventBufferer = new EventBufferer(); private _onDidChangeCollapseState = new Emitter>(); @@ -109,7 +110,7 @@ export class IndexTreeModel, TFilterData = voi } } - const nodesToInsert: IMutableTreeNode[] = []; + const nodesToInsert: IIndexTreeNode[] = []; let insertedVisibleChildrenCount = 0; let renderNodeCount = 0; @@ -234,7 +235,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _setListNodeCollapsed(node: IMutableTreeNode, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean { + private _setListNodeCollapsed(node: IIndexTreeNode, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean { const result = this._setNodeCollapsed(node, collapsed, recursive, false); if (!revealed || !node.visible) { @@ -249,7 +250,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _setNodeCollapsed(node: IMutableTreeNode, collapsed: boolean, recursive: boolean, deep: boolean): boolean { + private _setNodeCollapsed(node: IIndexTreeNode, collapsed: boolean, recursive: boolean, deep: boolean): boolean { let result = node.collapsible && node.collapsed !== collapsed; if (node.collapsible) { @@ -292,13 +293,13 @@ export class IndexTreeModel, TFilterData = voi private createTreeNode( treeElement: ITreeElement, - parent: IMutableTreeNode, + parent: IIndexTreeNode, parentVisibility: TreeVisibility, revealed: boolean, treeListElements: ITreeNode[], onDidCreateNode?: (node: ITreeNode) => void - ): IMutableTreeNode { - const node: IMutableTreeNode = { + ): IIndexTreeNode { + const node: IIndexTreeNode = { parent, element: treeElement.element, children: [], @@ -355,7 +356,7 @@ export class IndexTreeModel, TFilterData = voi return node; } - private updateNodeAfterCollapseChange(node: IMutableTreeNode): ITreeNode[] { + private updateNodeAfterCollapseChange(node: IIndexTreeNode): ITreeNode[] { const previousRenderNodeCount = node.renderNodeCount; const result: ITreeNode[] = []; @@ -365,7 +366,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _updateNodeAfterCollapseChange(node: IMutableTreeNode, result: ITreeNode[]): number { + private _updateNodeAfterCollapseChange(node: IIndexTreeNode, result: ITreeNode[]): number { if (node.visible === false) { return 0; } @@ -383,7 +384,7 @@ export class IndexTreeModel, TFilterData = voi return node.renderNodeCount; } - private updateNodeAfterFilterChange(node: IMutableTreeNode): ITreeNode[] { + private updateNodeAfterFilterChange(node: IIndexTreeNode): ITreeNode[] { const previousRenderNodeCount = node.renderNodeCount; const result: ITreeNode[] = []; @@ -393,7 +394,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _updateNodeAfterFilterChange(node: IMutableTreeNode, parentVisibility: TreeVisibility, result: ITreeNode[], revealed = true): boolean { + private _updateNodeAfterFilterChange(node: IIndexTreeNode, parentVisibility: TreeVisibility, result: ITreeNode[], revealed = true): boolean { let visibility: TreeVisibility; if (node !== this.root) { @@ -447,7 +448,7 @@ export class IndexTreeModel, TFilterData = voi return node.visible; } - private _updateAncestorsRenderNodeCount(node: IMutableTreeNode | undefined, diff: number): void { + private _updateAncestorsRenderNodeCount(node: IIndexTreeNode | undefined, diff: number): void { if (diff === 0) { return; } @@ -459,7 +460,7 @@ export class IndexTreeModel, TFilterData = voi } } - private _filterNode(node: IMutableTreeNode, parentVisibility: TreeVisibility): TreeVisibility { + private _filterNode(node: IIndexTreeNode, parentVisibility: TreeVisibility): TreeVisibility { const result = this.filter ? this.filter.filter(node.element, parentVisibility) : TreeVisibility.Visible; if (typeof result === 'boolean') { @@ -475,7 +476,7 @@ export class IndexTreeModel, TFilterData = voi } // cheap - private getTreeNode(location: number[], node: IMutableTreeNode = this.root): IMutableTreeNode { + private getTreeNode(location: number[], node: IIndexTreeNode = this.root): IIndexTreeNode { if (!location || location.length === 0) { return node; } @@ -490,7 +491,7 @@ export class IndexTreeModel, TFilterData = voi } // expensive - private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode, listIndex: number, revealed: boolean, visible: boolean } { + private getTreeNodeWithListIndex(location: number[]): { node: IIndexTreeNode, listIndex: number, revealed: boolean, visible: boolean } { if (location.length === 0) { return { node: this.root, listIndex: -1, revealed: true, visible: false }; } @@ -507,7 +508,7 @@ export class IndexTreeModel, TFilterData = voi return { node, listIndex, revealed, visible: visible && node.visible }; } - private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; visible: boolean; } { + private getParentNodeWithListIndex(location: number[], node: IIndexTreeNode = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IIndexTreeNode; listIndex: number; revealed: boolean; visible: boolean; } { const [index, ...rest] = location; if (index < 0 || index > node.children.length) { @@ -536,27 +537,24 @@ export class IndexTreeModel, TFilterData = voi // TODO@joao perf! getNodeLocation(node: ITreeNode): number[] { const location: number[] = []; + let indexTreeNode = node as IIndexTreeNode; // typing woes - while (node.parent) { - location.push(node.parent.children.indexOf(node)); - node = node.parent; + while (indexTreeNode.parent) { + location.push(indexTreeNode.parent.children.indexOf(indexTreeNode)); + indexTreeNode = indexTreeNode.parent; } return location.reverse(); } - getParentNodeLocation(location: number[]): number[] { - if (location.length <= 1) { + getParentNodeLocation(location: number[]): number[] | undefined { + if (location.length === 0) { + return undefined; + } else if (location.length === 1) { return []; + } else { + return tail2(location)[0]; } - - 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 { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index ce5a69e94ff51..af4df7fd48f07 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -180,11 +180,6 @@ export class ObjectTreeModel, TFilterData extends Non })); } - 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); @@ -252,13 +247,12 @@ export class ObjectTreeModel, TFilterData extends Non throw new Error(`Invalid getParentNodeLocation call`); } - const node = this.nodes.get(element); - - if (!node) { - throw new Error(`Tree element not found: ${element}`); - } + const node = this.nodes.get(element)!; + const location = this.model.getNodeLocation(node); + const parentLocation = this.model.getParentNodeLocation(location); + const parent = this.model.getNode(parentLocation); - return node.parent!.element; + return parent.element; } private getElementLocation(element: T | null): number[] { diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 5f9f2e14d6967..6d4d6ca880846 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -81,7 +81,6 @@ export interface ITreeElement { export interface ITreeNode { readonly element: T; - readonly parent: ITreeNode | undefined; readonly children: ITreeNode[]; readonly depth: number; readonly visibleChildrenCount: number; @@ -113,9 +112,8 @@ export interface ITreeModel { getListRenderCount(location: TRef): number; getNode(location?: TRef): ITreeNode; getNodeLocation(node: ITreeNode): TRef; - getParentNodeLocation(location: TRef): TRef; + getParentNodeLocation(location: TRef): TRef | undefined; - getParentElement(location: TRef): T; getFirstElementChild(location: TRef): T | undefined; getLastElementAncestor(location?: TRef): T | undefined; @@ -159,7 +157,6 @@ export interface ITreeContextMenuEvent { export interface ITreeNavigator { current(): T | null; previous(): T | null; - parent(): T | null; first(): T | null; last(): T | null; next(): T | null; diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 25e84a34f4c67..5f1d38d77a8e4 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { ITreeNode, ITreeFilter, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator } from 'vs/base/common/iterator'; -import { IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IndexTreeModel, IIndexTreeNode } from 'vs/base/browser/ui/tree/indexTreeModel'; function toSpliceable(arr: T[]): ISpliceable { return { @@ -576,7 +576,7 @@ suite('IndexTreeModel', function () { suite('getNodeLocation', function () { test('simple', function () { - const list: ITreeNode[] = []; + const list: IIndexTreeNode[] = []; const model = new IndexTreeModel(toSpliceable(list), -1); model.splice([0], 0, Iterator.fromArray([ @@ -600,7 +600,7 @@ suite('IndexTreeModel', function () { }); test('with filter', function () { - const list: ITreeNode[] = []; + const list: IIndexTreeNode[] = []; const filter = new class implements ITreeFilter { filter(element: number): TreeVisibility { return element % 2 === 0 ? TreeVisibility.Visible : TreeVisibility.Hidden; diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 1aed1573903ed..3d2571c539213 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -82,8 +82,6 @@ suite('ObjectTree', function () { assert.equal(navigator.previous(), null); assert.equal(navigator.next(), 0); assert.equal(navigator.next(), 10); - assert.equal(navigator.parent(), 0); - assert.equal(navigator.parent(), null); assert.equal(navigator.first(), 0); assert.equal(navigator.last(), 2); }); @@ -113,7 +111,6 @@ suite('ObjectTree', function () { assert.equal(navigator.previous(), 0); assert.equal(navigator.previous(), null); assert.equal(navigator.next(), 0); - assert.equal(navigator.parent(), null); assert.equal(navigator.first(), 0); assert.equal(navigator.last(), 2); }); @@ -148,8 +145,6 @@ suite('ObjectTree', function () { assert.equal(navigator.previous(), null); assert.equal(navigator.next(), 0); assert.equal(navigator.next(), 10); - assert.equal(navigator.parent(), 0); - assert.equal(navigator.parent(), null); assert.equal(navigator.first(), 0); assert.equal(navigator.last(), 2); }); @@ -181,8 +176,6 @@ suite('ObjectTree', function () { assert.equal(navigator.previous(), null); assert.equal(navigator.next(), 0); assert.equal(navigator.next(), 10); - assert.equal(navigator.parent(), 0); - assert.equal(navigator.parent(), null); assert.equal(navigator.first(), 0); assert.equal(navigator.last(), 2); }); @@ -265,4 +258,29 @@ suite('CompressibleObjectTree', function () { const rows = toArray(container.querySelectorAll('.monaco-tl-contents')); assert.equal(rows.length, 0); }); + + // test('simple', function () { + // const container = document.createElement('div'); + // container.style.width = '200px'; + // container.style.height = '200px'; + + // const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); + // tree.layout(200); + + // tree.setChildren(null, [ + // { + // element: 0, children: [ + // { element: 10 }, + // { element: 11 }, + // { element: 12 }, + // ] + // }, + // { element: 1 }, + // { element: 2 } + // ]); + + // const rows = toArray(container.querySelectorAll('.monaco-tl-contents')); + // assert.equal(rows.length, 0); + + // }); }); \ No newline at end of file diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 634dcd6959934..c087cdfade93d 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -669,14 +669,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (!start) { scope = undefined; } else { - const selectedNode = tree.getNode(start); - const parentNode = selectedNode.parent; - - if (!parentNode || !parentNode.parent) { // root - scope = undefined; - } else { - scope = parentNode.element; - } + scope = tree.getParentElement(start); } const newSelection: unknown[] = []; diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 0d2d44cb5a1d4..0338e1b296e93 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -5,7 +5,6 @@ import * as DOM from 'vs/base/browser/dom'; import { Action } from 'vs/base/common/actions'; -import { INavigator } from 'vs/base/common/iterator'; import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; @@ -32,6 +31,7 @@ import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/search import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; +import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean { const searchView = getSearchView(viewletService, panelService); @@ -450,7 +450,7 @@ export abstract class AbstractSearchAndReplaceAction extends Action { } getNextElementAfterRemoved(viewer: WorkbenchObjectTree, element: RenderableMatch): RenderableMatch { - const navigator: INavigator = viewer.navigate(element); + const navigator: ITreeNavigator = viewer.navigate(element); if (element instanceof BaseFolderMatch) { while (!!navigator.next() && !(navigator.current() instanceof BaseFolderMatch)) { } } else if (element instanceof FileMatch) { @@ -464,7 +464,7 @@ export abstract class AbstractSearchAndReplaceAction extends Action { } getPreviousElementAfterRemoved(viewer: WorkbenchObjectTree, element: RenderableMatch): RenderableMatch { - const navigator: INavigator = viewer.navigate(element); + const navigator: ITreeNavigator = viewer.navigate(element); let previousElement = navigator.previous(); // Hence take the previous element. @@ -616,7 +616,7 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { } private getElementToFocusAfterReplace(): Match { - const navigator: INavigator = this.viewer.navigate(); + const navigator: ITreeNavigator = this.viewer.navigate(); let fileMatched = false; let elementToFocus: any = null; do { diff --git a/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts b/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts index eb4a7517a690d..5717a23bfbe04 100644 --- a/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts +++ b/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts @@ -68,10 +68,6 @@ class ArrayNavigator implements ITreeNavigator { return this.elements[--this.index]; } - parent(): T | null { - throw new Error('not implemented'); - } - first(): T | null { this.index = 0; return this.elements[this.index]; From 70a6210497669a9aa12b3703312b9efad1dca9f7 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 16:23:38 +0200 Subject: [PATCH 06/17] first CompressibleObjectTree test --- .../test/browser/ui/tree/objectTree.test.ts | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 3d2571c539213..8c980ee8554e3 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -239,7 +239,7 @@ suite('CompressibleObjectTree', function () { return container; } renderElement(node: ITreeNode, _: number, templateData: HTMLElement): void { - templateData.textContent = `regular: ${node.element}`; + templateData.textContent = `${node.element}`; } renderCompressedElements(node: ITreeNode, void>, _: number, templateData: HTMLElement): void { templateData.textContent = `compressed: ${node.element.elements.join('/')}`; @@ -259,28 +259,29 @@ suite('CompressibleObjectTree', function () { assert.equal(rows.length, 0); }); - // test('simple', function () { - // const container = document.createElement('div'); - // container.style.width = '200px'; - // container.style.height = '200px'; - - // const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); - // tree.layout(200); - - // tree.setChildren(null, [ - // { - // element: 0, children: [ - // { element: 10 }, - // { element: 11 }, - // { element: 12 }, - // ] - // }, - // { element: 1 }, - // { element: 2 } - // ]); - - // const rows = toArray(container.querySelectorAll('.monaco-tl-contents')); - // assert.equal(rows.length, 0); - - // }); + test('simple', function () { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); + tree.layout(200); + + tree.setChildren(null, [ + { + element: 0, children: [ + { element: 10 }, + { element: 11 }, + { element: 12 }, + ] + }, + { element: 1 }, + { element: 2 } + ]); + + const rows = toArray(container.querySelectorAll('.monaco-tl-contents')) + .map(row => row.textContent); + + assert.deepEqual(rows, ['0', '10', '11', '12', '1', '2']); + }); }); \ No newline at end of file From 3641571707ea0edbcc60c7ac3d6a5c6258bc3aa9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 16:36:15 +0200 Subject: [PATCH 07/17] more CompressibleObjectTree tests --- .../test/browser/ui/tree/objectTree.test.ts | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 8c980ee8554e3..57a529b85b7c9 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -242,7 +242,7 @@ suite('CompressibleObjectTree', function () { templateData.textContent = `${node.element}`; } renderCompressedElements(node: ITreeNode, void>, _: number, templateData: HTMLElement): void { - templateData.textContent = `compressed: ${node.element.elements.join('/')}`; + templateData.textContent = `${node.element.elements.join('/')}`; } disposeTemplate(): void { } } @@ -279,9 +279,64 @@ suite('CompressibleObjectTree', function () { { element: 2 } ]); - const rows = toArray(container.querySelectorAll('.monaco-tl-contents')) - .map(row => row.textContent); - + const rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); assert.deepEqual(rows, ['0', '10', '11', '12', '1', '2']); }); + + test('compressed', () => { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); + tree.layout(200); + + tree.setChildren(null, Iterator.fromArray([ + { + element: 1, children: Iterator.fromArray([{ + element: 11, children: Iterator.fromArray([{ + element: 111, children: Iterator.fromArray([ + { element: 1111 }, + { element: 1112 }, + { element: 1113 }, + ]) + }]) + }]) + } + ])); + + let rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + + tree.setChildren(11, Iterator.fromArray([ + { element: 111 }, + { element: 112 }, + { element: 113 }, + ])); + + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11', '111', '112', '113']); + + tree.setChildren(113, Iterator.fromArray([ + { element: 1131 } + ])); + + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11', '111', '112', '113/1131']); + + tree.setChildren(1131, Iterator.fromArray([ + { element: 1132 } + ])); + + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11', '111', '112', '113/1131/1132']); + + tree.setChildren(1131, Iterator.fromArray([ + { element: 1132 }, + { element: 1133 }, + ])); + + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11', '111', '112', '113/1131', '1132', '1133']); + }); }); \ No newline at end of file From 7d581312b2b85d43cbfe026941864ef66364fc86 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 24 Jul 2019 17:52:37 +0200 Subject: [PATCH 08/17] CompressibleObjectTree tests: enableCompression --- .../ui/tree/compressedObjectTreeModel.ts | 110 +++++++++++------- src/vs/base/browser/ui/tree/objectTree.ts | 8 ++ .../test/browser/ui/tree/objectTree.test.ts | 36 ++++++ 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index e26e9310f248e..fca5eea2a7b18 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -21,6 +21,16 @@ export interface ICompressedTreeNode { 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) + }; +} + // Exported only for test reasons, do not use directly export function compress(element: ICompressedTreeElement): ITreeElement> { const elements = [element.element]; @@ -97,6 +107,7 @@ export class CompressedObjectTreeModel, TFilterData e private model: ObjectTreeModel, TFilterData>; private nodes = new Map>(); + private enabled: boolean = true; get size(): number { return this.nodes.size; } @@ -106,50 +117,12 @@ export class CompressedObjectTreeModel, TFilterData e setChildren( element: T | null, - children: ISequence> | undefined, - onDidCreateNode?: (node: ITreeNode, TFilterData>) => void, - onDidDeleteNode?: (node: ITreeNode, TFilterData>) => void + 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); - } - - // 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); - this.model.setChildren(null, compressedChildren, _onDidCreateNode, _onDidDeleteNode); + const compressedChildren = Iterator.map(Iterator.from(children), this.enabled ? compress : noCompress); + this._setChildren(null, compressedChildren); return; } @@ -165,12 +138,53 @@ export class CompressedObjectTreeModel, TFilterData e const decompressedElement = decompress(node); const splicedElement = splice(decompressedElement, element, Iterator.from(children)); - const recompressedElement = compress(splicedElement); + const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement); const parentChildren = parent.children .map(child => child === node ? recompressedElement : child); - this.model.setChildren(parent.element, parentChildren, _onDidCreateNode, _onDidDeleteNode); + 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 ITreeNode>[]); + 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 { @@ -360,10 +374,18 @@ export class CompressibleObjectTreeModel, TFilterData this.model = new CompressedObjectTreeModel(mapList(this.nodeMapper, list), mapOptions(compressedNodeMapper, options)); } - setChildren(element: T | null, children?: ISequence>): void { + 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); } diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index b7cb458d7560f..6b6ac580d8638 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -145,6 +145,14 @@ export class CompressibleObjectTree, TFilterData = vo return new CompressibleObjectTreeModel(view, options); } + isCompressionEnabled(): boolean { + return this.model.isCompressionEnabled(); + } + + setCompressionEnabled(enabled: boolean): void { + this.model.setCompressionEnabled(enabled); + } + getCompressedTreeNode(element: T): ITreeNode, TFilterData> { return this.model.getCompressedTreeNode(element)!; } diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index 57a529b85b7c9..5d70f8872c4fe 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -339,4 +339,40 @@ suite('CompressibleObjectTree', function () { rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); assert.deepEqual(rows, ['1/11', '111', '112', '113/1131', '1132', '1133']); }); + + test('enableCompression', () => { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree(container, new Delegate(), [new Renderer()]); + tree.layout(200); + + assert.equal(tree.isCompressionEnabled(), true); + + tree.setChildren(null, Iterator.fromArray([ + { + element: 1, children: Iterator.fromArray([{ + element: 11, children: Iterator.fromArray([{ + element: 111, children: Iterator.fromArray([ + { element: 1111 }, + { element: 1112 }, + { element: 1113 }, + ]) + }]) + }]) + } + ])); + + let rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + + tree.setCompressionEnabled(false); + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1', '11', '111', '1111', '1112', '1113']); + + tree.setCompressionEnabled(true); + rows = toArray(container.querySelectorAll('.monaco-tl-contents')).map(row => row.textContent); + assert.deepEqual(rows, ['1/11/111', '1111', '1112', '1113']); + }); }); \ No newline at end of file From 7434a775a64c8a6975ba3da52022926d4ac3853a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 9 Sep 2019 15:09:47 +0200 Subject: [PATCH 09/17] fix manual tree tests --- src/vs/base/browser/ui/list/listWidget.ts | 2 +- test/tree/public/index.html | 33 +++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 3bd172ff3189f..ab64dd8bdd422 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -862,7 +862,7 @@ export interface IListStyles { } const defaultStyles: IListStyles = { - listFocusBackground: Color.fromHex('#073655'), + listFocusBackground: Color.fromHex('#7FB0D0'), listActiveSelectionBackground: Color.fromHex('#0E639C'), listActiveSelectionForeground: Color.fromHex('#FFFFFF'), listFocusAndSelectionBackground: Color.fromHex('#094771'), diff --git a/test/tree/public/index.html b/test/tree/public/index.html index 17c88461c4ba2..3f66d50501674 100644 --- a/test/tree/public/index.html +++ b/test/tree/public/index.html @@ -44,7 +44,7 @@ require.config({ baseUrl: '/static' }); - require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/compressedObjectTree', 'vs/base/browser/ui/tree/asyncDataTree', 'vs/base/browser/ui/tree/dataTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { CompressedObjectTree }, { AsyncDataTree }, { DataTree }, { TreeVisibility }, { iter }) => { + require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/objectTree', 'vs/base/browser/ui/tree/asyncDataTree', 'vs/base/browser/ui/tree/dataTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { CompressibleObjectTree }, { AsyncDataTree }, { DataTree }, { TreeVisibility }, { iter }) => { function createIndexTree(opts) { opts = opts || {}; @@ -95,7 +95,7 @@ } }; - const tree = new IndexTree(container, delegate, [renderer], null, { ...opts, filter: treeFilter, setRowLineHeight: false }); + const tree = new IndexTree('test', container, delegate, [renderer], null, { ...opts, filter: treeFilter, setRowLineHeight: false }); return { tree, treeFilter }; } @@ -113,11 +113,10 @@ templateId: 'template', renderTemplate(container) { return container; }, renderElement(element, index, container) { - if (element.element.elements.length > 1) { - container.innerHTML = `🙈 ${element.element.elements.map(el => el.name).join('/')}`; - } else { - container.innerHTML = element.element.elements[0].name; - } + container.innerHTML = element.element.name; + }, + renderCompressedElements(node, index, container, height) { + container.innerHTML = `🙈 ${node.element.elements.map(el => el.name).join('/')}`; }, disposeElement() { }, disposeTemplate() { } @@ -146,7 +145,7 @@ } }; - const tree = new CompressedObjectTree(container, delegate, [renderer], { ...opts, filter: treeFilter, setRowLineHeight: false, collapseByDefault: true, setRowLineHeight: true }); + const tree = new CompressibleObjectTree('test', container, delegate, [renderer], { ...opts, filter: treeFilter, setRowLineHeight: false, collapseByDefault: true, setRowLineHeight: true }); return { tree, treeFilter }; } @@ -206,7 +205,7 @@ getChildren(element) { return new Promise((c, e) => { const xhr = new XMLHttpRequest(); - xhr.open('GET', element ? `/ api / readdir ? path = ${element.element.path} ` : '/api/readdir'); + xhr.open('GET', element ? `/api/readdir?path=${element.element.path}` : '/api/readdir'); xhr.send(); xhr.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { @@ -228,7 +227,7 @@ } }; - const tree = new AsyncDataTree(container, delegate, [renderer], dataSource, { filter: treeFilter, sorter, identityProvider }); + const tree = new AsyncDataTree('test', container, delegate, [renderer], dataSource, { filter: treeFilter, sorter, identityProvider }); return { tree, treeFilter }; } @@ -283,15 +282,15 @@ } }; - const tree = new DataTree(container, delegate, [renderer], dataSource, { filter: treeFilter, identityProvider }); + const tree = new DataTree('test', container, delegate, [renderer], dataSource, { filter: treeFilter, identityProvider }); - tree.input = { + tree.setInput({ children: [ { name: 'A', children: [{ name: 'AA' }, { name: 'AB' }] }, { name: 'B', children: [{ name: 'BA', children: [{ name: 'BAA' }] }, { name: 'BB' }] }, { name: 'C' } ] - }; + }); return { tree, treeFilter }; } @@ -324,9 +323,9 @@ expandall.onclick = () => perf('expand all', () => tree.expandAll()); collapseall.onclick = () => perf('collapse all', () => tree.collapseAll()); renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random())); - refresh.onclick = () => perf('refresh', () => tree.refresh(null, true)); + refresh.onclick = () => perf('refresh', () => tree.updateChildren()); - tree.refresh(null); + tree.setInput(null); break; } @@ -336,7 +335,7 @@ expandall.onclick = () => perf('expand all', () => tree.expandAll()); collapseall.onclick = () => perf('collapse all', () => tree.collapseAll()); renderwidth.onclick = () => perf('renderwidth', () => tree.layoutWidth(Math.random())); - refresh.onclick = () => perf('refresh', () => tree.refresh(null, true)); + refresh.onclick = () => perf('refresh', () => tree.updateChildren()); break; } @@ -401,4 +400,4 @@ - \ No newline at end of file + From 0a1555239b6e8cc283f5543cd4bdb2a1ff4d5bc4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 9 Sep 2019 17:01:07 +0200 Subject: [PATCH 10/17] compressed object tree model: use proxy --- .../ui/tree/compressedObjectTreeModel.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index b36e8c550b16b..44699a475cc97 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -305,12 +305,31 @@ export type NodeMapper = (node: ITreeNode export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; -function mapNode(compressedNodeMapper: CompressedNodeMapper, node: ITreeNode | null, TFilterData>): ITreeNode { - return { - ...node, - element: node.element === null ? null : compressedNodeMapper(node.element), - children: node.children.map(child => mapNode(compressedNodeMapper, child)) +function createNodeMapper(compressedNodeMapper: CompressedNodeMapper): NodeMapper { + const map = new WeakMap | null, TFilterData>, ITreeNode>(); + const nodeMapper: NodeMapper = node => { + let result = map.get(node); + + if (!result) { + result = new Proxy(node, { + get(obj, prop) { + if (prop === 'element') { + return node.element === null ? null : compressedNodeMapper(node.element); + } else if (prop === 'children') { + return node.children.map(nodeMapper); + } + + return (obj as any)[prop]; + } + }) as ITreeNode; + + map.set(node, result); + } + + return result; }; + + return nodeMapper; } function mapList(nodeMapper: NodeMapper, list: ISpliceable>): ISpliceable, TFilterData>> { @@ -379,7 +398,7 @@ export class CompressibleObjectTreeModel, TFilterData ) { this.elementMapper = options.elementMapper || DefaultElementMapper; const compressedNodeMapper: CompressedNodeMapper = node => this.elementMapper(node.elements); - this.nodeMapper = node => mapNode(compressedNodeMapper, node); + this.nodeMapper = createNodeMapper(compressedNodeMapper); this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeMapper, options)); } From 435ce2ec84c2c20173ab52f5c6203c6b832da58b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 10 Sep 2019 16:59:06 +0200 Subject: [PATCH 11/17] wip: CompressibleAsyncDataTree --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 68 ++++++++++++++++--- src/vs/platform/list/browser/listService.ts | 36 ++++++++-- .../files/browser/views/explorerView.ts | 6 +- .../files/browser/views/explorerViewer.ts | 48 ++++++++++++- 4 files changed, 138 insertions(+), 20 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 737a5da79c8b8..e48b12ee3c9d6 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; -import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -18,6 +18,7 @@ import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors import { toggleClass } from 'vs/base/browser/dom'; import { values } from 'vs/base/common/map'; import { ScrollEvent } from 'vs/base/common/scrollable'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; interface IAsyncDataTreeNode { element: TInput | T; @@ -101,8 +102,8 @@ class DataTreeRenderer implements ITreeRe private disposables: IDisposable[] = []; constructor( - private renderer: ITreeRenderer, - private nodeMapper: NodeMapper, + protected renderer: ITreeRenderer, + protected nodeMapper: NodeMapper, readonly onDidChangeTwistieState: Event> ) { this.templateId = renderer.templateId; @@ -317,9 +318,9 @@ export class AsyncDataTree implements IDisposable private readonly autoExpandSingleChildren: boolean; private readonly _onDidRender = new Emitter(); - private readonly _onDidChangeNodeSlowState = new Emitter>(); + protected readonly _onDidChangeNodeSlowState = new Emitter>(); - private readonly nodeMapper = new NodeMapper(); + protected readonly nodeMapper = new NodeMapper(); protected readonly disposables: IDisposable[] = []; @@ -366,11 +367,7 @@ export class AsyncDataTree implements IDisposable this.sorter = options.sorter; this.collapseByDefault = options.collapseByDefault; - const objectTreeDelegate = new ComposedTreeDelegate>(delegate); - const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); - const objectTreeOptions = asObjectTreeOptions(options) || {}; - - this.tree = new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); + this.tree = this.createTree(user, container, delegate, renderers, options); this.root = createAsyncDataTreeNode({ element: undefined!, @@ -390,6 +387,20 @@ export class AsyncDataTree implements IDisposable this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables); } + protected createTree( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ITreeRenderer[], + options: IAsyncDataTreeOptions + ): ObjectTree, TFilterData> { + const objectTreeDelegate = new ComposedTreeDelegate>(delegate); + const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); + const objectTreeOptions = asObjectTreeOptions(options) || {}; + + return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); + } + updateOptions(options: IAsyncDataTreeOptionsUpdate = {}): void { this.tree.updateOptions(options); } @@ -933,3 +944,40 @@ export class AsyncDataTree implements IDisposable dispose(this.disposables); } } + +class CompressibleDataTreeRenderer + extends DataTreeRenderer + implements ICompressibleTreeRenderer, TFilterData, IDataTreeListTemplateData> +{ + renderCompressedElements(node: ITreeNode>, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { + this.renderer.renderElement(this.nodeMapper.mapNode(node) as ITreeNode, index, templateData.templateData, height); + } +} + +export class CompressibleAsyncDataTree extends AsyncDataTree { + + constructor( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + dataSource: IAsyncDataSource, + options: IAsyncDataTreeOptions = {} + ) { + super(user, container, delegate, renderers, dataSource, options); + } + + protected createTree( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ITreeRenderer[], + options: IAsyncDataTreeOptions + ): ObjectTree, TFilterData> { + const objectTreeDelegate = new ComposedTreeDelegate>(delegate); + const objectTreeRenderers = renderers.map(r => new CompressibleDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); + const objectTreeOptions = asObjectTreeOptions(options) || {}; + + return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); + } +} diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6dd301cdc5f8b..5c33f6bd9da55 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -25,9 +25,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; -import { AsyncDataTree, IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; import { IKeyboardNavigationEventFilter, IAbstractTreeOptions, RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -681,7 +681,7 @@ export class TreeResourceNavigator2 extends Disposable { readonly onDidOpenResource: Event> = this._onDidOpenResource.event; constructor( - private tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree, + private tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options?: IResourceResultsNavigationOptions2 ) { super(); @@ -863,6 +863,34 @@ export class WorkbenchAsyncDataTree extends Async } } +export class WorkbenchCompressibleAsyncDataTree extends CompressibleAsyncDataTree { + + private internals: WorkbenchTreeInternals; + get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } + get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } + + constructor( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + dataSource: IAsyncDataSource, + options: IAsyncDataTreeOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService + ) { + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + super(user, container, delegate, renderers, dataSource, treeOptions); + this.disposables.push(disposable); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.disposables.push(this.internals); + } +} + function workbenchTreeDataPreamble | IAsyncDataTreeOptions>( container: HTMLElement, options: TOptions, @@ -928,7 +956,7 @@ class WorkbenchTreeInternals { private disposables: IDisposable[] = []; constructor( - tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree, + tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options: IAbstractTreeOptions | IAsyncDataTreeOptions, getAutomaticKeyboardNavigation: () => boolean | undefined, @IContextKeyService contextKeyService: IContextKeyService, diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 52583917ba028..3363d72a653c8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -24,7 +24,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; -import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { WorkbenchCompressibleAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; @@ -55,7 +55,7 @@ export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState'; - private tree!: WorkbenchAsyncDataTree; + private tree!: WorkbenchCompressibleAsyncDataTree; private filter!: FilesFilter; private resourceContext: ResourceContextKey; @@ -274,7 +274,7 @@ export class ExplorerView extends ViewletPanel { this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), [filesRenderer], + this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), [filesRenderer], this.instantiationService.createInstance(ExplorerDataSource), { accessibilityProvider: new ExplorerAccessibilityProvider(), ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 0c368120dff74..81097eed8b98e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -15,7 +15,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -47,6 +47,8 @@ import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/work import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { Emitter } from 'vs/base/common/event'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -116,7 +118,7 @@ export interface IFileTemplateData { container: HTMLElement; } -export class FilesRenderer implements ITreeRenderer, IDisposable { +export class FilesRenderer implements ICompressibleTreeRenderer, IDisposable { static readonly ID = 'file'; private config: IFilesConfiguration; @@ -267,7 +269,47 @@ export class FilesRenderer implements ITreeRenderer ignoreBlur = true); } - disposeElement?(element: ITreeNode, index: number, templateData: IFileTemplateData): void { + renderCompressedElements(node: ITreeNode, [number, number, number]>, index: number, templateData: IFileTemplateData, height: number | undefined): void { + templateData.elementDisposable.dispose(); + const stat = node.element.elements[node.element.elements.length - 1]; + const editableData = this.explorerService.getEditableData(stat); + + // File Label + if (!editableData) { + templateData.label.element.style.display = 'flex'; + const extraClasses = ['explorer-item']; + if (this.explorerService.isCut(stat)) { + extraClasses.push('cut'); + } + templateData.label.setFile(stat.resource, { + hidePath: true, + fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, + extraClasses, + fileDecorations: this.config.explorer.decorations, + matches: createMatches(node.filterData) + }); + + templateData.elementDisposable = templateData.label.onDidRender(() => { + try { + this.updateWidth(stat); + } catch (e) { + // noop since the element might no longer be in the tree, no update of width necessery + } + }); + } + + // Input Box + else { + templateData.label.element.style.display = 'none'; + templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData); + } + } + + disposeElement?(_element: ITreeNode, _index: number, templateData: IFileTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeCompressedElements?(_node: ITreeNode, FuzzyScore>, _index: number, templateData: IFileTemplateData): void { templateData.elementDisposable.dispose(); } From 60fa1de65ea1e380794abb9b06b21dccc35b0017 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 10 Sep 2019 17:54:06 +0200 Subject: [PATCH 12/17] wip: more tree node mappers --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 76 ++++++++++++++------ 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index e48b12ee3c9d6..2740452a62cd8 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -67,32 +67,41 @@ interface IDataTreeListTemplateData { templateData: T; } -class NodeMapper { +abstract class WeakMapper { - private map = new WeakMap | null, TFilterData>, ITreeNode>(); + private _map = new WeakMap(); - mapNode(node: ITreeNode | null, TFilterData>): ITreeNode { + map(obj: T): R { const that = this; - let result = this.map.get(node); + let result = this._map.get(obj); if (!result) { - result = new Proxy(node, { + result = new Proxy(obj, { get(obj, prop) { - if (prop === 'element') { - return node.element!.element; - } else if (prop === 'children') { - return node.children.map(child => that.mapNode(child)); - } - - return (obj as any)[prop]; + return that.getProp(obj, prop); } - }) as unknown as ITreeNode; + }) as unknown as R; - this.map.set(node, result); + this._map.set(obj, result); } return result; } + + protected abstract getProp(obj: T, prop: string | number | symbol): any; +} + +class AsyncDataTreeNodeMapper extends WeakMapper | null, TFilterData>, ITreeNode> { + + protected getProp(node: ITreeNode | null, TFilterData>, prop: string | number | symbol): any { + if (prop === 'element') { + return node.element!.element; + } else if (prop === 'children') { + return node.children.map(child => this.map(child)); + } + + return (node as any)[prop]; + } } class DataTreeRenderer implements ITreeRenderer, TFilterData, IDataTreeListTemplateData> { @@ -103,7 +112,7 @@ class DataTreeRenderer implements ITreeRe constructor( protected renderer: ITreeRenderer, - protected nodeMapper: NodeMapper, + protected nodeMapper: AsyncDataTreeNodeMapper, readonly onDidChangeTwistieState: Event> ) { this.templateId = renderer.templateId; @@ -115,7 +124,7 @@ class DataTreeRenderer implements ITreeRe } renderElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { - this.renderer.renderElement(this.nodeMapper.mapNode(node) as ITreeNode, index, templateData.templateData, height); + this.renderer.renderElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); } renderTwistie(element: IAsyncDataTreeNode, twistieElement: HTMLElement): boolean { @@ -125,7 +134,7 @@ class DataTreeRenderer implements ITreeRe disposeElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { if (this.renderer.disposeElement) { - this.renderer.disposeElement(this.nodeMapper.mapNode(node) as ITreeNode, index, templateData.templateData, height); + this.renderer.disposeElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); } } @@ -320,7 +329,7 @@ export class AsyncDataTree implements IDisposable private readonly _onDidRender = new Emitter(); protected readonly _onDidChangeNodeSlowState = new Emitter>(); - protected readonly nodeMapper = new NodeMapper(); + protected readonly nodeMapper = new AsyncDataTreeNodeMapper(); protected readonly disposables: IDisposable[] = []; @@ -536,7 +545,7 @@ export class AsyncDataTree implements IDisposable getNode(element: TInput | T = this.root.element): ITreeNode { const dataNode = this.getDataNode(element); const node = this.tree.getNode(dataNode === this.root ? null : dataNode); - return this.nodeMapper.mapNode(node); + return this.nodeMapper.map(node); } collapse(element: T, recursive: boolean = false): boolean { @@ -945,12 +954,37 @@ export class AsyncDataTree implements IDisposable } } +class CompressibleAsyncDataTreeNodeMapper extends WeakMapper | null>, TFilterData>, ITreeNode, TFilterData>> { + + constructor(private nodeMapper: AsyncDataTreeNodeMapper) { + super(); + } + + protected getProp(node: ITreeNode | null>, TFilterData>, prop: string | number | Symbol): any { + if (prop === 'element') { + return node.element.element; + } else if (prop === 'children') { + return node.children.map(child => this.map(child)); + } + + return (node as any)[prop]; + } +} + class CompressibleDataTreeRenderer extends DataTreeRenderer implements ICompressibleTreeRenderer, TFilterData, IDataTreeListTemplateData> { + constructor( + protected renderer: ICompressibleTreeRenderer, + nodeMapper: AsyncDataTreeNodeMapper, + onDidChangeTwistieState: Event> + ) { + super(renderer, nodeMapper, onDidChangeTwistieState); + } + renderCompressedElements(node: ITreeNode>, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { - this.renderer.renderElement(this.nodeMapper.mapNode(node) as ITreeNode, index, templateData.templateData, height); + this.renderer.renderCompressedElements(this.nodeMapper.map(node), index, templateData.templateData, height); } } @@ -971,7 +1005,7 @@ export class CompressibleAsyncDataTree extends As user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], + renderers: ICompressibleTreeRenderer[], options: IAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); From 6fbb757103cd32f033208af87eade905a59ee61a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 10 Sep 2019 17:56:58 +0200 Subject: [PATCH 13/17] wip: comment out bad code --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 2740452a62cd8..97aec98cc83ed 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -954,22 +954,22 @@ export class AsyncDataTree implements IDisposable } } -class CompressibleAsyncDataTreeNodeMapper extends WeakMapper | null>, TFilterData>, ITreeNode, TFilterData>> { - - constructor(private nodeMapper: AsyncDataTreeNodeMapper) { - super(); - } - - protected getProp(node: ITreeNode | null>, TFilterData>, prop: string | number | Symbol): any { - if (prop === 'element') { - return node.element.element; - } else if (prop === 'children') { - return node.children.map(child => this.map(child)); - } - - return (node as any)[prop]; - } -} +// class CompressibleAsyncDataTreeNodeMapper extends WeakMapper | null>, TFilterData>, ITreeNode, TFilterData>> { + +// constructor(private nodeMapper: AsyncDataTreeNodeMapper) { +// super(); +// } + +// protected getProp(node: ITreeNode | null>, TFilterData>, prop: string | number | Symbol): any { +// if (prop === 'element') { +// return node.element.element; +// } else if (prop === 'children') { +// return node.children.map(child => this.map(child)); +// } + +// return (node as any)[prop]; +// } +// } class CompressibleDataTreeRenderer extends DataTreeRenderer From b1c943026066fde200c51d2f4741349e86013ab7 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 09:56:49 +0200 Subject: [PATCH 14/17] CompressibleAsyncDataTree --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 88 ++++++++++++++------ 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index b0c4e5366274a..e65aa25127ffa 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -102,7 +102,7 @@ class AsyncDataTreeNodeWrapper implements ITreeNode | null, TFilterData>) { } } -class DataTreeRenderer implements ITreeRenderer, TFilterData, IDataTreeListTemplateData> { +class AsyncDataTreeRenderer implements ITreeRenderer, TFilterData, IDataTreeListTemplateData> { readonly templateId: string; private renderedNodes = new Map, IDataTreeListTemplateData>(); @@ -402,7 +402,7 @@ export class AsyncDataTree implements IDisposable options: IAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); - const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); + const objectTreeRenderers = renderers.map(r => new AsyncDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); const objectTreeOptions = asObjectTreeOptions(options) || {}; return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); @@ -952,42 +952,82 @@ export class AsyncDataTree implements IDisposable } } -// class CompressibleAsyncDataTreeNodeMapper extends WeakMapper | null>, TFilterData>, ITreeNode, TFilterData>> { +type CompressibleAsyncDataTreeNodeMapper = WeakMapper>, TFilterData>, ITreeNode, TFilterData>>; -// constructor(private nodeMapper: AsyncDataTreeNodeMapper) { -// super(); -// } +class CompressibleAsyncDataTreeNodeWrapper implements ITreeNode, TFilterData> { -// protected getProp(node: ITreeNode | null>, TFilterData>, prop: string | number | Symbol): any { -// if (prop === 'element') { -// return node.element.element; -// } else if (prop === 'children') { -// return node.children.map(child => this.map(child)); -// } + get element(): ICompressedTreeNode { + return { + elements: this.node.element.elements.map(e => e.element), + incompressible: this.node.element.incompressible + }; + } + + get children(): ITreeNode, TFilterData>[] { return this.node.children.map(node => new CompressibleAsyncDataTreeNodeWrapper(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 node: ITreeNode>, TFilterData>) { } +} + +class CompressibleAsyncDataTreeRenderer implements ICompressibleTreeRenderer, TFilterData, IDataTreeListTemplateData> { -// return (node as any)[prop]; -// } -// } + readonly templateId: string; + private renderedNodes = new Map, IDataTreeListTemplateData>(); + private disposables: IDisposable[] = []; -class CompressibleDataTreeRenderer - extends DataTreeRenderer - implements ICompressibleTreeRenderer, TFilterData, IDataTreeListTemplateData> -{ constructor( protected renderer: ICompressibleTreeRenderer, - nodeMapper: AsyncDataTreeNodeMapper, - onDidChangeTwistieState: Event> + protected nodeMapper: AsyncDataTreeNodeMapper, + protected compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper, + readonly onDidChangeTwistieState: Event> ) { - super(renderer, nodeMapper, onDidChangeTwistieState); + this.templateId = renderer.templateId; + } + + renderTemplate(container: HTMLElement): IDataTreeListTemplateData { + const templateData = this.renderer.renderTemplate(container); + return { templateData }; + } + + renderElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { + this.renderer.renderElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); } renderCompressedElements(node: ITreeNode>, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { - this.renderer.renderCompressedElements(this.nodeMapper.map(node), index, templateData.templateData, height); + this.renderer.renderCompressedElements(this.compressibleNodeMapper.map(node) as ITreeNode, TFilterData>, index, templateData.templateData, height); + } + + renderTwistie(element: IAsyncDataTreeNode, twistieElement: HTMLElement): boolean { + toggleClass(twistieElement, 'loading', element.slow); + return false; + } + + disposeElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { + if (this.renderer.disposeElement) { + this.renderer.disposeElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); + } + } + + disposeTemplate(templateData: IDataTreeListTemplateData): void { + this.renderer.disposeTemplate(templateData.templateData); + } + + dispose(): void { + this.renderedNodes.clear(); + this.disposables = dispose(this.disposables); } } export class CompressibleAsyncDataTree extends AsyncDataTree { + protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); + constructor( user: string, container: HTMLElement, @@ -1007,7 +1047,7 @@ export class CompressibleAsyncDataTree extends As options: IAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); - const objectTreeRenderers = renderers.map(r => new CompressibleDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event)); + const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event)); const objectTreeOptions = asObjectTreeOptions(options) || {}; return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); From ee6db8c9ea4b28c4a5356fc352241a36ad28bdc3 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 14:17:37 +0200 Subject: [PATCH 15/17] fixed AsyncDataTree.asTreeElement --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 64 +++++++++++-------- src/vs/base/browser/ui/tree/objectTree.ts | 6 +- src/vs/platform/list/browser/listService.ts | 7 +- .../files/browser/views/explorerView.ts | 6 +- .../files/browser/views/explorerViewer.ts | 16 +++-- 5 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index e65aa25127ffa..8878086aec120 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -18,7 +18,7 @@ import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors import { toggleClass } from 'vs/base/browser/dom'; import { values } from 'vs/base/common/map'; import { ScrollEvent } from 'vs/base/common/scrollable'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; interface IAsyncDataTreeNode { element: TInput | T; @@ -264,25 +264,6 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt }; } -function asTreeElement(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): ITreeElement> { - let collapsed: boolean | undefined; - - if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) { - collapsed = false; - } else { - collapsed = node.collapsedByDefault; - } - - node.collapsedByDefault = undefined; - - return { - element: node, - children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)) : [], - collapsible: node.hasChildren, - collapsed - }; -} - export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { } export interface IAsyncDataTreeOptions extends IAsyncDataTreeOptionsUpdate, Pick, Exclude, 'collapseByDefault'>> { @@ -909,7 +890,7 @@ export class AsyncDataTree implements IDisposable } private render(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): void { - const children = node.children.map(c => asTreeElement(c, viewStateContext)); + const children = node.children.map(node => this.asTreeElement(node, viewStateContext)); this.tree.setChildren(node === this.root ? null : node, children); if (node !== this.root) { @@ -919,6 +900,25 @@ export class AsyncDataTree implements IDisposable this._onDidRender.fire(); } + protected asTreeElement(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): ITreeElement> { + let collapsed: boolean | undefined; + + if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) { + collapsed = false; + } else { + collapsed = node.collapsedByDefault; + } + + node.collapsedByDefault = undefined; + + return { + element: node, + children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => this.asTreeElement(child, viewStateContext)) : [], + collapsible: node.hasChildren, + collapsed + }; + } + // view state getViewState(): IAsyncDataTreeViewState { @@ -984,7 +984,7 @@ class CompressibleAsyncDataTreeRenderer i constructor( protected renderer: ICompressibleTreeRenderer, protected nodeMapper: AsyncDataTreeNodeMapper, - protected compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper, + private compressibleNodeMapperProvider: () => CompressibleAsyncDataTreeNodeMapper, readonly onDidChangeTwistieState: Event> ) { this.templateId = renderer.templateId; @@ -1000,7 +1000,7 @@ class CompressibleAsyncDataTreeRenderer i } renderCompressedElements(node: ITreeNode>, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { - this.renderer.renderCompressedElements(this.compressibleNodeMapper.map(node) as ITreeNode, TFilterData>, index, templateData.templateData, height); + this.renderer.renderCompressedElements(this.compressibleNodeMapperProvider().map(node) as ITreeNode, TFilterData>, index, templateData.templateData, height); } renderTwistie(element: IAsyncDataTreeNode, twistieElement: HTMLElement): boolean { @@ -1024,6 +1024,10 @@ class CompressibleAsyncDataTreeRenderer i } } +export interface ITreeCompressionDelegate { + isIncompressible(element: T): boolean; +} + export class CompressibleAsyncDataTree extends AsyncDataTree { protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); @@ -1031,12 +1035,13 @@ export class CompressibleAsyncDataTree extends As constructor( user: string, container: HTMLElement, - delegate: IListVirtualDelegate, + virtualDelegate: IListVirtualDelegate, + private compressionDelegate: ITreeCompressionDelegate, renderers: ICompressibleTreeRenderer[], dataSource: IAsyncDataSource, options: IAsyncDataTreeOptions = {} ) { - super(user, container, delegate, renderers, dataSource, options); + super(user, container, virtualDelegate, renderers, dataSource, options); } protected createTree( @@ -1047,9 +1052,16 @@ export class CompressibleAsyncDataTree extends As options: IAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); - const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event)); + const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, () => this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event)); const objectTreeOptions = asObjectTreeOptions(options) || {}; return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); } + + protected asTreeElement(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): ICompressedTreeElement> { + return { + incompressible: this.compressionDelegate.isIncompressible(node.element as T), + ...super.asTreeElement(node, viewStateContext) + }; + } } diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 74683089025a7..243bb7aab217d 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -10,7 +10,7 @@ import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IColla import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; -import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { sorter?: ITreeSorter; @@ -143,6 +143,10 @@ export class CompressibleObjectTree, TFilterData = vo compressibleRenderers.forEach(r => r.compressedTreeNodeProvider = this); } + setChildren(element: T | null, children?: ISequence>): void { + this.model.setChildren(element, children); + } + protected createModel(user: string, view: ISpliceable>, options: ICompressibleObjectTreeOptions): ITreeModel { return new CompressibleObjectTreeModel(user, view, options); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 5c33f6bd9da55..73ed8295cc02f 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -27,7 +27,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; -import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; import { IKeyboardNavigationEventFilter, IAbstractTreeOptions, RenderIndentGuides } from 'vs/base/browser/ui/tree/abstractTree'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -872,7 +872,8 @@ export class WorkbenchCompressibleAsyncDataTree e constructor( user: string, container: HTMLElement, - delegate: IListVirtualDelegate, + virtualDelegate: IListVirtualDelegate, + compressionDelegate: ITreeCompressionDelegate, renderers: ICompressibleTreeRenderer[], dataSource: IAsyncDataSource, options: IAsyncDataTreeOptions, @@ -884,7 +885,7 @@ export class WorkbenchCompressibleAsyncDataTree e @IAccessibilityService accessibilityService: IAccessibilityService ) { const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); - super(user, container, delegate, renderers, dataSource, treeOptions); + super(user, container, virtualDelegate, compressionDelegate, renderers, dataSource, treeOptions); this.disposables.push(disposable); this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); this.disposables.push(this.internals); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 3e4cd12344e62..8f6375eef44ae 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -29,7 +29,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import { ExplorerListVirtualDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop, ExplorerTreeCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -282,7 +282,7 @@ export class ExplorerView extends ViewletPanel { this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), [filesRenderer], + this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerListVirtualDelegate(), new ExplorerTreeCompressionDelegate(), [filesRenderer], this.instantiationService.createInstance(ExplorerDataSource), { accessibilityProvider: new ExplorerAccessibilityProvider(), ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), @@ -309,7 +309,7 @@ export class ExplorerView extends ViewletPanel { sorter: this.instantiationService.createInstance(FileSorter), dnd: this.instantiationService.createInstance(FileDragAndDrop), autoExpandSingleChildren: true, - additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT + additionalScrollHeight: ExplorerListVirtualDelegate.ITEM_HEIGHT }); this._register(this.tree); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 777d34daa997d..34a9e3ea416f8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -49,20 +49,28 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { Emitter } from 'vs/base/common/event'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -export class ExplorerDelegate implements IListVirtualDelegate { +export class ExplorerListVirtualDelegate implements IListVirtualDelegate { static readonly ITEM_HEIGHT = 22; - getHeight(element: ExplorerItem): number { - return ExplorerDelegate.ITEM_HEIGHT; + getHeight(): number { + return ExplorerListVirtualDelegate.ITEM_HEIGHT; } - getTemplateId(element: ExplorerItem): string { + getTemplateId(): string { return FilesRenderer.ID; } } +export class ExplorerTreeCompressionDelegate implements ITreeCompressionDelegate { + + isIncompressible(element: ExplorerItem): boolean { + return !element.isDirectory; + } +} + export const explorerRootErrorEmitter = new Emitter(); export class ExplorerDataSource implements IAsyncDataSource { From cee8daee48b6adc1fdf7691954b3ef758cf966fe Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 14:21:58 +0200 Subject: [PATCH 16/17] undo explorer work --- .../files/browser/views/explorerView.ts | 10 +-- .../files/browser/views/explorerViewer.ts | 64 ++----------------- 2 files changed, 12 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 8f6375eef44ae..fda4351783374 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -24,12 +24,12 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; -import { WorkbenchCompressibleAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerListVirtualDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop, ExplorerTreeCompressionDelegate } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, FilesFilter, FileSorter, FileDragAndDrop } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -55,7 +55,7 @@ export class ExplorerView extends ViewletPanel { static readonly ID: string = 'workbench.explorer.fileView'; static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState'; - private tree!: WorkbenchCompressibleAsyncDataTree; + private tree!: WorkbenchAsyncDataTree; private filter!: FilesFilter; private resourceContext: ResourceContextKey; @@ -282,7 +282,7 @@ export class ExplorerView extends ViewletPanel { this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - this.tree = this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerListVirtualDelegate(), new ExplorerTreeCompressionDelegate(), [filesRenderer], + this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), [filesRenderer], this.instantiationService.createInstance(ExplorerDataSource), { accessibilityProvider: new ExplorerAccessibilityProvider(), ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), @@ -309,7 +309,7 @@ export class ExplorerView extends ViewletPanel { sorter: this.instantiationService.createInstance(FileSorter), dnd: this.instantiationService.createInstance(FileDragAndDrop), autoExpandSingleChildren: true, - additionalScrollHeight: ExplorerListVirtualDelegate.ITEM_HEIGHT + additionalScrollHeight: ExplorerDelegate.ITEM_HEIGHT }); this._register(this.tree); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 34a9e3ea416f8..6061e787c25a4 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -15,7 +15,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -47,30 +47,20 @@ import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/work import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { Emitter } from 'vs/base/common/event'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -export class ExplorerListVirtualDelegate implements IListVirtualDelegate { +export class ExplorerDelegate implements IListVirtualDelegate { static readonly ITEM_HEIGHT = 22; - getHeight(): number { - return ExplorerListVirtualDelegate.ITEM_HEIGHT; + getHeight(element: ExplorerItem): number { + return ExplorerDelegate.ITEM_HEIGHT; } - getTemplateId(): string { + getTemplateId(element: ExplorerItem): string { return FilesRenderer.ID; } } -export class ExplorerTreeCompressionDelegate implements ITreeCompressionDelegate { - - isIncompressible(element: ExplorerItem): boolean { - return !element.isDirectory; - } -} - export const explorerRootErrorEmitter = new Emitter(); export class ExplorerDataSource implements IAsyncDataSource { @@ -126,7 +116,7 @@ export interface IFileTemplateData { container: HTMLElement; } -export class FilesRenderer implements ICompressibleTreeRenderer, IDisposable { +export class FilesRenderer implements ITreeRenderer, IDisposable { static readonly ID = 'file'; private config: IFilesConfiguration; @@ -271,47 +261,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer, [number, number, number]>, index: number, templateData: IFileTemplateData, height: number | undefined): void { - templateData.elementDisposable.dispose(); - const stat = node.element.elements[node.element.elements.length - 1]; - const editableData = this.explorerService.getEditableData(stat); - - // File Label - if (!editableData) { - templateData.label.element.style.display = 'flex'; - const extraClasses = ['explorer-item']; - if (this.explorerService.isCut(stat)) { - extraClasses.push('cut'); - } - templateData.label.setFile(stat.resource, { - hidePath: true, - fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, - extraClasses, - fileDecorations: this.config.explorer.decorations, - matches: createMatches(node.filterData) - }); - - templateData.elementDisposable = templateData.label.onDidRender(() => { - try { - this.updateWidth(stat); - } catch (e) { - // noop since the element might no longer be in the tree, no update of width necessery - } - }); - } - - // Input Box - else { - templateData.label.element.style.display = 'none'; - templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData); - } - } - - disposeElement?(_element: ITreeNode, _index: number, templateData: IFileTemplateData): void { - templateData.elementDisposable.dispose(); - } - - disposeCompressedElements?(_node: ITreeNode, FuzzyScore>, _index: number, templateData: IFileTemplateData): void { + disposeElement?(element: ITreeNode, index: number, templateData: IFileTemplateData): void { templateData.elementDisposable.dispose(); } From ab021bface13062dc2da2753b856c7eb31aecb48 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 15:02:12 +0200 Subject: [PATCH 17/17] remove proxy usage --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 20 +---- .../ui/tree/compressedObjectTreeModel.ts | 79 ++++++++----------- src/vs/base/browser/ui/tree/tree.ts | 18 +++++ 3 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 8878086aec120..1c9aa7ff1e558 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -6,7 +6,7 @@ import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; -import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError } from 'vs/base/browser/ui/tree/tree'; +import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; @@ -67,24 +67,6 @@ interface IDataTreeListTemplateData { templateData: T; } -class WeakMapper { - - constructor(private fn: (k: K) => V) { } - - private _map = new WeakMap(); - - map(key: K): V { - let result = this._map.get(key); - - if (!result) { - result = this.fn(key); - this._map.set(key, result); - } - - return result; - } -} - type AsyncDataTreeNodeMapper = WeakMapper | null, TFilterData>, ITreeNode>; class AsyncDataTreeNodeWrapper implements ITreeNode { diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 44699a475cc97..5551203b350ec 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -6,7 +6,7 @@ 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 } from 'vs/base/browser/ui/tree/tree'; +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 @@ -300,62 +300,53 @@ export class CompressedObjectTreeModel, TFilterData e // Compressible Object Tree export type ElementMapper = (elements: T[]) => T; -export type CompressedNodeMapper = (node: ICompressedTreeNode) => T; -export type NodeMapper = (node: ITreeNode | null, TFilterData>) => ITreeNode; - export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; -function createNodeMapper(compressedNodeMapper: CompressedNodeMapper): NodeMapper { - const map = new WeakMap | null, TFilterData>, ITreeNode>(); - const nodeMapper: NodeMapper = node => { - let result = map.get(node); - - if (!result) { - result = new Proxy(node, { - get(obj, prop) { - if (prop === 'element') { - return node.element === null ? null : compressedNodeMapper(node.element); - } else if (prop === 'children') { - return node.children.map(nodeMapper); - } - - return (obj as any)[prop]; - } - }) as ITreeNode; +export type CompressedNodeUnwrapper = (node: ICompressedTreeNode) => T; +type CompressedNodeWeakMapper = WeakMapper | null, TFilterData>, ITreeNode>; - map.set(node, result); - } +class CompressedTreeNodeWrapper implements ITreeNode { - return result; - }; + 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; } - return nodeMapper; + constructor( + private unwrapper: CompressedNodeUnwrapper, + private node: ITreeNode | null, TFilterData> + ) { } } -function mapList(nodeMapper: NodeMapper, list: ISpliceable>): ISpliceable, 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(nodeMapper) as ITreeNode[]); + list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node)) as ITreeNode[]); } }; } -function mapOptions(compressedNodeMapper: CompressedNodeMapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { +function mapOptions(compressedNodeUnwrapper: CompressedNodeUnwrapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { return { ...options, sorter: options.sorter && { - compare(element: ICompressedTreeNode, otherElement: ICompressedTreeNode): number { - return options.sorter!.compare(compressedNodeMapper(element), compressedNodeMapper(otherElement)); + compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { + return options.sorter!.compare(compressedNodeUnwrapper(node), compressedNodeUnwrapper(otherNode)); } }, identityProvider: options.identityProvider && { - getId(element: ICompressedTreeNode): { toString(): string; } { - return options.identityProvider!.getId(compressedNodeMapper(element)); + getId(node: ICompressedTreeNode): { toString(): string; } { + return options.identityProvider!.getId(compressedNodeUnwrapper(node)); } }, filter: options.filter && { - filter(element: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { - return options.filter!.filter(compressedNodeMapper(element), parentVisibility); + filter(node: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { + return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility); } } }; @@ -371,24 +362,24 @@ export class CompressibleObjectTreeModel, TFilterData get onDidSplice(): Event> { return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({ - insertedNodes: insertedNodes.map(this.nodeMapper), - deletedNodes: deletedNodes.map(this.nodeMapper), + 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(node), + node: this.nodeMapper.map(node), deep })); } get onDidChangeRenderNodeCount(): Event> { - return Event.map(this.model.onDidChangeRenderNodeCount, this.nodeMapper); + return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node)); } private elementMapper: ElementMapper; - private nodeMapper: NodeMapper; + private nodeMapper: CompressedNodeWeakMapper; private model: CompressedObjectTreeModel; constructor( @@ -397,10 +388,10 @@ export class CompressibleObjectTreeModel, TFilterData options: ICompressibleObjectTreeModelOptions = {} ) { this.elementMapper = options.elementMapper || DefaultElementMapper; - const compressedNodeMapper: CompressedNodeMapper = node => this.elementMapper(node.elements); - this.nodeMapper = createNodeMapper(compressedNodeMapper); + 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(compressedNodeMapper, options)); + this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options)); } setChildren(element: T | null, children?: ISequence>): void { @@ -424,7 +415,7 @@ export class CompressibleObjectTreeModel, TFilterData } getNode(location?: T | null | undefined): ITreeNode { - return this.nodeMapper(this.model.getNode(location)); + return this.nodeMapper.map(this.model.getNode(location)); } getNodeLocation(node: ITreeNode): T | null { diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 5054c7d86ace1..bd94fd3d21b3f 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -199,3 +199,21 @@ export class TreeError extends Error { super(`TreeError [${user}] ${message}`); } } + +export class WeakMapper { + + constructor(private fn: (k: K) => V) { } + + private _map = new WeakMap(); + + map(key: K): V { + let result = this._map.get(key); + + if (!result) { + result = this.fn(key); + this._map.set(key, result); + } + + return result; + } +}