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

Explorer: Use compressed tree widget #83978

Merged
merged 16 commits into from
Nov 6, 2019
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
1 change: 0 additions & 1 deletion src/vs/base/browser/ui/tree/abstractTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,6 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
readonly collapseByDefault?: boolean; // defaults to false
readonly filter?: ITreeFilter<T, TFilterData>;
readonly dnd?: ITreeDragAndDrop<T>;
readonly autoExpandSingleChildren?: boolean;
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean);
readonly additionalScrollHeight?: number;
Expand Down
102 changes: 69 additions & 33 deletions src/vs/base/browser/ui/tree/asyncDataTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,6 @@ function asTreeContextMenuEvent<TInput, T>(e: ITreeContextMenuEvent<IAsyncDataTr
};
}

export enum ChildrenResolutionReason {
Refresh,
Expand
}

export interface IChildrenResolutionEvent<T> {
readonly element: T | null;
readonly reason: ChildrenResolutionReason;
}

function asAsyncDataTreeDragAndDropData<TInput, T>(data: IDragAndDropData): IDragAndDropData {
if (data instanceof ElementsDragAndDropData) {
const nodes = (data as ElementsDragAndDropData<IAsyncDataTreeNode<TInput, T>>).elements;
Expand Down Expand Up @@ -273,7 +263,7 @@ function dfs<TInput, T>(node: IAsyncDataTreeNode<TInput, T>, fn: (node: IAsyncDa

export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable {

private readonly tree: ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
protected readonly tree: ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
private readonly root: IAsyncDataTreeNode<TInput, T>;
private readonly nodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
private readonly sorter?: ITreeSorter<T>;
Expand All @@ -282,7 +272,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
private readonly subTreeRefreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
private readonly refreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, CancelablePromise<T[]>>();

private readonly identityProvider?: IIdentityProvider<T>;
protected readonly identityProvider?: IIdentityProvider<T>;
private readonly autoExpandSingleChildren: boolean;

private readonly _onDidRender = new Emitter<void>();
Expand Down Expand Up @@ -323,7 +313,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
get onDidDispose(): Event<void> { return this.tree.onDidDispose; }

constructor(
private user: string,
protected user: string,
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<T, TFilterData, any>[],
Expand Down Expand Up @@ -471,7 +461,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
await Event.toPromise(this._onDidRender.event);
}

await this.refreshAndRenderNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh, viewStateContext);
await this.refreshAndRenderNode(this.getDataNode(element), recursive, viewStateContext);
}

resort(element: TInput | T = this.root.element, recursive = true): void {
Expand Down Expand Up @@ -653,18 +643,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return node;
}

private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, reason: ChildrenResolutionReason, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async refreshAndRenderNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
await this.refreshNode(node, recursive, viewStateContext);
this.render(node, viewStateContext);

if (node !== this.root && this.autoExpandSingleChildren && reason === ChildrenResolutionReason.Expand) {
const treeNode = this.tree.getNode(node);
const visibleChildren = treeNode.children.filter(node => node.visible);

if (visibleChildren.length === 1) {
await this.tree.expand(visibleChildren[0].element, false);
}
}
}

private async refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
Expand Down Expand Up @@ -770,7 +751,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
if (deep) {
this.collapse(node.element.element as T);
} else {
this.refreshAndRenderNode(node.element, false, ChildrenResolutionReason.Expand)
this.refreshAndRenderNode(node.element, false)
.catch(onUnexpectedError);
}
}
Expand All @@ -783,13 +764,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}

const nodesToForget = new Map<T, IAsyncDataTreeNode<TInput, T>>();
const childrenTreeNodesById = new Map<string, ITreeNode<IAsyncDataTreeNode<TInput, T> | null, TFilterData>>();
const childrenTreeNodesById = new Map<string, { node: IAsyncDataTreeNode<TInput, T>, collapsed: boolean }>();

for (const child of node.children) {
nodesToForget.set(child.element as T, child);

if (this.identityProvider) {
childrenTreeNodesById.set(child.id!, this.tree.getNode(child));
const collapsed = this.tree.isCollapsed(child);
childrenTreeNodesById.set(child.id!, { node: child, collapsed });
}
}

Expand All @@ -810,10 +792,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}

const id = this.identityProvider.getId(element).toString();
const childNode = childrenTreeNodesById.get(id);
const result = childrenTreeNodesById.get(id);

