Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions src/vs/base/browser/ui/tree/asyncDataTree.ts
Original file line number Diff line number Diff line change
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, IObjectTreeElement, ITreeContextMenuEvent, ITreeDragAndDrop, 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, ObjectTreeElementCollapseState, 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 All @@ -31,13 +31,15 @@ interface IAsyncDataTreeNode<TInput, T> {
hasChildren: boolean;
stale: boolean;
slow: boolean;
collapsedByDefault: boolean | undefined;
readonly defaultCollapseState: undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded;
forceExpanded: boolean;
}

interface IAsyncDataTreeNodeRequiredProps<TInput, T> extends Partial<IAsyncDataTreeNode<TInput, T>> {
readonly element: TInput | T;
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
readonly hasChildren: boolean;
readonly defaultCollapseState: undefined | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded;
}

function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredProps<TInput, T>): IAsyncDataTreeNode<TInput, T> {
Expand All @@ -47,7 +49,7 @@ function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredPro
refreshPromise: undefined,
stale: true,
slow: false,
collapsedByDefault: undefined
forceExpanded: false
};
}

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

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

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

this.root = createAsyncDataTreeNode({
element: undefined!,
parent: null,
hasChildren: true
hasChildren: true,
defaultCollapseState: undefined
});

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

if (!this.identityProvider) {
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren });
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });

if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
asyncDataTreeNode.collapsedByDefault = false;
if (hasChildren && asyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
childrenToRefresh.push(asyncDataTreeNode);
}

Expand Down Expand Up @@ -966,15 +968,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
} else {
childrenToRefresh.push(asyncDataTreeNode);
}
} else if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
asyncDataTreeNode.collapsedByDefault = false;
} else if (hasChildren && !result.collapsed) {
childrenToRefresh.push(asyncDataTreeNode);
}

return asyncDataTreeNode;
}

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

if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
viewStateContext.focus.push(childAsyncDataTreeNode);
Expand All @@ -986,8 +987,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable

if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
childrenToRefresh.push(childAsyncDataTreeNode);
} else if (hasChildren && this.collapseByDefault && !this.collapseByDefault(element)) {
childAsyncDataTreeNode.collapsedByDefault = false;
} else if (hasChildren && childAsyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
childrenToRefresh.push(childAsyncDataTreeNode);
}

Expand All @@ -1006,7 +1006,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable

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

Expand Down Expand Up @@ -1042,16 +1042,17 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
};
}

let collapsed: boolean | undefined;
let collapsed: boolean | ObjectTreeElementCollapseState.PreserveOrCollapsed | ObjectTreeElementCollapseState.PreserveOrExpanded | undefined;

if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
collapsed = false;
} else if (node.forceExpanded) {
collapsed = false;
node.forceExpanded = false;
} else {
collapsed = node.collapsedByDefault;
collapsed = node.defaultCollapseState;
}

node.collapsedByDefault = undefined;

return {
element: node,
children: node.hasChildren ? Iterable.map(node.children, child => this.asTreeElement(child, viewStateContext)) : [],
Expand Down
62 changes: 59 additions & 3 deletions src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import * as assert from 'assert';
import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
import { timeout } from 'vs/base/common/async';
import { Iterable } from 'vs/base/common/iterator';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
Expand Down Expand Up @@ -37,7 +39,7 @@ function find(element: Element, id: string): Element | undefined {
return undefined;
}

class Renderer implements ITreeRenderer<Element, void, HTMLElement> {
class Renderer implements ICompressibleTreeRenderer<Element, void, HTMLElement> {
readonly templateId = 'default';
renderTemplate(container: HTMLElement): HTMLElement {
return container;
Expand All @@ -48,6 +50,15 @@ class Renderer implements ITreeRenderer<Element, void, HTMLElement> {
disposeTemplate(templateData: HTMLElement): void {
// noop
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<Element>, void>, index: number, templateData: HTMLElement, height: number | undefined): void {
const result: string[] = [];

for (const element of node.element.elements) {
result.push(element.id + (element.suffix || ''));
}

templateData.textContent = result.join('/');
}
}

class IdentityProvider implements IIdentityProvider<Element> {
Expand Down Expand Up @@ -520,4 +531,49 @@ suite('AsyncDataTree', function () {
assert(tree.isCollapsible(a), 'a is still collapsible');
assert(!tree.isCollapsed(a), 'a is expanded');
});

test('issue #199441', async () => {
const container = document.createElement('div');

const dataSource = new class implements IAsyncDataSource<Element, Element> {
hasChildren(element: Element): boolean {
return !!element.children && element.children.length > 0;
}
async getChildren(element: Element) {
return element.children ?? Iterable.empty();
}
};

const compressionDelegate = new class implements ITreeCompressionDelegate<Element> {
isIncompressible(element: Element): boolean {
return !dataSource.hasChildren(element);
}
};

const model = new Model({
id: 'root',
children: [{
id: 'a', children: [{
id: 'b',
children: [{ id: 'b.txt' }]
}]
}]
});

const collapseByDefault = (element: Element) => false;

const tree = store.add(new CompressibleAsyncDataTree<Element, Element>('test', container, new VirtualDelegate(), compressionDelegate, [new Renderer()], dataSource, { identityProvider: new IdentityProvider(), collapseByDefault }));
tree.layout(200);

await tree.setInput(model.root);
assert.deepStrictEqual(Array.from(container.querySelectorAll('.monaco-list-row')).map(e => e.textContent), ['a/b', 'b.txt']);

model.get('a').children!.push({
id: 'c',
children: [{ id: 'c.txt' }]
});

await tree.updateChildren(model.root, true);
assert.deepStrictEqual(Array.from(container.querySelectorAll('.monaco-list-row')).map(e => e.textContent), ['a', 'b', 'b.txt', 'c', 'c.txt']);
});
});