diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 7c2564c5056f5..e996ae8bc2105 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -11,7 +11,7 @@ import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOption import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel'; import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; -import { IAsyncDataSource, ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeElement, ITreeEvent, ITreeFilter, ITreeMouseEvent, ITreeNode, ITreeRenderer, ITreeSorter, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; +import { IAsyncDataSource, ICollapseStateChangeEvent, IObjectTreeElement, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeMouseEvent, ITreeNode, ITreeRenderer, ITreeSorter, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { CancelablePromise, createCancelablePromise, Promises, timeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -988,7 +988,7 @@ export class AsyncDataTree implements IDisposable this._onDidRender.fire(); } - protected asTreeElement(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): ITreeElement> { + protected asTreeElement(node: IAsyncDataTreeNode, viewStateContext?: IAsyncDataTreeViewStateContext): IObjectTreeElement> { if (node.stale) { return { element: node, diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 8ea45f5e3e585..241c26daf23e1 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -6,12 +6,12 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { IIndexTreeModelSpliceOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { IObjectTreeModel, IObjectTreeModelOptions, IObjectTreeModelSetChildrenOptions, ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { ICollapseStateChangeEvent, ITreeElement, ITreeModel, ITreeModelSpliceEvent, ITreeNode, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; +import { ICollapseStateChangeEvent, IObjectTreeElement, ITreeModel, ITreeModelSpliceEvent, ITreeNode, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; // Exported only for test reasons, do not use directly -export interface ICompressedTreeElement extends ITreeElement { +export interface ICompressedTreeElement extends IObjectTreeElement { readonly children?: Iterable>; readonly incompressible?: boolean; } @@ -22,7 +22,7 @@ export interface ICompressedTreeNode { readonly incompressible: boolean; } -function noCompress(element: ICompressedTreeElement): ITreeElement> { +function noCompress(element: ICompressedTreeElement): ICompressedTreeElement> { const elements = [element.element]; const incompressible = element.incompressible || false; @@ -35,7 +35,7 @@ function noCompress(element: ICompressedTreeElement): ITreeElement(element: ICompressedTreeElement): ITreeElement> { +export function compress(element: ICompressedTreeElement): ICompressedTreeElement> { const elements = [element.element]; const incompressible = element.incompressible || false; @@ -65,7 +65,7 @@ export function compress(element: ICompressedTreeElement): ITreeElement(element: ITreeElement>, index = 0): ICompressedTreeElement { +function _decompress(element: ICompressedTreeElement>, index = 0): ICompressedTreeElement { let children: Iterable>; if (index < element.element.elements.length - 1) { @@ -93,7 +93,7 @@ function _decompress(element: ITreeElement>, index = 0 } // Exported only for test reasons, do not use directly -export function decompress(element: ITreeElement>): ICompressedTreeElement { +export function decompress(element: ICompressedTreeElement>): ICompressedTreeElement { return _decompress(element, 0); } @@ -205,7 +205,7 @@ export class CompressedObjectTreeModel, TFilterData e private _setChildren( node: ICompressedTreeNode | null, - children: Iterable>>, + children: Iterable>>, options: IIndexTreeModelSpliceOptions, TFilterData>, ): void { const insertedElements = new Set(); diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 86aca57ac40fc..5880b0bb25dae 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -8,7 +8,7 @@ import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from ' import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { IObjectTreeModel, ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { ICollapseStateChangeEvent, ITreeElement, ITreeModel, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { ICollapseStateChangeEvent, IObjectTreeElement, ITreeModel, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { memoize } from 'vs/base/common/decorators'; import { Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; @@ -52,7 +52,7 @@ export class ObjectTree, TFilterData = void> extends super(user, container, delegate, renderers, options as IObjectTreeOptions); } - setChildren(element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions): void { + setChildren(element: T | null, children: Iterable> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions): void { this.model.setChildren(element, children, options); } diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index fe44f7e91c7d5..919f14b2b3ae7 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -5,14 +5,14 @@ import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { IIndexTreeModelOptions, IIndexTreeModelSpliceOptions, IList, IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; -import { ICollapseStateChangeEvent, ITreeElement, ITreeModel, ITreeModelSpliceEvent, ITreeNode, ITreeSorter, TreeError } from 'vs/base/browser/ui/tree/tree'; +import { ICollapseStateChangeEvent, IObjectTreeElement, ITreeElement, ITreeModel, ITreeModelSpliceEvent, ITreeNode, ITreeSorter, ObjectTreeElementCollapseState, TreeError } from 'vs/base/browser/ui/tree/tree'; import { Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; export type ITreeNodeCallback = (node: ITreeNode) => void; export interface IObjectTreeModel, TFilterData extends NonNullable = void> extends ITreeModel { - setChildren(element: T | null, children: Iterable> | undefined, options?: IObjectTreeModelSetChildrenOptions): void; + setChildren(element: T | null, children: Iterable> | undefined, options?: IObjectTreeModelSetChildrenOptions): void; resort(element?: T | null, recursive?: boolean): void; updateElementHeight(element: T, height: number | undefined): void; } @@ -64,7 +64,7 @@ export class ObjectTreeModel, TFilterData extends Non setChildren( element: T | null, - children: Iterable> = Iterable.empty(), + children: Iterable> = Iterable.empty(), options: IObjectTreeModelSetChildrenOptions = {}, ): void { const location = this.getElementLocation(element); @@ -127,7 +127,7 @@ export class ObjectTreeModel, TFilterData extends Non ); } - private preserveCollapseState(elements: Iterable> = Iterable.empty()): Iterable> { + private preserveCollapseState(elements: Iterable> = Iterable.empty()): Iterable> { if (this.sorter) { elements = [...elements].sort(this.sorter.compare.bind(this.sorter)); } @@ -141,14 +141,37 @@ export class ObjectTreeModel, TFilterData extends Non } if (!node) { + let collapsed: boolean | undefined; + + if (typeof treeElement.collapsed === 'undefined') { + collapsed = undefined; + } else if (treeElement.collapsed === ObjectTreeElementCollapseState.Collapsed || treeElement.collapsed === ObjectTreeElementCollapseState.PreserveOrCollapsed) { + collapsed = true; + } else if (treeElement.collapsed === ObjectTreeElementCollapseState.Expanded || treeElement.collapsed === ObjectTreeElementCollapseState.PreserveOrExpanded) { + collapsed = false; + } else { + collapsed = Boolean(treeElement.collapsed); + } + return { ...treeElement, - children: this.preserveCollapseState(treeElement.children) + children: this.preserveCollapseState(treeElement.children), + collapsed }; } const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible; - const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : node.collapsed; + let collapsed: boolean | undefined; + + if (typeof treeElement.collapsed === 'undefined' || treeElement.collapsed === ObjectTreeElementCollapseState.PreserveOrCollapsed || treeElement.collapsed === ObjectTreeElementCollapseState.PreserveOrExpanded) { + collapsed = node.collapsed; + } else if (treeElement.collapsed === ObjectTreeElementCollapseState.Collapsed) { + collapsed = true; + } else if (treeElement.collapsed === ObjectTreeElementCollapseState.Expanded) { + collapsed = false; + } else { + collapsed = Boolean(treeElement.collapsed); + } return { ...treeElement, diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index d9826115a57e9..94e8650f2defa 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -78,6 +78,28 @@ export interface ITreeElement { readonly collapsed?: boolean; } +export enum ObjectTreeElementCollapseState { + Expanded, + Collapsed, + + /** + * If the element is already in the tree, preserve its current state. Else, expand it. + */ + PreserveOrExpanded, + + /** + * If the element is already in the tree, preserve its current state. Else, collapse it. + */ + PreserveOrCollapsed, +} + +export interface IObjectTreeElement { + readonly element: T; + readonly children?: Iterable>; + readonly collapsible?: boolean; + readonly collapsed?: boolean | ObjectTreeElementCollapseState; +} + export interface ITreeNode { readonly element: T; readonly children: ITreeNode[]; diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index df96ed6840901..2b9917e4ef972 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { ITreeFilter, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; +import { ITreeFilter, ITreeNode, ObjectTreeElementCollapseState, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { timeout } from 'vs/base/common/async'; function toList(arr: T[]): IList { @@ -171,6 +171,27 @@ suite('ObjectTreeModel', function () { assert.deepStrictEqual(toArray(list), ['father']); }); + test('collapse state can be optionally preserved with strict identity', () => { + const list: ITreeNode[] = []; + const model = new ObjectTreeModel('test', toList(list), { collapseByDefault: true }); + const data = [{ element: 'father', collapsed: ObjectTreeElementCollapseState.PreserveOrExpanded, children: [{ element: 'child' }] }]; + + model.setChildren(null, data); + assert.deepStrictEqual(toArray(list), ['father', 'child']); + + model.setCollapsed('father', true); + assert.deepStrictEqual(toArray(list), ['father']); + + model.setChildren(null, data); + assert.deepStrictEqual(toArray(list), ['father']); + + model.setCollapsed('father', false); + assert.deepStrictEqual(toArray(list), ['father', 'child']); + + model.setChildren(null, data); + assert.deepStrictEqual(toArray(list), ['father', 'child']); + }); + test('sorter', () => { const compare: (a: string, b: string) => number = (a, b) => a < b ? -1 : 1;