if (childNode) {
const asyncDataTreeNode = childNode.element!;
if (result) {
const asyncDataTreeNode = result.node;

nodesToForget.delete(asyncDataTreeNode.element as T);
this.nodes.delete(asyncDataTreeNode.element as T);
Expand All @@ -823,8 +805,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
asyncDataTreeNode.hasChildren = hasChildren;

if (recursive) {
if (childNode.collapsed) {
dfs(asyncDataTreeNode, node => node.stale = true);
if (result.collapsed) {
asyncDataTreeNode.children.forEach(node => dfs(node, node => this.nodes.delete(node.element as T)));
asyncDataTreeNode.children.splice(0, asyncDataTreeNode.children.length);
asyncDataTreeNode.stale = true;
} else {
childrenToRefresh.push(asyncDataTreeNode);
}
Expand Down Expand Up @@ -866,6 +850,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable

node.children.splice(0, node.children.length, ...children);

// 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;
childrenToRefresh.push(children[0]);
}

return childrenToRefresh;
}

Expand All @@ -881,6 +871,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}

protected asTreeElement(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
if (node.stale) {
return {
element: node,
collapsible: node.hasChildren,
collapsed: true
};
}

let collapsed: boolean | undefined;

if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
Expand Down Expand Up @@ -1023,11 +1021,17 @@ function asCompressibleObjectTreeOptions<TInput, T, TFilterData>(options?: IComp
}

export interface ICompressibleAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncDataTreeOptions<T, TFilterData> {
readonly compressionEnabled?: boolean;
readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider<T>;
}

export interface ICompressibleAsyncDataTreeOptionsUpdate extends IAsyncDataTreeOptionsUpdate {
readonly compressionEnabled?: boolean;
}

export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends AsyncDataTree<TInput, T, TFilterData> {

protected readonly tree: CompressibleObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper<TInput, T, TFilterData> = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node));

constructor(
Expand All @@ -1037,7 +1041,7 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
private compressionDelegate: ITreeCompressionDelegate<T>,
renderers: ICompressibleTreeRenderer<T, TFilterData, any>[],
dataSource: IAsyncDataSource<TInput, T>,
options: IAsyncDataTreeOptions<T, TFilterData> = {}
options: ICompressibleAsyncDataTreeOptions<T, TFilterData> = {}
) {
super(user, container, virtualDelegate, renderers, dataSource, options);
}
Expand All @@ -1062,4 +1066,36 @@ export class CompressibleAsyncDataTree<TInput, T, TFilterData = void> extends As
...super.asTreeElement(node, viewStateContext)
};
}

updateOptions(options: ICompressibleAsyncDataTreeOptionsUpdate = {}): void {
this.tree.updateOptions(options);
}

getViewState(): IAsyncDataTreeViewState {
if (!this.identityProvider) {
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');
}

const getId = (element: T) => this.identityProvider!.getId(element).toString();
const focus = this.getFocus().map(getId);
const selection = this.getSelection().map(getId);

const expanded: string[] = [];
const root = this.tree.getCompressedTreeNode();
const queue = [root];

while (queue.length > 0) {
const node = queue.shift()!;

if (node !== root && node.collapsible && !node.collapsed) {
for (const asyncNode of node.element!.elements) {
expanded.push(getId(asyncNode.element as T));
}
}

queue.push(...node.children);
}

return { focus, selection, expanded, scrollTop: this.scrollTop };
}
}
19 changes: 10 additions & 9 deletions src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,15 @@ export function decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>): IC

function splice<T>(treeElement: ICompressedTreeElement<T>, element: T, children: Iterator<ICompressedTreeElement<T>>): ICompressedTreeElement<T> {
if (treeElement.element === element) {
return { element, children };
return { ...treeElement, children };
}

return {
...treeElement,
children: Iterator.map(Iterator.from(treeElement.children), e => splice(e, element, children))
};
return { ...treeElement, children: Iterator.map(Iterator.from(treeElement.children), e => splice(e, element, children)) };
}

interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<ICompressedTreeNode<T>, TFilterData> { }
interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<ICompressedTreeNode<T>, TFilterData> {
readonly compressionEnabled?: boolean;
}

// Exported only for test reasons, do not use directly
export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
Expand All @@ -122,7 +121,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e

private model: ObjectTreeModel<ICompressedTreeNode<T>, TFilterData>;
private nodes = new Map<T | null, ICompressedTreeNode<T>>();
private enabled: boolean = true;
private enabled: boolean;

get size(): number { return this.nodes.size; }

