diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 3bd172ff3189f94be383701c800c4aa4a8d8f3ca..ab64dd8bdd42230dc22fb18c8da2b411dbe97a92 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/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index c51308ae6c8cdf8f87fac3fada60af4d936402f6..2f739b87132f59b3b49f659e78f4be317f6bce64 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 = {} @@ -381,10 +389,19 @@ class TreeRenderer implements IListRenderer('.indent-guide', { style: `width: ${this.indent}px` }); if (this.activeIndentNodes.has(parent)) { @@ -412,12 +429,16 @@ class TreeRenderer implements IListRenderer>(); + const model = this.modelProvider(); nodes.forEach(node => { + const ref = model.getNodeLocation(node); + const parentRef = model.getParentNodeLocation(ref); + if (node.collapsible && node.children.length > 0 && !node.collapsed) { set.add(node); - } else if (node.parent) { - set.add(node.parent); + } else if (parentRef) { + set.add(model.getNode(parentRef)); } }); @@ -1153,7 +1174,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; @@ -1211,7 +1232,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; @@ -1383,7 +1404,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 { @@ -1539,7 +1562,7 @@ export abstract class AbstractTree implements IDisposable if (!didChange) { const parentLocation = this.model.getParentNodeLocation(location); - if (parentLocation === null) { + if (!parentLocation) { return; } @@ -1641,22 +1664,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 c20a8771c87e0c915eda38dabab47eeef5f73245..1c9aa7ff1e558a6d69a9bafb965bcef4c1d69a4a 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 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 { 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'; @@ -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, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; interface IAsyncDataTreeNode { element: TInput | T; @@ -66,10 +67,11 @@ interface IDataTreeListTemplateData { templateData: T; } +type AsyncDataTreeNodeMapper = WeakMapper | null, TFilterData>, ITreeNode>; + 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; } @@ -82,14 +84,15 @@ 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>(); private disposables: IDisposable[] = []; constructor( - private renderer: ITreeRenderer, + protected renderer: ITreeRenderer, + protected nodeMapper: AsyncDataTreeNodeMapper, readonly onDidChangeTwistieState: Event> ) { this.templateId = renderer.templateId; @@ -101,7 +104,7 @@ class DataTreeRenderer implements ITreeRe } renderElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { - this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, height); + this.renderer.renderElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); } renderTwistie(element: IAsyncDataTreeNode, twistieElement: HTMLElement): boolean { @@ -111,7 +114,7 @@ class DataTreeRenderer implements ITreeRe disposeElement(node: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData, height: number | undefined): void { if (this.renderer.disposeElement) { - this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, height); + this.renderer.disposeElement(this.nodeMapper.map(node) as ITreeNode, index, templateData.templateData, height); } } @@ -243,25 +246,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'>> { @@ -304,7 +288,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>(); + + protected readonly nodeMapper: AsyncDataTreeNodeMapper = new WeakMapper(node => new AsyncDataTreeNodeWrapper(node)); protected readonly disposables: IDisposable[] = []; @@ -351,11 +337,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._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!, @@ -375,6 +357,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 AsyncDataTreeRenderer(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); } @@ -510,7 +506,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 new AsyncDataTreeNodeWrapper(node); + return this.nodeMapper.map(node); } collapse(element: T, recursive: boolean = false): boolean { @@ -876,7 +872,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) { @@ -886,6 +882,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 { @@ -918,3 +933,117 @@ export class AsyncDataTree implements IDisposable dispose(this.disposables); } } + +type CompressibleAsyncDataTreeNodeMapper = WeakMapper>, TFilterData>, ITreeNode, TFilterData>>; + +class CompressibleAsyncDataTreeNodeWrapper implements ITreeNode, TFilterData> { + + 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> { + + readonly templateId: string; + private renderedNodes = new Map, IDataTreeListTemplateData>(); + private disposables: IDisposable[] = []; + + constructor( + protected renderer: ICompressibleTreeRenderer, + protected nodeMapper: AsyncDataTreeNodeMapper, + private compressibleNodeMapperProvider: () => CompressibleAsyncDataTreeNodeMapper, + readonly onDidChangeTwistieState: Event> + ) { + 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.compressibleNodeMapperProvider().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 interface ITreeCompressionDelegate { + isIncompressible(element: T): boolean; +} + +export class CompressibleAsyncDataTree extends AsyncDataTree { + + protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); + + constructor( + user: string, + container: HTMLElement, + virtualDelegate: IListVirtualDelegate, + private compressionDelegate: ITreeCompressionDelegate, + renderers: ICompressibleTreeRenderer[], + dataSource: IAsyncDataSource, + options: IAsyncDataTreeOptions = {} + ) { + super(user, container, virtualDelegate, renderers, dataSource, options); + } + + protected createTree( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + 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 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/compressedObjectTree.ts b/src/vs/base/browser/ui/tree/compressedObjectTree.ts deleted file mode 100644 index beba028a9233117eb9d9d40909a3da7e35ece707..0000000000000000000000000000000000000000 --- a/src/vs/base/browser/ui/tree/compressedObjectTree.ts +++ /dev/null @@ -1,57 +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( - user: string, - container: HTMLElement, - delegate: IListVirtualDelegate>, - renderers: ITreeRenderer, TFilterData, any>[], - options: IObjectTreeOptions, TFilterData> = {} - ) { - super(user, 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(user: string, view: ISpliceable, TFilterData>>, options: IObjectTreeOptions, TFilterData>): ITreeModel | null, TFilterData, T | null> { - return new CompressedTreeModel(user, view, options); - } -} diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 22f9ce43205d052eb696abbd66df12e28aaf9646..5551203b350ec24d3938095566a05cfc025fa654 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -6,19 +6,32 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator, ISequence } from 'vs/base/common/iterator'; import { Event } from 'vs/base/common/event'; -import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; +import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; +// Exported only for test reasons, do not use directly export interface ICompressedTreeElement extends ITreeElement { readonly children?: Iterator> | ICompressedTreeElement[]; readonly incompressible?: boolean; } +// Exported only for test reasons, do not use directly export interface ICompressedTreeNode { readonly elements: T[]; readonly incompressible: boolean; } +function noCompress(element: ICompressedTreeElement): ITreeElement> { + const elements = [element.element]; + const incompressible = element.incompressible || false; + + return { + element: { elements, incompressible }, + children: Iterator.map(Iterator.from(element.children), noCompress) + }; +} + +// 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 +62,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 +78,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 +94,10 @@ export function splice(treeElement: ICompressedTreeElement, element: T, ch }; } -export interface ICompressedTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } +interface ICompressedObjectTreeModelOptions extends IObjectTreeModelOptions, TFilterData> { } -export class CompressedTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { +// 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; @@ -92,39 +107,77 @@ export class CompressedTreeModel, TFilterData extends private model: ObjectTreeModel, TFilterData>; private nodes = new Map>(); + private enabled: boolean = true; get size(): number { return this.nodes.size; } constructor( private user: string, list: ISpliceable, TFilterData>>, - options: ICompressedTreeModelOptions = {} + options: ICompressedObjectTreeModelOptions = {} ) { this.model = new ObjectTreeModel(user, list, options); } setChildren( element: T | null, - children: ISequence> | undefined, - onDidCreateNode?: (node: ITreeNode, TFilterData>) => void, - onDidDeleteNode?: (node: ITreeNode, TFilterData>) => void - ): Iterator> { + children: ISequence> | undefined + ): void { + + if (element === null) { + const compressedChildren = Iterator.map(Iterator.from(children), this.enabled ? compress : noCompress); + this._setChildren(null, compressedChildren); + return; + } + + const compressedNode = this.nodes.get(element); + + if (!compressedNode) { + throw new Error('Unknown compressed tree node'); + } + + const node = this.model.getNode(compressedNode) as ITreeNode, TFilterData>; + const compressedParentNode = this.model.getParentNodeLocation(compressedNode); + const parent = this.model.getNode(compressedParentNode) as ITreeNode, TFilterData>; + + const decompressedElement = decompress(node); + const splicedElement = splice(decompressedElement, element, Iterator.from(children)); + const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement); + + const parentChildren = parent.children + .map(child => child === node ? recompressedElement : child); + + this._setChildren(parent.element, parentChildren); + } + + isCompressionEnabled(): boolean { + return this.enabled; + } + + setCompressionEnabled(enabled: boolean): void { + if (enabled === this.enabled) { + return; + } + + this.enabled = enabled; + + const root = this.model.getNode(); + const rootChildren = Iterator.from(root.children as 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); } - - // 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>) => { @@ -133,40 +186,9 @@ export class CompressedTreeModel, TFilterData extends this.nodes.delete(element); } } - - // if (this.identityProvider) { - // const id = this.identityProvider.getId(node.element).toString(); - // if (!insertedElementIds.has(id)) { - // this.nodesByIdentity.delete(id); - // } - // } - - if (onDidDeleteNode) { - onDidDeleteNode(node); - } }; - if (element === null) { - const compressedChildren = Iterator.map(Iterator.from(children), compress); - const result = this.model.setChildren(null, compressedChildren, _onDidCreateNode, _onDidDeleteNode); - return Iterator.map(result, decompress); - } - - const compressedNode = this.nodes.get(element); - const node = this.model.getNode(compressedNode) as ITreeNode, TFilterData>; - const parent = node.parent!; - - const decompressedElement = decompress(node); - const splicedElement = splice(decompressedElement, element, Iterator.from(children)); - const recompressedElement = compress(splicedElement); - - const parentChildren = parent.children - .map(child => child === node ? recompressedElement : child); - - this.model.setChildren(parent.element, parentChildren, _onDidCreateNode, _onDidDeleteNode); - - // TODO - return Iterator.empty(); + this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode); } getListIndex(location: T | null): number { @@ -211,11 +233,6 @@ export class CompressedTreeModel, TFilterData extends 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); @@ -265,7 +282,7 @@ export class CompressedTreeModel, TFilterData extends this.model.resort(compressedNode, recursive); } - private getCompressedNode(element: T | null): ICompressedTreeNode | null { + getCompressedNode(element: T | null): ICompressedTreeNode | null { if (element === null) { return null; } @@ -280,72 +297,113 @@ export class CompressedTreeModel, TFilterData extends } } +// Compressible Object Tree + export type ElementMapper = (elements: T[]) => T; export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; -export type NodeMapper = (node: ITreeNode | null, TFilterData>) => ITreeNode; +export type CompressedNodeUnwrapper = (node: ICompressedTreeNode) => T; +type CompressedNodeWeakMapper = WeakMapper | null, TFilterData>, ITreeNode>; + +class CompressedTreeNodeWrapper implements ITreeNode { + + get element(): T | null { return this.node.element === null ? null : this.unwrapper(this.node.element); } + get children(): ITreeNode[] { return this.node.children.map(node => new CompressedTreeNodeWrapper(this.unwrapper, node)); } + get depth(): number { return this.node.depth; } + get visibleChildrenCount(): number { return this.node.visibleChildrenCount; } + get visibleChildIndex(): number { return this.node.visibleChildIndex; } + get collapsible(): boolean { return this.node.collapsible; } + get collapsed(): boolean { return this.node.collapsed; } + get visible(): boolean { return this.node.visible; } + get filterData(): TFilterData | undefined { return this.node.filterData; } -function mapNode(elementMapper: ElementMapper, node: ITreeNode | null, TFilterData>): ITreeNode { + constructor( + private unwrapper: CompressedNodeUnwrapper, + private node: ITreeNode | null, TFilterData> + ) { } +} + +function mapList(nodeMapper: CompressedNodeWeakMapper, list: ISpliceable>): ISpliceable, TFilterData>> { 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) + splice(start: number, deleteCount: number, toInsert: ITreeNode, TFilterData>[]): void { + list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node)) as ITreeNode[]); + } }; } -function createNodeMapper(elementMapper: ElementMapper): NodeMapper { - return node => mapNode(elementMapper, node); +function mapOptions(compressedNodeUnwrapper: CompressedNodeUnwrapper, options: ICompressibleObjectTreeModelOptions): ICompressedObjectTreeModelOptions { + return { + ...options, + sorter: options.sorter && { + compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { + return options.sorter!.compare(compressedNodeUnwrapper(node), compressedNodeUnwrapper(otherNode)); + } + }, + identityProvider: options.identityProvider && { + getId(node: ICompressedTreeNode): { toString(): string; } { + return options.identityProvider!.getId(compressedNodeUnwrapper(node)); + } + }, + filter: options.filter && { + filter(node: ICompressedTreeNode, parentVisibility: TreeVisibility): TreeFilterResult { + return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility); + } + } + }; } -export interface ICompressedObjectTreeModelOptions extends ICompressedTreeModelOptions { +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(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.mapNode(node), + node: this.nodeMapper.map(node), deep })); } get onDidChangeRenderNodeCount(): Event> { - return Event.map(this.model.onDidChangeRenderNodeCount, this.mapNode); + return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node)); } - private mapElement: ElementMapper; - private mapNode: NodeMapper; - private model: CompressedTreeModel; + private elementMapper: ElementMapper; + private nodeMapper: CompressedNodeWeakMapper; + private model: CompressedObjectTreeModel; constructor( user: string, - list: ISpliceable, TFilterData>>, - options: ICompressedObjectTreeModelOptions = {} + list: ISpliceable>, + options: ICompressibleObjectTreeModelOptions = {} ) { - this.mapElement = options.elementMapper || DefaultElementMapper; - this.mapNode = createNodeMapper(this.mapElement); - this.model = new CompressedTreeModel(user, list, options); + this.elementMapper = options.elementMapper || DefaultElementMapper; + const compressedNodeUnwrapper: CompressedNodeUnwrapper = node => this.elementMapper(node.elements); + this.nodeMapper = new WeakMapper(node => new CompressedTreeNodeWrapper(compressedNodeUnwrapper, node)); + + this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options)); } - setChildren( - element: T | null, - children: ISequence> | undefined - ): Iterator> { + setChildren(element: T | null, children?: ISequence>): void { this.model.setChildren(element, children); + } - // TODO - return Iterator.empty(); + isCompressionEnabled(): boolean { + return this.model.isCompressionEnabled(); + } + + setCompressionEnabled(enabled: boolean): void { + this.model.setCompressionEnabled(enabled); } getListIndex(location: T | null): number { @@ -357,7 +415,7 @@ export class CompressedObjectTreeModel, TFilterData e } getNode(location?: T | null | undefined): ITreeNode { - return this.mapNode(this.model.getNode(location)); + return this.nodeMapper.map(this.model.getNode(location)); } getNodeLocation(node: ITreeNode): T | null { @@ -368,16 +426,6 @@ export class CompressedObjectTreeModel, TFilterData e return this.model.getParentNodeLocation(location); } - getParentElement(location: T | null): T | null { - const result = this.model.getParentElement(location); - - if (result === null) { - return result; - } - - return this.mapElement(result.elements); - } - getFirstElementChild(location: T | null): T | null | undefined { const result = this.model.getFirstElementChild(location); @@ -385,7 +433,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 { @@ -395,7 +443,7 @@ export class CompressedObjectTreeModel, TFilterData e return result; } - return this.mapElement(result.elements); + return this.elementMapper(result.elements); } isCollapsible(location: T | null): boolean { @@ -429,4 +477,8 @@ export class CompressedObjectTreeModel, TFilterData e resort(element: T | null = null, recursive = true): void { return this.model.resort(element, recursive); } + + getCompressedTreeNode(element: T): ITreeNode, TFilterData> { + return this.model.getNode(element) as ITreeNode, TFilterData>; + } } diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts index fcb9e60c4b2031f410d39f19fa02de6b437dcdc7..7cf73ee5fd7b9008d041a708a865ee2bfd9f8ce4 100644 --- a/src/vs/base/browser/ui/tree/indexTree.ts +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -28,8 +28,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 511ccbbf66c6a873713677460d46d5cf70ebf8a6..6911c5fac8d7a9d65a4a47def0eda15f3194f556 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; @@ -33,13 +34,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; @@ -65,7 +59,7 @@ export class IndexTreeModel, TFilterData = voi readonly rootRef = []; - private root: IMutableTreeNode; + private root: IIndexTreeNode; private eventBufferer = new EventBufferer(); private _onDidChangeCollapseState = new Emitter>(); @@ -112,7 +106,7 @@ export class IndexTreeModel, TFilterData = voi toInsert?: ISequence>, onDidCreateNode?: (node: ITreeNode) => void, onDidDeleteNode?: (node: ITreeNode) => void - ): Iterator> { + ): void { if (location.length === 0) { throw new TreeError(this.user, 'Invalid tree location'); } @@ -136,7 +130,7 @@ export class IndexTreeModel, TFilterData = voi } } - const nodesToInsert: IMutableTreeNode[] = []; + const nodesToInsert: IIndexTreeNode[] = []; let insertedVisibleChildrenCount = 0; let renderNodeCount = 0; @@ -190,9 +184,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 { @@ -275,7 +267,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _setListNodeCollapseState(node: IMutableTreeNode, listIndex: number, revealed: boolean, update: CollapseStateUpdate): boolean { + private _setListNodeCollapseState(node: IIndexTreeNode, listIndex: number, revealed: boolean, update: CollapseStateUpdate): boolean { const result = this._setNodeCollapseState(node, update, false); if (!revealed || !node.visible || !result) { @@ -290,7 +282,7 @@ export class IndexTreeModel, TFilterData = voi return result; } - private _setNodeCollapseState(node: IMutableTreeNode, update: CollapseStateUpdate, deep: boolean): boolean { + private _setNodeCollapseState(node: IIndexTreeNode, update: CollapseStateUpdate, deep: boolean): boolean { let result: boolean; if (node === this.root) { @@ -341,13 +333,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: [], @@ -404,7 +396,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[] = []; @@ -414,7 +406,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; } @@ -432,7 +424,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[] = []; @@ -442,7 +434,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) { @@ -496,7 +488,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; } @@ -508,7 +500,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') { @@ -524,7 +516,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; } @@ -539,7 +531,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 }; } @@ -556,7 +548,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) { @@ -585,27 +577,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/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index d64062f1b13170f64c7be0a14e22a75482f87dfc..243bb7aab217d0498c364b09572deae6e1a95495 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -3,13 +3,14 @@ * 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'; 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, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { sorter?: ITreeSorter; @@ -31,11 +32,8 @@ export class ObjectTree, TFilterData = void> extends super(user, 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 { @@ -55,3 +53,113 @@ export class ObjectTree, TFilterData = void> extends return new ObjectTreeModel(user, view, options); } } + +interface ICompressedTreeNodeProvider { + getCompressedTreeNode(element: T): ITreeNode, TFilterData>; +} + +export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { + readonly elementMapper?: ElementMapper; +} + +export interface ICompressibleTreeRenderer extends ITreeRenderer { + 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 { + compressedTreeNode: ITreeNode, TFilterData> | undefined; + readonly data: TTemplateData; +} + +class CompressibleRenderer implements ITreeRenderer> { + + readonly templateId: string; + readonly onDidChangeTwistieState: Event | undefined; + + compressedTreeNodeProvider: ICompressedTreeNodeProvider; + + constructor(private renderer: ICompressibleTreeRenderer) { + this.templateId = renderer.templateId; + + if (renderer.onDidChangeTwistieState) { + this.onDidChangeTwistieState = renderer.onDidChangeTwistieState; + } + } + + renderTemplate(container: HTMLElement): CompressibleTemplateData { + const data = this.renderer.renderTemplate(container); + return { compressedTreeNode: undefined, data }; + } + + renderElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element); + + if (compressedTreeNode.element.elements.length === 1) { + templateData.compressedTreeNode = undefined; + this.renderer.renderElement(node, index, templateData.data, height); + } else { + templateData.compressedTreeNode = compressedTreeNode; + this.renderer.renderCompressedElements(compressedTreeNode, index, templateData.data, height); + } + } + + disposeElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { + if (templateData.compressedTreeNode) { + if (this.renderer.disposeCompressedElements) { + this.renderer.disposeCompressedElements(templateData.compressedTreeNode, index, templateData.data, height); + } + } else { + if (this.renderer.disposeElement) { + this.renderer.disposeElement(node, 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( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + options: IObjectTreeOptions = {} + ) { + const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r)); + super(user, container, delegate, compressibleRenderers, options); + 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); + } + + 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/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index e2f78b9ebea9feb71113e59ca45fb75c8f231a9d..33fde84435c46341a2c6d587b0d80be16fa0814e 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; } @@ -64,9 +64,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( @@ -74,7 +74,7 @@ export class ObjectTreeModel, TFilterData extends Non children: ISequence> | undefined, onDidCreateNode?: ITreeNodeCallback, onDidDeleteNode?: ITreeNodeCallback - ): Iterator> { + ): void { const insertedElements = new Set(); const insertedElementIds = new Set(); @@ -110,15 +110,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> { @@ -186,11 +184,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); @@ -263,13 +256,12 @@ export class ObjectTreeModel, TFilterData extends Non throw new TreeError(this.user, `Invalid getParentNodeLocation call`); } - const node = this.nodes.get(element); - - if (!node) { - throw new TreeError(this.user, `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 5be49ab229f7f4168244e4e0afd2e38014f95e43..bd94fd3d21b3f3b3baa744d9934db200f6d67468 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; @@ -160,7 +158,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; @@ -202,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; + } +} 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 73f78e9b85500d509a35a149efaa99763cb9414a..b69e61abcb826f430dcae31490c45df96e0e80c9 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('test', toSpliceable(list)); + const model = new CompressedObjectTreeModel('test', 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('test', toSpliceable(list)); + const model = new CompressedObjectTreeModel('test', 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('test', toSpliceable(list)); + const model = new CompressedObjectTreeModel('test', toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { @@ -376,7 +376,7 @@ suite('CompressedObjectTree', function () { test('compressed', () => { const list: ITreeNode>[] = []; - const model = new CompressedTreeModel('test', toSpliceable(list)); + const model = new CompressedObjectTreeModel('test', toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { 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 6d660f358e9665032b7559fd958e04693246d551..01a42eae236458c2605b803f811a17f073503248 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 { @@ -637,7 +637,7 @@ suite('IndexTreeModel', function () { suite('getNodeLocation', function () { test('simple', function () { - const list: ITreeNode[] = []; + const list: IIndexTreeNode[] = []; const model = new IndexTreeModel('test', toSpliceable(list), -1); model.splice([0], 0, Iterator.fromArray([ @@ -661,7 +661,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 bcd9086b7c6bfba5cf1843a3d1338a51c8cad4bf..6a69ea4bf120cb7757ad0c868dfbafcbfe3f829f 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 () { @@ -81,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); }); @@ -112,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); }); @@ -147,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); }); @@ -180,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); }); @@ -225,3 +219,160 @@ suite('ObjectTree', function () { 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 = `${node.element}`; + } + renderCompressedElements(node: ITreeNode, void>, _: number, templateData: HTMLElement): void { + templateData.textContent = `${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('test', container, new Delegate(), [new Renderer()]); + tree.layout(200); + + 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('test', 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']); + }); + + test('compressed', () => { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree('test', 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']); + }); + + test('enableCompression', () => { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '200px'; + + const tree = new CompressibleObjectTree('test', 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']); + }); +}); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6dd301cdc5f8b09f14c9152136a18f4a98d7c08d..73ed8295cc02f4d6d024349e04955535caf01ec8 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, 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'; @@ -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,35 @@ 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, + virtualDelegate: IListVirtualDelegate, + compressionDelegate: ITreeCompressionDelegate, + 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, 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); + } +} + function workbenchTreeDataPreamble | IAsyncDataTreeOptions>( container: HTMLElement, options: TOptions, @@ -928,7 +957,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/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 7de343ab1d333298b2548308d3e65ac5878df331..d33cb450d5d38cd5fff6e34a35e9473ff0c90b5b 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -701,14 +701,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 1973a3e26bf70752d5e078d7b83bd0c143fd1eb6..a8c6d8774f3849314ac06511037186c0b0c4a490 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 { isWindows, OS } from 'vs/base/common/platform'; import { repeat } from 'vs/base/common/strings'; @@ -29,6 +28,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); @@ -447,7 +447,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 FolderMatch) { while (!!navigator.next() && !(navigator.current() instanceof FolderMatch)) { } } else if (element instanceof FileMatch) { @@ -461,7 +461,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. @@ -613,7 +613,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 eb4a7517a690d6653cd0aca87dff5b8f5057f447..5717a23bfbe04a53163fd43fd4f8dd062ca742f6 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]; diff --git a/test/tree/public/index.html b/test/tree/public/index.html index 17c88461c4ba2fce0e7e88cfb14287c442f63d69..3f66d50501674dc6a9097b4db43773395ee86dcf 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 +