Tree: Allow preserving preexisting element collapse state (#176507)

fixes #176505
This commit is contained in:
João Moreno
2023-03-08 15:09:39 +01:00
committed by GitHub
parent 0e14de63e8
commit fb17fb314a
6 changed files with 84 additions and 18 deletions
+2 -2
View File
@@ -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<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,
@@ -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;
}
@@ -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;
@@ -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;
@@ -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) {
@@ -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);
}
@@ -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>();
+2 -2
View File
@@ -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<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);
}
+29 -6
View File
@@ -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;
}
@@ -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);
@@ -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));
}
@@ -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,
+22
View File
@@ -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>[];
@@ -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> {
@@ -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;