Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree: Allow preserving preexisting element collapse state #176507

Merged
merged 1 commit into from Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/vs/base/browser/ui/tree/asyncDataTree.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -988,7 +988,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this._onDidRender.fire();
}

protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): IObjectTreeElement<IAsyncDataTreeNode<TInput, T>> {
if (node.stale) {
return {
element: node,
Expand Down
14 changes: 7 additions & 7 deletions src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts
Expand Up @@ -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<T> extends ITreeElement<T> {
export interface ICompressedTreeElement<T> extends IObjectTreeElement<T> {
readonly children?: Iterable<ICompressedTreeElement<T>>;
readonly incompressible?: boolean;
}
Expand All @@ -22,7 +22,7 @@ export interface ICompressedTreeNode<T> {
readonly incompressible: boolean;
}

function noCompress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
function noCompress<T>(element: ICompressedTreeElement<T>): ICompressedTreeElement<ICompressedTreeNode<T>> {
const elements = [element.element];
const incompressible = element.incompressible || false;

Expand All @@ -35,7 +35,7 @@ function noCompress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompre
}

// Exported only for test reasons, do not use directly
export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
export function compress<T>(element: ICompressedTreeElement<T>): ICompressedTreeElement<ICompressedTreeNode<T>> {
const elements = [element.element];
const incompressible = element.incompressible || false;

Expand Down Expand Up @@ -65,7 +65,7 @@ export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<IC
};
}

function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, index = 0): ICompressedTreeElement<T> {
function _decompress<T>(element: ICompressedTreeElement<ICompressedTreeNode<T>>, index = 0): ICompressedTreeElement<T> {
let children: Iterable<ICompressedTreeElement<T>>;

if (index < element.element.elements.length - 1) {
Expand Down Expand Up @@ -93,7 +93,7 @@ function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, index = 0
}

// Exported only for test reasons, do not use directly
export function decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>): ICompressedTreeElement<T> {
export function decompress<T>(element: ICompressedTreeElement<ICompressedTreeNode<T>>): ICompressedTreeElement<T> {
return _decompress(element, 0);
}

Expand Down Expand Up @@ -205,7 +205,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e

private _setChildren(
node: ICompressedTreeNode<T> | null,
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>,
children: Iterable<IObjectTreeElement<ICompressedTreeNode<T>>>,
options: IIndexTreeModelSpliceOptions<ICompressedTreeNode<T>, TFilterData>,
): void {
const insertedElements = new Set<T | null>();
Expand Down
4 changes: 2 additions & 2 deletions src/vs/base/browser/ui/tree/objectTree.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -52,7 +52,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
super(user, container, delegate, renderers, options as IObjectTreeOptions<T | null, TFilterData>);
}

setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
setChildren(element: T | null, children: Iterable<IObjectTreeElement<T>> = Iterable.empty(), options?: IObjectTreeSetChildrenOptions<T>): void {
this.model.setChildren(element, children, options);
}

Expand Down
35 changes: 29 additions & 6 deletions src/vs/base/browser/ui/tree/objectTreeModel.ts
Expand Up @@ -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<T, TFilterData> = (node: ITreeNode<T, TFilterData>) => void;

export interface IObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> extends ITreeModel<T | null, TFilterData, T | null> {
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined, options?: IObjectTreeModelSetChildrenOptions<T, TFilterData>): void;
setChildren(element: T | null, children: Iterable<IObjectTreeElement<T>> | undefined, options?: IObjectTreeModelSetChildrenOptions<T, TFilterData>): void;
resort(element?: T | null, recursive?: boolean): void;
updateElementHeight(element: T, height: number | undefined): void;
}
Expand Down Expand Up @@ -64,7 +64,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non

setChildren(
element: T | null,
children: Iterable<ITreeElement<T>> = Iterable.empty(),
children: Iterable<IObjectTreeElement<T>> = Iterable.empty(),
options: IObjectTreeModelSetChildrenOptions<T, TFilterData> = {},
): void {
const location = this.getElementLocation(element);
Expand Down Expand Up @@ -127,7 +127,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
);
}

private preserveCollapseState(elements: Iterable<ITreeElement<T>> = Iterable.empty()): Iterable<ITreeElement<T>> {
private preserveCollapseState(elements: Iterable<IObjectTreeElement<T>> = Iterable.empty()): Iterable<ITreeElement<T>> {
if (this.sorter) {
elements = [...elements].sort(this.sorter.compare.bind(this.sorter));
}
Expand All @@ -141,14 +141,37 @@ export class ObjectTreeModel<T extends NonNullable<any>, 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,
Expand Down
22 changes: 22 additions & 0 deletions src/vs/base/browser/ui/tree/tree.ts
Expand Up @@ -78,6 +78,28 @@ export interface ITreeElement<T> {
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<T> {
readonly element: T;
readonly children?: Iterable<IObjectTreeElement<T>>;
readonly collapsible?: boolean;
readonly collapsed?: boolean | ObjectTreeElementCollapseState;
}

export interface ITreeNode<T, TFilterData = void> {
readonly element: T;
readonly children: ITreeNode<T, TFilterData>[];
Expand Down
23 changes: 22 additions & 1 deletion src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts
Expand Up @@ -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<T>(arr: T[]): IList<T> {
Expand Down Expand Up @@ -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<string>[] = [];
const model = new ObjectTreeModel<string>('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;

Expand Down