Skip to content

Commit f840784

Browse files
authored
improve async data tree default collapse state handling (microsoft#199937)
* improve async data tree default collapsed state computation fixes microsoft#199441 * unset forceExpanded flag
1 parent df42214 commit f840784

File tree

2 files changed

+79
-22
lines changed

2 files changed

+79
-22
lines changed

src/vs/base/browser/ui/tree/asyncDataTree.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOption
1111
import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
1212
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
1313
import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
14-
import { IAsyncDataSource, ICollapseStateChangeEvent, IObjectTreeElement, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeMouseEvent, ITreeNode, ITreeRenderer, ITreeSorter, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree';
14+
import { IAsyncDataSource, ICollapseStateChangeEvent, IObjectTreeElement, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeMouseEvent, ITreeNode, ITreeRenderer, ITreeSorter, ObjectTreeElementCollapseState, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree';
1515
import { CancelablePromise, createCancelablePromise, Promises, timeout } from 'vs/base/common/async';
1616
import { Codicon } from 'vs/base/common/codicons';
1717
import { ThemeIcon } from 'vs/base/common/themables';
@@ -31,13 +31,15 @@ interface IAsyncDataTreeNode<TInput, T> {
3131
hasChildren: boolean;
3232
stale: boolean;
3333
slow: boolean;
34-
collapsedByDefault: boolean | undefined;
34+
readonly defaultCollapseState: undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded;
35+
forceExpanded: boolean;
3536
}
3637

3738
interface IAsyncDataTreeNodeRequiredProps<TInput, T> extends Partial<IAsyncDataTreeNode<TInput, T>> {
3839
readonly element: TInput | T;
3940
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
4041
readonly hasChildren: boolean;
42+
readonly defaultCollapseState: undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded;
4143
}
4244

4345
function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredProps<TInput, T>): IAsyncDataTreeNode<TInput, T> {
@@ -47,7 +49,7 @@ function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredPro
4749
refreshPromise: undefined,
4850
stale: true,
4951
slow: false,
50-
collapsedByDefault: undefined
52+
forceExpanded: false
5153
};
5254
}
5355

@@ -321,7 +323,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
321323
protected readonly root: IAsyncDataTreeNode<TInput, T>;
322324
private readonly nodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
323325
private readonly sorter?: ITreeSorter<T>;
324-
private readonly collapseByDefault?: { (e: T): boolean };
326+
private readonly getDefaultCollapseState: { (e: T): undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded };
325327

326328
private readonly subTreeRefreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
327329
private readonly refreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, CancelablePromise<Iterable<T>>>();
@@ -387,15 +389,16 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
387389
this.identityProvider = options.identityProvider;
388390
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
389391
this.sorter = options.sorter;
390-
this.collapseByDefault = options.collapseByDefault;
392+
this.getDefaultCollapseState = e => options.collapseByDefault ? (options.collapseByDefault(e) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded) : undefined;
391393

392394
this.tree = this.createTree(user, container, delegate, renderers, options);
393395
this.onDidChangeFindMode = this.tree.onDidChangeFindMode;
394396

395397
this.root = createAsyncDataTreeNode({
396398
element: undefined!,
397399
parent: null,
398-
hasChildren: true
400+
hasChildren: true,
401+
defaultCollapseState: undefined
399402
});
400403

401404
if (this.identityProvider) {
@@ -932,10 +935,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
932935
const hasChildren = !!this.dataSource.hasChildren(element);
933936

934937
if (!this.identityProvider) {
935-
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren });
938+
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
936939

937-
if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
938-
asyncDataTreeNode.collapsedByDefault = false;
940+
if (hasChildren && asyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
939941
childrenToRefresh.push(asyncDataTreeNode);
940942
}
941943

@@ -963,15 +965,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
963965
} else {
964966
childrenToRefresh.push(asyncDataTreeNode);
965967
}
966-
} else if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
967-
asyncDataTreeNode.collapsedByDefault = false;
968+
} else if (hasChildren && !result.collapsed) {
968969
childrenToRefresh.push(asyncDataTreeNode);
969970
}
970971

971972
return asyncDataTreeNode;
972973
}
973974

974-
const childAsyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, id, hasChildren });
975+
const childAsyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, id, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
975976

976977
if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
977978
viewStateContext.focus.push(childAsyncDataTreeNode);
@@ -983,8 +984,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
983984

984985
if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
985986
childrenToRefresh.push(childAsyncDataTreeNode);
986-
} else if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
987-
childAsyncDataTreeNode.collapsedByDefault = false;
987+
} else if (hasChildren && childAsyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
988988
childrenToRefresh.push(childAsyncDataTreeNode);
989989
}
990990

