From bede31d0d9a3adb710c8b287d438f6cdd7f98cec Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 11 Oct 2019 17:47:35 +0200 Subject: [PATCH] resource tree: support data in branches --- src/vs/base/common/resourceTree.ts | 155 ++++++++---------- src/vs/base/test/common/resourceTree.test.ts | 58 +++++-- .../contrib/scm/browser/repositoryPanel.ts | 103 ++++++------ 3 files changed, 159 insertions(+), 157 deletions(-) diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index af53307e036..4adca4ca4f2 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -11,58 +11,48 @@ import { URI } from 'vs/base/common/uri'; import { mapValues } from 'vs/base/common/collections'; import { PathIterator } from 'vs/base/common/map'; -export interface ILeafNode { +export interface IResourceNode { readonly uri: URI; readonly relativePath: string; readonly name: string; - readonly element: T; + readonly element: T | undefined; + readonly children: Iterator>; + readonly childrenCount: number; + readonly parent: IResourceNode | undefined; readonly context: C; + get(childName: string): IResourceNode | undefined; } -export interface IBranchNode { - readonly uri: URI; - readonly relativePath: string; - readonly name: string; - readonly size: number; - readonly children: Iterator>; - readonly parent: IBranchNode | undefined; - readonly context: C; - get(childName: string): INode | undefined; -} - -export type INode = IBranchNode | ILeafNode; - -// Internals - -class Node { - - @memoize - get name(): string { return paths.posix.basename(this.relativePath); } - - constructor(readonly uri: URI, readonly relativePath: string, readonly context: C) { } -} - -class BranchNode extends Node implements IBranchNode { +class Node implements IResourceNode { - private _children = new Map | LeafNode>(); + private _children = new Map>(); - get size(): number { + get childrenCount(): number { return this._children.size; } - get children(): Iterator | LeafNode> { + get children(): Iterator> { return Iterator.fromArray(mapValues(this._children)); } - constructor(uri: URI, relativePath: string, context: C, readonly parent: IBranchNode | undefined = undefined) { - super(uri, relativePath, context); + @memoize + get name(): string { + return paths.posix.basename(this.relativePath); } - get(path: string): BranchNode | LeafNode | undefined { + constructor( + readonly uri: URI, + readonly relativePath: string, + readonly context: C, + public element: T | undefined = undefined, + readonly parent: IResourceNode | undefined = undefined + ) { } + + get(path: string): Node | undefined { return this._children.get(path); } - set(path: string, child: BranchNode | LeafNode): void { + set(path: string, child: Node): void { this._children.set(path, child); } @@ -75,32 +65,21 @@ class BranchNode extends Node implements IBranchNode { } } -class LeafNode extends Node implements ILeafNode { - - constructor(uri: URI, path: string, context: C, readonly element: T) { - super(uri, path, context); - } -} - -function collect(node: INode, result: T[]): T[] { - if (ResourceTree.isBranchNode(node)) { - Iterator.forEach(node.children, child => collect(child, result)); - } else { +function collect(node: IResourceNode, result: T[]): T[] { + if (typeof node.element !== 'undefined') { result.push(node.element); } + Iterator.forEach(node.children, child => collect(child, result)); + return result; } export class ResourceTree, C> { - readonly root: BranchNode; - - static isBranchNode(obj: any): obj is IBranchNode { - return obj instanceof BranchNode; - } + readonly root: Node; - static getRoot(node: IBranchNode): IBranchNode { + static getRoot(node: IResourceNode): IResourceNode { while (node.parent) { node = node.parent; } @@ -108,12 +87,16 @@ export class ResourceTree, C> { return node; } - static collect(node: INode): T[] { + static collect(node: IResourceNode): T[] { return collect(node, []); } + static isResourceNode(obj: any): obj is IResourceNode { + return obj instanceof Node; + } + constructor(context: C, rootURI: URI = URI.file('/')) { - this.root = new BranchNode(rootURI, '', context); + this.root = new Node(rootURI, '', context); } add(uri: URI, element: T): void { @@ -129,26 +112,17 @@ export class ResourceTree, C> { let child = node.get(name); if (!child) { - if (iterator.hasNext()) { - child = new BranchNode(joinPath(this.root.uri, path), path, this.root.context, node); - node.set(name, child); - } else { - child = new LeafNode(uri, path, this.root.context, element); - node.set(name, child); - return; - } - } - - if (!(child instanceof BranchNode)) { - if (iterator.hasNext()) { - throw new Error('Inconsistent tree: can\'t override leaf with branch.'); - } - - // replace - node.set(name, new LeafNode(uri, path, this.root.context, element)); - return; + child = new Node( + joinPath(this.root.uri, path), + path, + this.root.context, + iterator.hasNext() ? undefined : element, + node + ); + + node.set(name, child); } else if (!iterator.hasNext()) { - throw new Error('Inconsistent tree: can\'t override branch with leaf.'); + child.element = element; } node = child; @@ -167,7 +141,7 @@ export class ResourceTree, C> { return this._delete(this.root, iterator); } - private _delete(node: BranchNode, iterator: PathIterator): T | undefined { + private _delete(node: Node, iterator: PathIterator): T | undefined { const name = iterator.value(); const child = node.get(name); @@ -175,25 +149,14 @@ export class ResourceTree, C> { return undefined; } - // not at end if (iterator.hasNext()) { - if (child instanceof BranchNode) { - const result = this._delete(child, iterator.next()); + const result = this._delete(child, iterator.next()); - if (typeof result !== 'undefined' && child.size === 0) { - node.delete(name); - } - - return result; - } else { - throw new Error('Inconsistent tree: Expected a branch, found a leaf instead.'); + if (typeof result !== 'undefined' && child.childrenCount === 0) { + node.delete(name); } - } - //at end - if (child instanceof BranchNode) { - // TODO: maybe we can allow this - throw new Error('Inconsistent tree: Expected a leaf, found a branch instead.'); + return result; } node.delete(name); @@ -203,4 +166,22 @@ export class ResourceTree, C> { clear(): void { this.root.clear(); } + + getNode(uri: URI): IResourceNode | undefined { + const key = relativePath(this.root.uri, uri) || uri.fsPath; + const iterator = new PathIterator(false).reset(key); + let node = this.root; + + while (true) { + const name = iterator.value(); + const child = node.get(name); + + if (!child || !iterator.hasNext()) { + return child; + } + + node = child; + iterator.next(); + } + } } diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index d3050bcd990..3784e38ccc7 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -4,46 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ResourceTree, IBranchNode, ILeafNode } from 'vs/base/common/resourceTree'; +import { ResourceTree } from 'vs/base/common/resourceTree'; import { URI } from 'vs/base/common/uri'; -suite('ResourceTree', function () { +suite.only('ResourceTree', function () { test('ctor', function () { const tree = new ResourceTree(null); - assert(ResourceTree.isBranchNode(tree.root)); - assert.equal(tree.root.size, 0); + assert.equal(tree.root.childrenCount, 0); }); test('simple', function () { const tree = new ResourceTree(null); tree.add(URI.file('/foo/bar.txt'), 'bar contents'); - assert(ResourceTree.isBranchNode(tree.root)); - assert.equal(tree.root.size, 1); + assert.equal(tree.root.childrenCount, 1); - let foo = tree.root.get('foo') as IBranchNode; + let foo = tree.root.get('foo')!; assert(foo); - assert(ResourceTree.isBranchNode(foo)); - assert.equal(foo.size, 1); + assert.equal(foo.childrenCount, 1); - let bar = foo.get('bar.txt') as ILeafNode; + let bar = foo.get('bar.txt')!; assert(bar); - assert(!ResourceTree.isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); tree.add(URI.file('/hello.txt'), 'hello contents'); - assert.equal(tree.root.size, 2); + assert.equal(tree.root.childrenCount, 2); - let hello = tree.root.get('hello.txt') as ILeafNode; + let hello = tree.root.get('hello.txt')!; assert(hello); - assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); tree.delete(URI.file('/foo/bar.txt')); - assert.equal(tree.root.size, 1); - hello = tree.root.get('hello.txt') as ILeafNode; + assert.equal(tree.root.childrenCount, 1); + hello = tree.root.get('hello.txt')!; assert(hello); - assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); }); + + test('folders with data', function () { + const tree = new ResourceTree(null); + + assert.equal(tree.root.childrenCount, 0); + + tree.add(URI.file('/foo'), 'foo'); + assert.equal(tree.root.childrenCount, 1); + assert.equal(tree.root.get('foo')!.element, 'foo'); + + tree.add(URI.file('/bar'), 'bar'); + assert.equal(tree.root.childrenCount, 2); + assert.equal(tree.root.get('bar')!.element, 'bar'); + + tree.add(URI.file('/foo/file.txt'), 'file'); + assert.equal(tree.root.childrenCount, 2); + assert.equal(tree.root.get('foo')!.element, 'foo'); + assert.equal(tree.root.get('bar')!.element, 'bar'); + assert.equal(tree.root.get('foo')!.get('file.txt')!.element, 'file'); + + tree.delete(URI.file('/foo')); + assert.equal(tree.root.childrenCount, 1); + assert(!tree.root.get('foo')); + assert.equal(tree.root.get('bar')!.element, 'bar'); + + tree.delete(URI.file('/bar')); + assert.equal(tree.root.childrenCount, 0); + assert(!tree.root.get('foo')); + assert(!tree.root.get('bar')); + }); }); diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index ab56475c074..f41d99ee46e 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scmViewlet'; import { Event, Emitter } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { basename, isEqual } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom'; @@ -37,7 +37,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import * as platform from 'vs/base/common/platform'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { ISplice } from 'vs/base/common/sequence'; -import { ResourceTree, IBranchNode, INode } from 'vs/base/common/resourceTree'; +import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; import { ObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; @@ -53,7 +53,7 @@ import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/th import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; -type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; +type TreeElement = ISCMResourceGroup | IResourceNode | ISCMResource; interface ResourceGroupTemplate { readonly name: HTMLElement; @@ -132,11 +132,11 @@ interface ResourceTemplate { class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => (ISCMResource | IBranchNode)[]) { + constructor(private getSelectedResources: () => (ISCMResource | IResourceNode)[]) { super(); } - runAction(action: IAction, context: ISCMResource | IBranchNode): Promise { + runAction(action: IAction, context: ISCMResource | IResourceNode): Promise { if (!(action instanceof MenuItemAction)) { return super.runAction(action, context); } @@ -144,12 +144,12 @@ class MultipleSelectionActionRunner extends ActionRunner { const selection = this.getSelectedResources(); const contextIsSelected = selection.some(s => s === context); const actualContext = contextIsSelected ? selection : [context]; - const args = flatten(actualContext.map(e => ResourceTree.isBranchNode(e) ? ResourceTree.collect(e) : [e])); + const args = flatten(actualContext.map(e => ResourceTree.isResourceNode(e) ? ResourceTree.collect(e) : [e])); return action.run(...args); } } -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { static readonly TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -158,7 +158,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer ViewModel, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, - private getSelectedResources: () => (ISCMResource | IBranchNode)[], + private getSelectedResources: () => (ISCMResource | IResourceNode)[], private themeService: IThemeService, private menus: SCMMenus ) { } @@ -179,16 +179,16 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); const resourceOrFolder = node.element; const theme = this.themeService.getTheme(); - const icon = !ResourceTree.isBranchNode(resourceOrFolder) && (theme.type === LIGHT ? resourceOrFolder.decorations.icon : resourceOrFolder.decorations.iconDark); + const icon = !ResourceTree.isResourceNode(resourceOrFolder) && (theme.type === LIGHT ? resourceOrFolder.decorations.icon : resourceOrFolder.decorations.iconDark); - const uri = ResourceTree.isBranchNode(resourceOrFolder) ? resourceOrFolder.uri : resourceOrFolder.sourceUri; - const fileKind = ResourceTree.isBranchNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; + const uri = ResourceTree.isResourceNode(resourceOrFolder) ? resourceOrFolder.uri : resourceOrFolder.sourceUri; + const fileKind = ResourceTree.isResourceNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); template.fileLabel.setFile(uri, { @@ -201,7 +201,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); } - renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); - const compressed = node.element as ICompressedTreeNode>; + const compressed = node.element as ICompressedTreeNode>; const folder = compressed.elements[compressed.elements.length - 1]; const label = compressed.elements.map(e => e.name).join('/'); @@ -261,7 +261,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + disposeCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); } @@ -276,7 +276,7 @@ class ProviderListDelegate implements IListVirtualDelegate { getHeight() { return 22; } getTemplateId(element: TreeElement) { - if (ResourceTree.isBranchNode(element) || isSCMResource(element)) { + if (ResourceTree.isResourceNode(element) || isSCMResource(element)) { return ResourceRenderer.TEMPLATE_ID; } else { return ResourceGroupRenderer.TEMPLATE_ID; @@ -287,7 +287,7 @@ class ProviderListDelegate implements IListVirtualDelegate { class SCMTreeFilter implements ITreeFilter { filter(element: TreeElement): boolean { - if (ResourceTree.isBranchNode(element)) { + if (ResourceTree.isResourceNode(element)) { return true; } else if (isSCMResourceGroup(element)) { return element.elements.length > 0 || !element.hideWhenEmpty; @@ -313,15 +313,15 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } - const oneIsDirectory = ResourceTree.isBranchNode(one); - const otherIsDirectory = ResourceTree.isBranchNode(other); + const oneIsDirectory = ResourceTree.isResourceNode(one); + const otherIsDirectory = ResourceTree.isResourceNode(other); if (oneIsDirectory !== otherIsDirectory) { return oneIsDirectory ? -1 : 1; } - const oneName = ResourceTree.isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); - const otherName = ResourceTree.isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); + const oneName = ResourceTree.isResourceNode(one) ? one.name : basename((one as ISCMResource).sourceUri); + const otherName = ResourceTree.isResourceNode(other) ? other.name : basename((other as ISCMResource).sourceUri); return compareFileNames(oneName, otherName); } @@ -330,7 +330,7 @@ export class SCMTreeSorter implements ITreeSorter { export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider { getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { - if (ResourceTree.isBranchNode(element)) { + if (ResourceTree.isResourceNode(element)) { return element.name; } else if (isSCMResourceGroup(element)) { return element.label; @@ -340,7 +340,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb } getCompressedNodeKeyboardNavigationLabel(elements: TreeElement[]): { toString(): string | undefined; } | undefined { - const folders = elements as IBranchNode[]; + const folders = elements as IResourceNode[]; return folders.map(e => e.name).join('/'); } } @@ -348,7 +348,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb class SCMResourceIdentityProvider implements IIdentityProvider { getId(element: TreeElement): string { - if (ResourceTree.isBranchNode(element)) { + if (ResourceTree.isResourceNode(element)) { const group = element.context; return `${group.provider.contextValue}/${group.id}/$FOLDER/${element.uri.toString()}`; } else if (isSCMResource(element)) { @@ -377,16 +377,16 @@ function groupItemAsTreeElement(item: IGroupItem, mode: ViewModelMode): ICompres return { element: item.group, children, incompressible: true, collapsible: true }; } -function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { - if (ResourceTree.isBranchNode(node)) { - return { - element: node, - children: Iterator.map(node.children, node => asTreeElement(node, false)), - incompressible - }; +function asTreeElement(node: IResourceNode, forceIncompressible: boolean): ICompressedTreeElement { + if (node.childrenCount === 0 && node.element) { + return { element: node.element, incompressible: true }; } - return { element: node.element, incompressible: true }; + return { + element: node, + children: Iterator.map(node.children, node => asTreeElement(node, false)), + incompressible: forceIncompressible + }; } const enum ViewModelMode { @@ -538,17 +538,14 @@ class ViewModel { } // go backwards from last group - for (let i = this.provider.groups.elements.length - 1; i >= 0; i--) { - const group = this.provider.groups.elements[i]; - - for (const resource of group.elements) { - if (isEqual(uri, resource.sourceUri)) { - this.tree.reveal(resource); - this.tree.setSelection([resource]); - this.tree.setFocus([resource]); - - return; - } + for (let i = this.items.length - 1; i >= 0; i--) { + const node = this.items[i].tree.getNode(uri); + + if (node && node.element) { + this.tree.reveal(node.element); + this.tree.setSelection([node.element]); + this.tree.setFocus([node.element]); + return; } } } @@ -753,12 +750,12 @@ export class RepositoryPanel extends ViewletPanel { this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) - .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) + .filter(e => !!e && !isSCMResourceGroup(e) && !ResourceTree.isResourceNode(e)) .on(this.open, this)); this._register(Event.chain(this.tree.onDidPin) .map(e => e.elements[0]) - .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) + .filter(e => !!e && !isSCMResourceGroup(e) && !ResourceTree.isResourceNode(e)) .on(this.pin, this)); this._register(this.tree.onContextMenu(this.onListContextMenu, this)); @@ -911,12 +908,12 @@ export class RepositoryPanel extends ViewletPanel { const element = e.element; let actions: IAction[] = []; - if (ResourceTree.isBranchNode(element)) { + if (isSCMResourceGroup(element)) { + actions = this.menus.getResourceGroupContextActions(element); + } else if (ResourceTree.isResourceNode(element)) { actions = this.menus.getResourceFolderContextActions(element.context); - } else if (isSCMResource(element)) { - actions = this.menus.getResourceContextActions(element); } else { - actions = this.menus.getResourceGroupContextActions(element); + actions = this.menus.getResourceContextActions(element); } this.contextMenuService.showContextMenu({ @@ -927,7 +924,7 @@ export class RepositoryPanel extends ViewletPanel { }); } - private getSelectedResources(): (ISCMResource | IBranchNode)[] { + private getSelectedResources(): (ISCMResource | IResourceNode)[] { return this.tree.getSelection() .filter(r => !!r && !isSCMResourceGroup(r))! as any; } -- GitLab