mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-26 19:44:25 +01:00
Merge pull request #65008 from Microsoft/isidorn/explorerNewTree
Explorer adopt new tree
This commit is contained in:
@@ -173,6 +173,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
private supportDynamicHeights: boolean;
|
||||
|
||||
private dnd: IListViewDragAndDrop<T>;
|
||||
private canDrop: boolean = false;
|
||||
private currentDragData: IDragAndDropData | undefined;
|
||||
private currentDragFeedback: number[] | undefined;
|
||||
private currentDragFeedbackDisposable: IDisposable = Disposable.None;
|
||||
@@ -658,9 +659,11 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.browserEvent);
|
||||
const canDrop = typeof result === 'boolean' ? result : result.accept;
|
||||
this.canDrop = typeof result === 'boolean' ? result : result.accept;
|
||||
|
||||
if (!canDrop) {
|
||||
if (!this.canDrop) {
|
||||
this.currentDragFeedback = undefined;
|
||||
this.currentDragFeedbackDisposable.dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -727,6 +730,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
private onDrop(event: IListDragEvent<T>): void {
|
||||
if (!this.canDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragData = this.currentDragData;
|
||||
this.teardownDragAndDropScrollTopAnimation();
|
||||
this.clearDragOverFeedback();
|
||||
@@ -743,6 +750,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
private onDragEnd(): void {
|
||||
this.canDrop = false;
|
||||
this.teardownDragAndDropScrollTopAnimation();
|
||||
this.clearDragOverFeedback();
|
||||
this.currentDragData = undefined;
|
||||
|
||||
@@ -104,6 +104,9 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
|
||||
}
|
||||
|
||||
drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
|
||||
this.autoExpandDisposable.dispose();
|
||||
this.autoExpandNode = undefined;
|
||||
|
||||
this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
|
||||
}
|
||||
}
|
||||
@@ -193,7 +196,7 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
|
||||
this.renderedNodes.set(node, templateData);
|
||||
this.renderedElements.set(node.element, node);
|
||||
|
||||
templateData.twistie.style.width = `${10 + node.depth * 10}px`;
|
||||
templateData.twistie.style.marginLeft = `${node.depth * 10}px`;
|
||||
this.renderTwistie(node, templateData.twistie);
|
||||
|
||||
this.renderer.renderElement(node, index, templateData.templateData);
|
||||
|
||||
@@ -9,18 +9,20 @@ import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOve
|
||||
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { timeout, always } from 'vs/base/common/async';
|
||||
import { timeout, always, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { toggleClass } from 'vs/base/browser/dom';
|
||||
import { Iterator } from 'vs/base/common/iterator';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
|
||||
enum AsyncDataTreeNodeState {
|
||||
Uninitialized,
|
||||
Loaded,
|
||||
Loading,
|
||||
Slow
|
||||
Slow,
|
||||
Disposed
|
||||
}
|
||||
|
||||
interface IAsyncDataTreeNode<TInput, T> {
|
||||
@@ -31,6 +33,16 @@ interface IAsyncDataTreeNode<TInput, T> {
|
||||
state: AsyncDataTreeNodeState;
|
||||
}
|
||||
|
||||
function isAncestor<TInput, T>(ancestor: IAsyncDataTreeNode<TInput, T>, descendant: IAsyncDataTreeNode<TInput, T>): boolean {
|
||||
if (!descendant.parent) {
|
||||
return false;
|
||||
} else if (descendant.parent === ancestor) {
|
||||
return true;
|
||||
} else {
|
||||
return isAncestor(ancestor, descendant.parent);
|
||||
}
|
||||
}
|
||||
|
||||
interface IDataTreeListTemplateData<T> {
|
||||
templateData: T;
|
||||
}
|
||||
@@ -114,16 +126,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;
|
||||
@@ -221,7 +223,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
private 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 refreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
|
||||
private readonly refreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, CancelablePromise<T[]>>();
|
||||
private readonly identityProvider?: IIdentityProvider<T>;
|
||||
|
||||
private readonly _onDidChangeNodeState = new Emitter<IAsyncDataTreeNode<TInput, T>>();
|
||||
@@ -232,9 +234,6 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
get onDidChangeSelection(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); }
|
||||
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidOpen, asTreeEvent); }
|
||||
|
||||
private readonly _onDidResolveChildren = new Emitter<IChildrenResolutionEvent<T>>();
|
||||
readonly onDidResolveChildren: Event<IChildrenResolutionEvent<T>> = this._onDidResolveChildren.event;
|
||||
|
||||
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseClick, asTreeMouseEvent); }
|
||||
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); }
|
||||
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.tree.onContextMenu, asTreeContextMenuEvent); }
|
||||
@@ -327,6 +326,9 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
|
||||
setInput(input: TInput): Promise<void> {
|
||||
this.refreshPromises.forEach(promise => promise.cancel());
|
||||
this.refreshPromises.clear();
|
||||
|
||||
this.root.element = input!;
|
||||
return this.refresh(input);
|
||||
}
|
||||
@@ -336,7 +338,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
throw new Error('Tree input not set');
|
||||
}
|
||||
|
||||
return this.refreshNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh);
|
||||
return this.refreshNode(this.getDataNode(element), recursive);
|
||||
}
|
||||
|
||||
// Tree
|
||||
@@ -348,20 +350,21 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
|
||||
collapse(element: T, recursive: boolean = false): boolean {
|
||||
return this.tree.collapse(this.getDataNode(element), recursive);
|
||||
const node = this.getDataNode(element);
|
||||
return this.tree.collapse(node === this.root ? null : node, recursive);
|
||||
}
|
||||
|
||||
async expand(element: T, recursive: boolean = false): Promise<boolean> {
|
||||
const node = this.getDataNode(element);
|
||||
|
||||
if (!this.tree.isCollapsed(node)) {
|
||||
if (!this.tree.isCollapsed(node === this.root ? null : node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.tree.expand(node, recursive);
|
||||
this.tree.expand(node === this.root ? null : node, recursive);
|
||||
|
||||
if (node.state !== AsyncDataTreeNodeState.Loaded) {
|
||||
await this.refreshNode(node, false, ChildrenResolutionReason.Expand);
|
||||
await this.refreshNode(node, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -485,33 +488,48 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
return node;
|
||||
}
|
||||
|
||||
private async refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, reason: ChildrenResolutionReason): Promise<void> {
|
||||
await this._refreshNode(node, recursive, reason);
|
||||
private async refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean): Promise<void> {
|
||||
await this.queueRefresh(node, recursive);
|
||||
|
||||
if (recursive && node.children) {
|
||||
await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason)));
|
||||
await Promise.all(node.children.map(child => this.refreshNode(child, recursive)));
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, reason: ChildrenResolutionReason): Promise<void> {
|
||||
let result = this.refreshPromises.get(node);
|
||||
private currentRefreshCalls = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
|
||||
|
||||
private async queueRefresh(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean): Promise<void> {
|
||||
if (node.state === AsyncDataTreeNodeState.Disposed) {
|
||||
console.error('Async data tree node is disposed');
|
||||
return;
|
||||
}
|
||||
|
||||
let result: Promise<void> | undefined;
|
||||
|
||||
this.currentRefreshCalls.forEach((refreshPromise, refreshNode) => {
|
||||
if (isAncestor(refreshNode, node) || isAncestor(node, refreshNode)) {
|
||||
result = refreshPromise.then(() => this.queueRefresh(node, recursive));
|
||||
}
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this.doRefresh(node, recursive, reason);
|
||||
this.refreshPromises.set(node, result);
|
||||
return always(result, () => this.refreshPromises.delete(node));
|
||||
result = this.doRefresh(node, recursive);
|
||||
|
||||
this.currentRefreshCalls.set(node, result);
|
||||
return always(result, () => this.currentRefreshCalls.delete(node));
|
||||
}
|
||||
|
||||
private doRefresh(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, reason: ChildrenResolutionReason): Promise<void> {
|
||||
private doRefresh(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean): Promise<void> {
|
||||
const hasChildren = !!this.dataSource.hasChildren(node.element!);
|
||||
|
||||
if (!hasChildren) {
|
||||
this.setChildren(node, [], recursive);
|
||||
return Promise.resolve();
|
||||
} else if (node !== this.root && (!this.tree.isCollapsible(node) || this.tree.isCollapsed(node))) {
|
||||
node.state = AsyncDataTreeNodeState.Uninitialized;
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
node.state = AsyncDataTreeNodeState.Loading;
|
||||
@@ -524,21 +542,24 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
}, _ => null);
|
||||
|
||||
return Promise.resolve(this.dataSource.getChildren(node.element!))
|
||||
return Promise.resolve(this.doGetChildren(node))
|
||||
.then(children => {
|
||||
slowTimeout.cancel();
|
||||
node.state = AsyncDataTreeNodeState.Loaded;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
|
||||
this.setChildren(node, children, recursive);
|
||||
this._onDidResolveChildren.fire({ element: node.element as T, reason });
|
||||
}, err => {
|
||||
if (isPromiseCanceledError(err)) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
slowTimeout.cancel();
|
||||
node.state = AsyncDataTreeNodeState.Uninitialized;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
|
||||
if (node !== this.root) {
|
||||
this.tree.collapse(node);
|
||||
this.tree.collapse(node === this.root ? null : node);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
@@ -546,12 +567,24 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private doGetChildren(node: IAsyncDataTreeNode<TInput, T>): Promise<T[]> {
|
||||
let result = this.refreshPromises.get(node);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = createCancelablePromise(_ => Promise.resolve(this.dataSource.getChildren(node.element!)));
|
||||
this.refreshPromises.set(node, result);
|
||||
return always(result, () => this.refreshPromises.delete(node));
|
||||
}
|
||||
|
||||
private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent<IAsyncDataTreeNode<TInput, T>, any>): void {
|
||||
if (!node.collapsed && node.element.state === AsyncDataTreeNodeState.Uninitialized) {
|
||||
if (deep) {
|
||||
this.collapse(node.element.element as T);
|
||||
} else {
|
||||
this.refreshNode(node.element, false, ChildrenResolutionReason.Expand);
|
||||
this.refreshNode(node.element, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,12 +633,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
asyncDataTreeNode.element = element;
|
||||
|
||||
const collapsible = !!this.dataSource.hasChildren(element);
|
||||
const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode);
|
||||
const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode === this.root ? null : asyncDataTreeNode);
|
||||
|
||||
if (recursive) {
|
||||
asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized;
|
||||
|
||||
if (this.tree.isCollapsed(asyncDataTreeNode)) {
|
||||
if (this.tree.isCollapsed(asyncDataTreeNode === this.root ? null : asyncDataTreeNode)) {
|
||||
asyncDataTreeNode.children!.length = 0;
|
||||
|
||||
return {
|
||||
@@ -642,6 +675,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
const onDidDeleteNode = (treeNode: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>) => {
|
||||
if (treeNode.element.element) {
|
||||
if (!insertedElements.has(treeNode.element.element as T)) {
|
||||
treeNode.element.state = AsyncDataTreeNodeState.Disposed;
|
||||
this.nodes.delete(treeNode.element.element as T);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
text-align: right;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.monaco-tl-contents {
|
||||
|
||||
Reference in New Issue
Block a user