diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 049adbb93b9..f5cc030c0b6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -695,9 +695,7 @@ export class Repository implements Disposable { this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, onDotGitFileChange, outputChannel)); const root = Uri.file(repository.root); - this._sourceControl = scm.createSourceControl('git', 'Git', root, { - treeRendering: true - }); + this._sourceControl = scm.createSourceControl('git', 'Git', root); this._sourceControl.acceptInputCommand = { command: 'git.commit', title: localize('commit', "Commit"), arguments: [this._sourceControl] }; this._sourceControl.quickDiffProvider = this; 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; -} +class Node implements IResourceNode { -export type INode = IBranchNode | ILeafNode; + private _children = new Map>(); -// 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 { - - private _children = new Map | LeafNode>(); - - 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; + readonly root: Node; - static isBranchNode(obj: any): obj is IBranchNode { - return obj instanceof BranchNode; - } - - 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; - } - } + child = new Node( + joinPath(this.root.uri, path), + path, + this.root.context, + iterator.hasNext() ? undefined : element, + node + ); - 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; + 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/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b4ab5f9a664..30bc8d76e13 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -764,35 +764,6 @@ declare module 'vscode' { //#endregion - //#region Joao: SCM tree rendering - - /** - * Options for creating a [SourceControl](#SourceControl) instance. - */ - export interface SourceControlOptions { - - /** - * Whether tree rendering is supported by the [SourceControl](#SourceControl) instance. - */ - readonly treeRendering?: boolean; - } - - export namespace scm { - - /** - * Creates a new [source control](#SourceControl) instance. - * - * @param id An `id` for the source control. Something short, e.g.: `git`. - * @param label A human-readable string for the source control. E.g.: `Git`. - * @param rootUri An optional Uri of the root of the source control. E.g.: `Uri.parse(workspaceRoot)`. - * @param options Additional options for creating the source control. - * @return An instance of [source control](#SourceControl). - */ - export function createSourceControl(id: string, label: string, rootUri?: Uri, options?: SourceControlOptions): SourceControl; - } - - //#endregion - //#region Joao: SCM Input Box /** diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 9e9f9ff7dbf..4488720cf9a 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMProviderProps, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/modes'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ISplice, Sequence } from 'vs/base/common/sequence'; @@ -128,15 +128,12 @@ class MainThreadSCMProvider implements ISCMProvider { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; - get treeRendering(): boolean { return this._props.treeRendering; } - constructor( private readonly proxy: ExtHostSCMShape, private readonly _handle: number, private readonly _contextValue: string, private readonly _label: string, private readonly _rootUri: URI | undefined, - private readonly _props: SCMProviderProps, @ISCMService scmService: ISCMService ) { } @@ -290,8 +287,8 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables.dispose(); } - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, props: SCMProviderProps): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.revive(rootUri), props, this.scmService); + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void { + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri && URI.revive(rootUri), this.scmService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 98f5405717a..aa5b86537fe 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -709,8 +709,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get inputBox() { return extHostSCM.getLastInputBox(extension)!; // Strict null override - Deprecated api }, - createSourceControl(id: string, label: string, rootUri?: vscode.Uri, opts?: vscode.SourceControlOptions) { - return extHostSCM.createSourceControl(extension, id, label, rootUri, opts); + createSourceControl(id: string, label: string, rootUri?: vscode.Uri) { + return extHostSCM.createSourceControl(extension, id, label, rootUri); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 23de861b428..6400262b603 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -656,10 +656,6 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionHostExit(code: number): void; } -export interface SCMProviderProps { - readonly treeRendering: boolean; -} - export interface SCMProviderFeatures { hasQuickDiffProvider?: boolean; count?: number; @@ -693,7 +689,7 @@ export type SCMRawResourceSplices = [ ]; export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, props: SCMProviderProps): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 47db2513d83..6bc3d01ff2c 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -9,7 +9,7 @@ import { debounce } from 'vs/base/common/decorators'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, SCMProviderProps } from './extHost.protocol'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import * as vscode from 'vscode'; @@ -452,15 +452,10 @@ class ExtHostSourceControl implements vscode.SourceControl { private _commands: ExtHostCommands, private _id: string, private _label: string, - private _rootUri: vscode.Uri | undefined, - _props: SCMProviderProps + private _rootUri?: vscode.Uri ) { - if (!_extension.enableProposedApi && _props.treeRendering) { - throw new Error(`[${_extension.identifier.value}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${_extension.identifier.value}`); - } - this._inputBox = new ExtHostSCMInputBox(_extension, this._proxy, this.handle); - this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, _props); + this._proxy.$registerSourceControl(this.handle, _id, _label, _rootUri); } private updatedResourceGroups = new Set(); @@ -522,12 +517,6 @@ class ExtHostSourceControl implements vscode.SourceControl { } } -function asProps(options: vscode.SourceControlOptions | undefined): SCMProviderProps { - return { - treeRendering: options && !!options.treeRendering || false - }; -} - export class ExtHostSCM implements ExtHostSCMShape { private static _handlePool: number = 0; @@ -587,11 +576,11 @@ export class ExtHostSCM implements ExtHostSCMShape { }); } - createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined, options?: vscode.SourceControlOptions): vscode.SourceControl { + createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined): vscode.SourceControl { this.logService.trace('ExtHostSCM#createSourceControl', extension.identifier.value, id, label, rootUri); const handle = ExtHostSCM._handlePool++; - const sourceControl = new ExtHostSourceControl(extension, this._proxy, this._commands, id, label, rootUri, asProps(options)); + const sourceControl = new ExtHostSourceControl(extension, this._proxy, this._commands, id, label, rootUri); this._sourceControls.set(handle, sourceControl); const sourceControls = this._sourceControlsByExtension.get(ExtensionIdentifier.toKey(extension.identifier)) || []; diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index ab56475c074..7ca8bac8f86 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -6,12 +6,12 @@ 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'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -36,8 +36,8 @@ import { ThrottledDelayer, disposableTimeout } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; 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 { ISequence, ISplice } from 'vs/base/common/sequence'; 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,17 @@ 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 iconResource = ResourceTree.isResourceNode(resourceOrFolder) ? resourceOrFolder.element : resourceOrFolder; + const icon = iconResource && (theme.type === LIGHT ? iconResource.decorations.icon : iconResource.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,17 +202,23 @@ 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 +268,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 +283,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 +294,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 +320,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 +337,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 +347,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 +355,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 +384,12 @@ 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 - }; - } - - return { element: node.element, incompressible: true }; +function asTreeElement(node: IResourceNode, forceIncompressible: boolean): ICompressedTreeElement { + return { + element: (node.childrenCount === 0 && node.element) ? node.element : node, + children: Iterator.map(node.children, node => asTreeElement(node, false)), + incompressible: !!node.element || forceIncompressible + }; } const enum ViewModelMode { @@ -401,12 +404,6 @@ class ViewModel { get mode(): ViewModelMode { return this._mode; } set mode(mode: ViewModelMode) { - mode = this.provider.treeRendering ? mode : ViewModelMode.List; - - if (mode === this._mode) { - return; - } - this._mode = mode; for (const item of this.items) { @@ -430,7 +427,7 @@ class ViewModel { private disposables = new DisposableStore(); constructor( - private provider: ISCMProvider, + private groups: ISequence, private tree: ObjectTree, private _mode: ViewModelMode, @IEditorService protected editorService: IEditorService, @@ -489,8 +486,8 @@ class ViewModel { setVisible(visible: boolean): void { if (visible) { this.visibilityDisposables = new DisposableStore(); - this.provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables); - this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.provider.groups.elements }); + this.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables); + this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.groups.elements }); if (typeof this.scrollTop === 'number') { this.tree.scrollTop = this.scrollTop; @@ -538,17 +535,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 (let i = this.items.length - 1; i >= 0; i--) { + const node = this.items[i].tree.getNode(uri); - for (const resource of group.elements) { - if (isEqual(uri, resource.sourceUri)) { - this.tree.reveal(resource); - this.tree.setSelection([resource]); - this.tree.setFocus([resource]); - - return; - } + if (node && node.element) { + this.tree.reveal(node.element); + this.tree.setSelection([node.element]); + this.tree.setFocus([node.element]); + return; } } } @@ -753,36 +747,30 @@ 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)); this._register(this.tree); - let mode: ViewModelMode; + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - if (!this.repository.provider.treeRendering) { - mode = ViewModelMode.List; - } else { - mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; + const rootUri = this.repository.provider.rootUri; - const rootUri = this.repository.provider.rootUri; + if (typeof rootUri !== 'undefined') { + const storageMode = this.storageService.get(`scm.repository.viewMode:${rootUri.toString()}`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof rootUri !== 'undefined') { - const storageMode = this.storageService.get(`scm.repository.viewMode:${rootUri.toString()}`, StorageScope.WORKSPACE) as ViewModelMode; - - if (typeof storageMode === 'string') { - mode = storageMode; - } + if (typeof storageMode === 'string') { + mode = storageMode; } } - this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider, this.tree, mode); + this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, mode); this._register(this.viewModel); addClass(this.listContainer, 'file-icon-themable-tree'); @@ -792,10 +780,8 @@ export class RepositoryPanel extends ViewletPanel { this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); - if (this.repository.provider.treeRendering) { - this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel); - this._register(this.toggleViewModelModeAction); - } + this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel); + this._register(this.toggleViewModelModeAction); this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this)); @@ -911,12 +897,16 @@ export class RepositoryPanel extends ViewletPanel { const element = e.element; let actions: IAction[] = []; - if (ResourceTree.isBranchNode(element)) { - actions = this.menus.getResourceFolderContextActions(element.context); - } else if (isSCMResource(element)) { - actions = this.menus.getResourceContextActions(element); - } else { + if (isSCMResourceGroup(element)) { actions = this.menus.getResourceGroupContextActions(element); + } else if (ResourceTree.isResourceNode(element)) { + if (element.element) { + actions = this.menus.getResourceContextActions(element.element); + } else { + actions = this.menus.getResourceFolderContextActions(element.context); + } + } else { + actions = this.menus.getResourceContextActions(element); } this.contextMenuService.showContextMenu({ @@ -927,7 +917,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; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index de4979ae7d9..106a85c5f81 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -62,7 +62,6 @@ export interface ISCMProvider extends IDisposable { readonly acceptInputCommand?: Command; readonly statusBarCommands?: Command[]; readonly onDidChange: Event; - readonly treeRendering: boolean; getOriginalResource(uri: URI): Promise; }