diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 65c8b83ef844632bad340516a702a77d5973afaf..640ca169306e77fb50fe404da4a24454c8a75df7 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -277,9 +277,10 @@ function asTreeContextMenuEvent(event: IListContextMenuEvent extends IListOptions { - collapseByDefault?: boolean; // defaults to false - filter?: ITreeFilter; + readonly collapseByDefault?: boolean; // defaults to false + readonly filter?: ITreeFilter; readonly dnd?: ITreeDragAndDrop; + readonly autoExpandSingleChildren?: boolean; } export abstract class AbstractTree implements IDisposable { diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index c66a021bee3bf3724f75b8f27942930b6a02ffac..a02ddccef816a66393b7a5d7454064f5908f791b 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -183,6 +183,7 @@ class AsyncDataTreeNodeListDragAndDrop implements IListDragAndDrop(options?: IAsyncDataTreeOptions): IObjectTreeOptions, TFilterData> | undefined { return options && { ...options, + collapseByDefault: true, identityProvider: options.identityProvider && { getId(el) { return options.identityProvider!.getId(el.element as T); @@ -230,6 +231,7 @@ function asTreeElement(node: IAsyncDataTreeNode): ITreeEle export interface IAsyncDataTreeOptions extends IAbstractTreeOptions { identityProvider?: IIdentityProvider; sorter?: ITreeSorter; + autoExpandSingleChildren?: boolean; } export class AsyncDataTree implements IDisposable { @@ -239,6 +241,7 @@ export class AsyncDataTree implements IDisposable private readonly nodes = new Map>(); private readonly refreshPromises = new Map, CancelablePromise>(); private readonly identityProvider?: IIdentityProvider; + private readonly autoExpandSingleChildren: boolean; private readonly _onDidChangeNodeState = new Emitter>(); @@ -261,14 +264,14 @@ export class AsyncDataTree implements IDisposable delegate: IListVirtualDelegate, renderers: ITreeRenderer[], private dataSource: IAsyncDataSource, - options?: IAsyncDataTreeOptions + options: IAsyncDataTreeOptions = {} ) { - this.identityProvider = options && options.identityProvider; + this.identityProvider = options.identityProvider; + this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren; const objectTreeDelegate = new ComposedTreeDelegate>(delegate); const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event)); const objectTreeOptions = asObjectTreeOptions(options) || {}; - objectTreeOptions.collapseByDefault = true; this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); @@ -371,7 +374,7 @@ export class AsyncDataTree implements IDisposable async expand(element: T, recursive: boolean = false): Promise { const node = this.getDataNode(element); - if (!this.tree.isCollapsed(node === this.root ? null : node)) { + if (node !== this.root && !this.tree.isCollapsed(node === this.root ? null : node)) { return false; } @@ -536,48 +539,59 @@ export class AsyncDataTree implements IDisposable return always(result, () => this.currentRefreshCalls.delete(node)); } - private doRefresh(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { + private async doRefresh(node: IAsyncDataTreeNode, recursive: boolean, reason: ChildrenResolutionReason): Promise { 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))) { + return; + } + + if (node !== this.root && (!this.tree.isCollapsible(node) || this.tree.isCollapsed(node))) { node.state = AsyncDataTreeNodeState.Uninitialized; - return Promise.resolve(); - } else { - node.state = AsyncDataTreeNodeState.Loading; + return; + } + + node.state = AsyncDataTreeNodeState.Loading; + this._onDidChangeNodeState.fire(node); + + const slowTimeout = timeout(800); + + slowTimeout.then(() => { + node.state = AsyncDataTreeNodeState.Slow; this._onDidChangeNodeState.fire(node); + }, _ => null); - const slowTimeout = timeout(800); + try { + const children = await this.doGetChildren(node); + slowTimeout.cancel(); + node.state = AsyncDataTreeNodeState.Loaded; + this._onDidChangeNodeState.fire(node); - slowTimeout.then(() => { - node.state = AsyncDataTreeNodeState.Slow; - this._onDidChangeNodeState.fire(node); - }, _ => null); + this.setChildren(node, children, recursive); - return Promise.resolve(this.doGetChildren(node)) - .then(children => { - slowTimeout.cancel(); - node.state = AsyncDataTreeNodeState.Loaded; - this._onDidChangeNodeState.fire(node); + if (node !== this.root && this.autoExpandSingleChildren && reason === ChildrenResolutionReason.Expand) { + const treeNode = this.tree.getNode(node); + const visibleChildren = treeNode.children.filter(node => node.visible); - this.setChildren(node, children, recursive); - }, err => { - if (isPromiseCanceledError(err)) { - return Promise.resolve(null); - } + if (visibleChildren.length === 1) { + this.tree.expand(visibleChildren[0].element, false); + } + } + } catch (err) { + if (isPromiseCanceledError(err)) { + return; + } - slowTimeout.cancel(); - node.state = AsyncDataTreeNodeState.Uninitialized; - this._onDidChangeNodeState.fire(node); + slowTimeout.cancel(); + node.state = AsyncDataTreeNodeState.Uninitialized; + this._onDidChangeNodeState.fire(node); - if (node !== this.root) { - this.tree.collapse(node === this.root ? null : node); - } + if (node !== this.root) { + this.tree.collapse(node === this.root ? null : node); + } - return Promise.reject(err); - }); + throw err; } } diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 4390a4dc512c0a0ad16563873aa09c4918a75e7f..4c9fa0390a8f797796c9fb16b558241061539e4f 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -39,8 +39,9 @@ function getVisibleState(visibility: boolean | TreeVisibility): TreeVisibility { } export interface IIndexTreeModelOptions { - collapseByDefault?: boolean; // defaults to false - filter?: ITreeFilter; + readonly collapseByDefault?: boolean; // defaults to false + readonly filter?: ITreeFilter; + readonly autoExpandSingleChildren?: boolean; } export class IndexTreeModel, TFilterData = void> implements ITreeModel { @@ -58,10 +59,12 @@ export class IndexTreeModel, TFilterData = voi private collapseByDefault: boolean; private filter?: ITreeFilter; + private autoExpandSingleChildren: boolean; constructor(private list: ISpliceable>, rootElement: T, options: IIndexTreeModelOptions = {}) { this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault; this.filter = options.filter; + this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren; // this.onDidChangeCollapseState(node => console.log(node.collapsed, node)); @@ -140,18 +143,45 @@ export class IndexTreeModel, TFilterData = voi } setCollapsed(location: number[], collapsed?: boolean, recursive?: boolean): boolean { - const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location); + const node = this.getTreeNode(location); if (typeof collapsed === 'undefined') { collapsed = !node.collapsed; } - return this.eventBufferer.bufferEvents(() => { - return this._setCollapsed(node, listIndex, revealed, collapsed!, recursive || false); - }); + return this.eventBufferer.bufferEvents(() => this._setCollapsed(location, collapsed!, recursive)); + } + + private _setCollapsed(location: number[], collapsed: boolean, recursive?: boolean): boolean { + const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location); + + const result = this._setListNodeCollapsed(node, listIndex, revealed, collapsed!, recursive || false); + + if (this.autoExpandSingleChildren && !collapsed! && !recursive) { + let onlyVisibleChildIndex = -1; + + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + + if (child.visible) { + if (onlyVisibleChildIndex > -1) { + onlyVisibleChildIndex = -1; + break; + } else { + onlyVisibleChildIndex = i; + } + } + } + + if (onlyVisibleChildIndex > -1) { + this._setCollapsed([...location, onlyVisibleChildIndex], false, false); + } + } + + return result; } - private _setCollapsed(node: IMutableTreeNode, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean { + private _setListNodeCollapsed(node: IMutableTreeNode, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean { const result = this._setNodeCollapsed(node, collapsed, recursive, false); if (!revealed || !node.visible) { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 7ed2449e061648a9611ec7b50413490828a779f7..3ac8b1b335378609f15276eb1c2605765e250140 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -10,7 +10,7 @@ import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { - sorter?: ITreeSorter; + readonly sorter?: ITreeSorter; } export class ObjectTreeModel, TFilterData extends NonNullable = void> implements ITreeModel { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 850055107f1648f6e19203f77ce2e8687ea63f1a..9550ca9bdf6a485ec044b268e36c5f5cc20e00d3 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -251,7 +251,8 @@ export class ExplorerView extends ViewletPanel { multipleSelectionSupport: true, filter: this.filter, sorter: this.instantiationService.createInstance(FileSorter), - dnd: this.instantiationService.createInstance(FileDragAndDrop) + dnd: this.instantiationService.createInstance(FileDragAndDrop), + autoExpandSingleChildren: true }, this.contextKeyService, this.listService, this.themeService, this.configurationService, this.keybindingService); this.disposables.push(this.tree);