@@ -1003,7 +1003,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
10031003

10041004
// TODO@joao this doesn't take filter into account
10051005
if (node !== this.root && this.autoExpandSingleChildren && children.length === 1 && childrenToRefresh.length === 0) {
1006-
children[0].collapsedByDefault = false;
1006+
children[0].forceExpanded = true;
10071007
childrenToRefresh.push(children[0]);
10081008
}
10091009

@@ -1039,16 +1039,17 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
10391039
};
10401040
}
10411041

1042-
let collapsed: boolean | undefined;
1042+
let collapsed: boolean | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded | undefined;
10431043

10441044
if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
10451045
collapsed = false;
1046+
} else if (node.forceExpanded) {
1047+
collapsed = false;
1048+
node.forceExpanded = false;
10461049
} else {
1047-
collapsed = node.collapsedByDefault;
1050+
collapsed = node.defaultCollapseState;
10481051
}
10491052

1050-
node.collapsedByDefault = undefined;
1051-
10521053
return {
10531054
element: node,
10541055
children: node.hasChildren ? Iterable.map(node.children, child => this.asTreeElement(child, viewStateContext)) : [],

src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
import * as assert from 'assert';
77
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
8-
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
9-
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
8+
import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
9+
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
10+
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
11+
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
1012
import { timeout } from 'vs/base/common/async';
1113
import { Iterable } from 'vs/base/common/iterator';
1214
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
@@ -37,7 +39,7 @@ function find(element: Element, id: string): Element | undefined {
3739
return undefined;
3840
}
3941

40-
class Renderer implements ITreeRenderer<Element, void, HTMLElement> {
42+
class Renderer implements ICompressibleTreeRenderer<Element, void, HTMLElement> {
4143
readonly templateId = 'default';
4244
renderTemplate(container: HTMLElement): HTMLElement {
4345
return container;
@@ -48,6 +50,15 @@ class Renderer implements ITreeRenderer<Element, void, HTMLElement> {
4850
disposeTemplate(templateData: HTMLElement): void {
4951
// noop
5052
}
53+
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<Element>, void>, index: number, templateData: HTMLElement, height: number | undefined): void {
54+
const result: string[] = [];
55+
56+
for (const element of node.element.elements) {
57+
result.push(element.id + (element.suffix || ''));
58+
}
59+
60+
templateData.textContent = result.join('/');
61+
}
5162
}
5263

5364
class IdentityProvider implements IIdentityProvider<Element> {
@@ -495,4 +506,49 @@ suite('AsyncDataTree', function () {
495506
assert(tree.isCollapsible(a), 'a is still collapsible');
496507
assert(!tree.isCollapsed(a), 'a is expanded');
497508
});
509+
510+
test('issue #199441', async () => {
511+
const container = document.createElement('div');
512+
513+
const dataSource = new class implements IAsyncDataSource<Element, Element> {
514+
hasChildren(element: Element): boolean {
515+
return !!element.children && element.children.length > 0;
516+
}
517+
async getChildren(element: Element) {
518+
return element.children ?? Iterable.empty();
519+
}
520+
};
521+
522+
const compressionDelegate = new class implements ITreeCompressionDelegate<Element> {
523+
isIncompressible(element: Element): boolean {
524+
return !dataSource.hasChildren(element);
525+
}
526+
};
527+
528+
const model = new Model({
529+
id: 'root',
530+
children: [{
531+
id: 'a', children: [{
532+
id: 'b',
533+
children: [{ id: 'b.txt' }]
534+
}]
535+
}]
536+
});
537+
538+
const collapseByDefault = (element: Element) => false;
539+
540+
const tree = store.add(new CompressibleAsyncDataTree<Element, Element>('test', container, new VirtualDelegate(), compressionDelegate, [new Renderer()], dataSource, { identityProvider: new IdentityProvider(), collapseByDefault }));
541+
tree.layout(200);
542+
543+
await tree.setInput(model.root);
544+
assert.deepStrictEqual(Array.from(container.querySelectorAll('.monaco-list-row')).map(e => e.textContent), ['a/b', 'b.txt']);
545+
546+
model.get('a').children!.push({
547+
id: 'c',
548+
children: [{ id: 'c.txt' }]
549+
});
550+
551+
await tree.updateChildren(model.root, true);
552+
assert.deepStrictEqual(Array.from(container.querySelectorAll('.monaco-list-row')).map(e => e.textContent), ['a', 'b', 'b.txt', 'c', 'c.txt']);
553+
});
498554
});

0 commit comments

Comments
 (0)