/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Iterable } from '../../../../../vs/base/common/iterator'; import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, } from '../../../../../vs/base/browser/ui/tree/abstractTree'; 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, IKeyboardNavigationLabelProvider, IIdentityProvider, } from '../../../../../vs/base/browser/ui/list/list'; import { Event } from '../../../../../vs/base/common/event'; import { CompressibleObjectTreeModel, ICompressedTreeNode, ICompressedTreeElement, } from '../../../../../vs/base/browser/ui/tree/compressedObjectTreeModel'; import { memoize } from '../../../../../vs/base/common/decorators'; import { IList } from '../../../../../vs/base/browser/ui/tree/indexTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { readonly sorter?: ITreeSorter; } export interface IObjectTreeSetChildrenOptions { /** * Identity provider used to optimize splice() calls in the IndexTree. If * this is not present, optimized splicing is not enabled. * * Warning: if this is present, calls to `setChildren()` will not replace * or update nodes if their identity is the same, even if the elements are * different. For this, you should call `rerender()`. */ readonly diffIdentityProvider?: IIdentityProvider; } export class ObjectTree< T extends NonNullable, TFilterData = void > extends AbstractTree { protected override model!: IObjectTreeModel; override get onDidChangeCollapseState(): Event< ICollapseStateChangeEvent > { return this.model.onDidChangeCollapseState; } constructor( user: string, container: HTMLElement, delegate: IListVirtualDelegate, renderers: ITreeRenderer[], options: IObjectTreeOptions = {} ) { super( user, container, delegate, renderers, options as IObjectTreeOptions ); } setChildren( element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions ): void { this.model.setChildren(element, children, options); } rerender(element?: T): void { if (element === undefined) { this.view.rerender(); return; } this.model.rerender(element); } hasElement(element: T): boolean { return this.model.has(element); } protected createModel( user: string, view: IList>, options: IObjectTreeOptions ): ITreeModel { return new ObjectTreeModel(user, view, options); } } interface ICompressedTreeNodeProvider { getCompressedTreeNode( location: T | null ): ITreeNode | null, TFilterData>; } export interface ICompressibleTreeRenderer< T, TFilterData = void, TTemplateData = void > 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< T extends NonNullable, TFilterData, TTemplateData > implements ITreeRenderer< T, TFilterData, CompressibleTemplateData > { readonly templateId: string; readonly onDidChangeTwistieState: Event | undefined; @memoize private get compressedTreeNodeProvider(): ICompressedTreeNodeProvider< T, TFilterData > { return this._compressedTreeNodeProvider(); } constructor( private _compressedTreeNodeProvider: () => ICompressedTreeNodeProvider< T, TFilterData >, 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 ) as ITreeNode, TFilterData>; 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): boolean { if (this.renderer.renderTwistie) { return this.renderer.renderTwistie(element, twistieElement); } return false; } } export interface ICompressibleKeyboardNavigationLabelProvider extends IKeyboardNavigationLabelProvider { getCompressedNodeKeyboardNavigationLabel( elements: T[] ): { toString(): string | undefined } | undefined; } export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider; } function asObjectTreeOptions( compressedTreeNodeProvider: () => ICompressedTreeNodeProvider, options?: ICompressibleObjectTreeOptions ): IObjectTreeOptions | undefined { return ( options && { ...options, keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { getKeyboardNavigationLabel(e: T) { let compressedTreeNode: ITreeNode< ICompressedTreeNode, TFilterData >; try { compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode( e ) as ITreeNode, TFilterData>; } catch { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel( e ); } if (compressedTreeNode.element.elements.length === 1) { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel( e ); } else { return options.keyboardNavigationLabelProvider!.getCompressedNodeKeyboardNavigationLabel( compressedTreeNode.element.elements ); } }, }, } ); } export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { readonly compressionEnabled?: boolean; } export class CompressibleObjectTree< T extends NonNullable, TFilterData = void > extends ObjectTree implements ICompressedTreeNodeProvider { protected override model!: CompressibleObjectTreeModel; constructor( user: string, container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], options: ICompressibleObjectTreeOptions = {} ) { const compressedTreeNodeProvider = () => this; const compressibleRenderers = renderers.map( (r) => new CompressibleRenderer( compressedTreeNodeProvider, r ) ); super( user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options) ); } override setChildren( element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions ): void { this.model.setChildren(element, children, options); } protected override createModel( user: string, view: IList>, options: ICompressibleObjectTreeOptions ): ITreeModel { return new CompressibleObjectTreeModel(user, view, options); } override updateOptions( optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {} ): void { super.updateOptions(optionsUpdate); if (typeof optionsUpdate.compressionEnabled !== 'undefined') { this.model.setCompressionEnabled(optionsUpdate.compressionEnabled); } } getCompressedTreeNode( element: T | null = null ): ITreeNode | null, TFilterData> { return this.model.getCompressedTreeNode(element); } }