Expand All @@ -132,6 +131,7 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
options: ICompressedObjectTreeModelOptions<T, TFilterData> = {}
) {
this.model = new ObjectTreeModel(user, list, options);
this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled;
}

setChildren(
Expand Down Expand Up @@ -368,6 +368,7 @@ function mapOptions<T, TFilterData>(compressedNodeUnwrapper: CompressedNodeUnwra
}

export interface ICompressibleObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<T, TFilterData> {
readonly compressionEnabled?: boolean;
readonly elementMapper?: ElementMapper<T>;
}

Expand Down Expand Up @@ -493,7 +494,7 @@ export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData
return this.model.resort(element, recursive);
}

getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData> {
return this.model.getNode(element) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
getCompressedTreeNode(location: T | null = null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData> {
return this.model.getNode(location);
}
}
31 changes: 18 additions & 13 deletions src/vs/base/browser/ui/tree/objectTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { ISequence } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
Expand Down Expand Up @@ -56,7 +56,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
}

interface ICompressedTreeNodeProvider<T, TFilterData> {
getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData>;
getCompressedTreeNode(location: T | null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData>;
}

export interface ICompressibleTreeRenderer<T, TFilterData = void, TTemplateData = void> extends ITreeRenderer<T, TFilterData, TTemplateData> {
Expand All @@ -69,7 +69,7 @@ interface CompressibleTemplateData<T, TFilterData, TTemplateData> {
readonly data: TTemplateData;
}

class CompressibleRenderer<T, TFilterData, TTemplateData> implements ITreeRenderer<T, TFilterData, CompressibleTemplateData<T, TFilterData, TTemplateData>> {
class CompressibleRenderer<T extends NonNullable<any>, TFilterData, TTemplateData> implements ITreeRenderer<T, TFilterData, CompressibleTemplateData<T, TFilterData, TTemplateData>> {

readonly templateId: string;
readonly onDidChangeTwistieState: Event<T> | undefined;
Expand All @@ -93,7 +93,7 @@ class CompressibleRenderer<T, TFilterData, TTemplateData> implements ITreeRender
}

renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>, height: number | undefined): void {
const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element);
const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;

if (compressedTreeNode.element.elements.length === 1) {
templateData.compressedTreeNode = undefined;
Expand Down Expand Up @@ -132,6 +132,7 @@ export interface ICompressibleKeyboardNavigationLabelProvider<T> extends IKeyboa
}

export interface ICompressibleObjectTreeOptions<T, TFilterData = void> extends IObjectTreeOptions<T, TFilterData> {
readonly compressionEnabled?: boolean;
readonly elementMapper?: ElementMapper<T>;
readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider<T>;
}
Expand All @@ -144,7 +145,7 @@ function asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider: () => I
let compressedTreeNode: ITreeNode<ICompressedTreeNode<T>, TFilterData>;

try {
compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e);
compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
} catch {
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e);
}
Expand All @@ -159,6 +160,10 @@ function asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider: () => I
};
}

export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate {
readonly compressionEnabled?: boolean;
}

export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = void> extends ObjectTree<T, TFilterData> implements ICompressedTreeNodeProvider<T, TFilterData> {

protected model!: CompressibleObjectTreeModel<T, TFilterData>;
Expand All @@ -171,7 +176,7 @@ export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = vo
options: ICompressibleObjectTreeOptions<T, TFilterData> = {}
) {
const compressedTreeNodeProvider = () => this;
const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, r));
const compressibleRenderers = renderers.map(r => new CompressibleRenderer<T, TFilterData, any>(compressedTreeNodeProvider, r));
super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options));
}

Expand All @@ -183,15 +188,15 @@ export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = vo
return new CompressibleObjectTreeModel(user, view, options);
}

isCompressionEnabled(): boolean {
return this.model.isCompressionEnabled();
}
updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void {
super.updateOptions(optionsUpdate);

setCompressionEnabled(enabled: boolean): void {
this.model.setCompressionEnabled(enabled);
if (typeof optionsUpdate.compressionEnabled !== 'undefined') {
this.model.setCompressionEnabled(optionsUpdate.compressionEnabled);
}
}

getCompressedTreeNode(element: T): ITreeNode<ICompressedTreeNode<T>, TFilterData> {
return this.model.getCompressedTreeNode(element)!;
getCompressedTreeNode(element: T | null = null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData> {
return this.model.getCompressedTreeNode(element);
}
}
Loading