diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index b4cbdad0986..6587b65421d 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -55,7 +55,7 @@ "command.syncRebase": "Sync (Rebase)", "command.publish": "Publish Branch", "command.showOutput": "Show Git Output", - "command.ignore": "Add File to .gitignore", + "command.ignore": "Add to .gitignore", "command.stashIncludeUntracked": "Stash (Include Untracked)", "command.stash": "Stash", "command.stashPop": "Pop Stash...", diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index e4420c078f1..094c49dc2e8 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -6,57 +6,61 @@ import { memoize } from 'vs/base/common/decorators'; import * as paths from 'vs/base/common/path'; import { Iterator } from 'vs/base/common/iterator'; -import { relativePath } from 'vs/base/common/resources'; +import { relativePath, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -export interface ILeafNode { - readonly path: string; +export interface ILeafNode { + readonly uri: URI; + readonly relativePath: string; readonly name: string; readonly element: T; + readonly context: C; } -export interface IBranchNode { - readonly path: string; +export interface IBranchNode { + readonly uri: URI; + readonly relativePath: string; readonly name: string; readonly size: number; - readonly children: Iterator>; - readonly parent: IBranchNode | undefined; - get(childName: string): INode | undefined; + readonly children: Iterator>; + readonly parent: IBranchNode | undefined; + readonly context: C; + get(childName: string): INode | undefined; } -export type INode = IBranchNode | ILeafNode; +export type INode = IBranchNode | ILeafNode; // Internals -class Node { +class Node { @memoize - get name(): string { return paths.posix.basename(this.path); } + get name(): string { return paths.posix.basename(this.relativePath); } - constructor(readonly path: string) { } + constructor(readonly uri: URI, readonly relativePath: string, readonly context: C) { } } -class BranchNode extends Node implements IBranchNode { +class BranchNode extends Node implements IBranchNode { - private _children = new Map | LeafNode>(); + private _children = new Map | LeafNode>(); get size(): number { return this._children.size; } - get children(): Iterator | LeafNode> { + get children(): Iterator | LeafNode> { return Iterator.fromIterableIterator(this._children.values()); } - constructor(path: string, readonly parent: IBranchNode | undefined = undefined) { - super(path); + constructor(uri: URI, relativePath: string, context: C, readonly parent: IBranchNode | undefined = undefined) { + super(uri, relativePath, context); } - get(path: string): BranchNode | LeafNode | undefined { + get(path: string): BranchNode | LeafNode | undefined { return this._children.get(path); } - set(path: string, child: BranchNode | LeafNode): void { + set(path: string, child: BranchNode | LeafNode): void { this._children.set(path, child); } @@ -65,22 +69,32 @@ class BranchNode extends Node implements IBranchNode { } } -class LeafNode extends Node implements ILeafNode { +class LeafNode extends Node implements ILeafNode { - constructor(path: string, readonly element: T) { - super(path); + constructor(uri: URI, path: string, context: C, readonly element: T) { + super(uri, path, context); } } -export class ResourceTree> { +function collect(node: INode, result: T[]): T[] { + if (ResourceTree.isBranchNode(node)) { + Iterator.forEach(node.children, child => collect(child, result)); + } else { + result.push(node.element); + } - readonly root = new BranchNode(''); + return result; +} - static isBranchNode(obj: any): obj is IBranchNode { +export class ResourceTree, C> { + + readonly root: BranchNode; + + static isBranchNode(obj: any): obj is IBranchNode { return obj instanceof BranchNode; } - static getRoot(node: IBranchNode): IBranchNode { + static getRoot(node: IBranchNode): IBranchNode { while (node.parent) { node = node.parent; } @@ -88,13 +102,19 @@ export class ResourceTree> { return node; } - constructor(private rootURI: URI = URI.file('/')) { } + static collect(node: INode): T[] { + return collect(node, []); + } + + constructor(context: C, rootURI: URI = URI.file('/')) { + this.root = new BranchNode(rootURI, '', context); + } add(uri: URI, element: T): void { - const key = relativePath(this.rootURI, uri) || uri.fsPath; + const key = relativePath(this.root.uri, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); let node = this.root; - let path = this.root.path; + let path = ''; for (let i = 0; i < parts.length; i++) { const name = parts[i]; @@ -104,10 +124,10 @@ export class ResourceTree> { if (!child) { if (i < parts.length - 1) { - child = new BranchNode(path, node); + child = new BranchNode(joinPath(this.root.uri, path), path, this.root.context, node); node.set(name, child); } else { - child = new LeafNode(path, element); + child = new LeafNode(uri, path, this.root.context, element); node.set(name, child); return; } @@ -119,7 +139,7 @@ export class ResourceTree> { } // replace - node.set(name, new LeafNode(path, element)); + node.set(name, new LeafNode(uri, path, this.root.context, element)); return; } else if (i === parts.length - 1) { throw new Error('Inconsistent tree: can\'t override branch with leaf.'); @@ -130,12 +150,12 @@ export class ResourceTree> { } delete(uri: URI): T | undefined { - const key = relativePath(this.rootURI, uri) || uri.fsPath; + const key = relativePath(this.root.uri, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); return this._delete(this.root, parts, 0); } - private _delete(node: BranchNode, parts: string[], index: number): T | undefined { + private _delete(node: BranchNode, parts: string[], index: number): T | undefined { const name = parts[index]; const child = node.get(name); diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 6bc77542499..d3050bcd990 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -9,24 +9,24 @@ import { URI } from 'vs/base/common/uri'; suite('ResourceTree', function () { test('ctor', function () { - const tree = new ResourceTree(); + const tree = new ResourceTree(null); assert(ResourceTree.isBranchNode(tree.root)); assert.equal(tree.root.size, 0); }); test('simple', function () { - const tree = new ResourceTree(); + 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); - let foo = tree.root.get('foo') as IBranchNode; + let foo = tree.root.get('foo') as IBranchNode; assert(foo); assert(ResourceTree.isBranchNode(foo)); assert.equal(foo.size, 1); - let bar = foo.get('bar.txt') as ILeafNode; + let bar = foo.get('bar.txt') as ILeafNode; assert(bar); assert(!ResourceTree.isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); @@ -34,14 +34,14 @@ suite('ResourceTree', function () { tree.add(URI.file('/hello.txt'), 'hello contents'); assert.equal(tree.root.size, 2); - let hello = tree.root.get('hello.txt') as ILeafNode; + let hello = tree.root.get('hello.txt') as ILeafNode; 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; + hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index dd900f7dd82..f6937fd9e01 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -91,6 +91,7 @@ .scm-viewlet .monaco-list-row .resource-group { display: flex; height: 100%; + align-items: center; } .scm-viewlet .monaco-list-row .resource-group > .name { @@ -99,6 +100,7 @@ font-weight: bold; overflow: hidden; text-overflow: ellipsis; + text-decoration: underline; } .scm-viewlet .monaco-list-row .resource { @@ -125,6 +127,7 @@ .scm-viewlet .monaco-list-row .resource-group > .count { padding: 0 8px; + display: flex; } .scm-viewlet .monaco-list-row .resource > .decoration-icon { diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6596fb964f8..1d3d1212c13 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -48,14 +48,14 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; -type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; +type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; interface ResourceGroupTemplate { - name: HTMLElement; - count: CountBadge; - actionBar: ActionBar; + readonly name: HTMLElement; + readonly count: CountBadge; + readonly actionBar: ActionBar; elementDisposables: IDisposable; - disposables: IDisposable; + readonly disposables: IDisposable; } class ResourceGroupRenderer implements ICompressibleTreeRenderer { @@ -130,23 +130,36 @@ class MultipleSelectionActionRunner extends ActionRunner { super(); } - runAction(action: IAction, context: ISCMResource): Promise { - if (action instanceof MenuItemAction) { - const selection = this.getSelectedResources(); - const filteredSelection = selection.filter(s => s !== context); - - if (selection.length === filteredSelection.length || selection.length === 1) { - return action.run(context); - } - - return action.run(context, ...filteredSelection); + runAction(action: IAction, context: ISCMResource | IBranchNode): Promise { + if (!(action instanceof MenuItemAction)) { + return super.runAction(action, context); } - return super.runAction(action, context); + // TODO + // const resources = ResourceTree.isBranchNode(context) + // ? ResourceTree.collect(context) + // : [context]; + + const selection = this.getSelectedResources(); + + if (ResourceTree.isBranchNode(context)) { + const selection = this.getSelectedResources(); + const resources = ResourceTree.collect(context); + + return action.run(...resources, ...selection); + } + + const filteredSelection = selection.filter(s => s !== context); + + if (selection.length === filteredSelection.length || selection.length === 1) { + return action.run(context); + } + + return action.run(context, ...filteredSelection); } } -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -176,16 +189,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 resource = node.element; + const resourceOrFolder = node.element; const theme = this.themeService.getTheme(); - const icon = ResourceTree.isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); + const icon = !ResourceTree.isBranchNode(resourceOrFolder) && (theme.type === LIGHT ? resourceOrFolder.decorations.icon : resourceOrFolder.decorations.iconDark); - const uri = ResourceTree.isBranchNode(resource) ? URI.file(resource.path) : resource.sourceUri; - const fileKind = ResourceTree.isBranchNode(resource) ? FileKind.FOLDER : FileKind.FILE; + const uri = ResourceTree.isBranchNode(resourceOrFolder) ? resourceOrFolder.uri : resourceOrFolder.sourceUri; + const fileKind = ResourceTree.isBranchNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); template.fileLabel.setFile(uri, { @@ -196,21 +209,19 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): 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 { template.elementDisposables.dispose(); - const disposables = new DisposableStore(); - const compressed = node.element as ICompressedTreeNode>; - const branchNode = compressed.elements[compressed.elements.length - 1]; + const elementDisposables = new DisposableStore(); + const compressed = node.element as ICompressedTreeNode>; + const folder = compressed.elements[compressed.elements.length - 1]; const label = compressed.elements.map(e => e.name).join('/'); - const uri = URI.file(branchNode.path); const fileKind = FileKind.FOLDER; - template.fileLabel.setResource({ resource: uri, name: label }, { + template.fileLabel.setResource({ resource: folder.uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind, matches: createMatches(node.filterData) }); template.actionBar.clear(); - template.actionBar.context = 'what'; // TODO + template.actionBar.context = folder; - const viewModel = this.viewModelProvider(); - const group = viewModel.getResourceGroupOf(branchNode); - - if (group) { - disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(group), template.actionBar)); - } + elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(folder.context), template.actionBar)); + removeClass(template.name, 'strike-through'); + removeClass(template.element, 'faded'); template.decorationIcon.style.display = 'none'; template.decorationIcon.style.backgroundImage = ''; - template.element.setAttribute('data-tooltip', branchNode.path); - template.elementDisposables = disposables; + template.element.setAttribute('data-tooltip', ''); + template.elementDisposables = elementDisposables; } - disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + disposeCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); } @@ -331,25 +343,27 @@ export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati } } -const scmResourceIdentityProvider = new class implements IIdentityProvider { - getId(e: TreeElement): string { - if (ResourceTree.isBranchNode(e)) { - return e.path; - } else if (isSCMResource(e)) { - const group = e.resourceGroup; +class SCMResourceIdentityProvider implements IIdentityProvider { + + getId(element: TreeElement): string { + if (ResourceTree.isBranchNode(element)) { + const group = element.context; + return `${group.provider.contextValue}/${group.id}/$FOLDER/${element.uri.toString()}`; + } else if (isSCMResource(element)) { + const group = element.resourceGroup; const provider = group.provider; - return `${provider.contextValue}/${group.id}/${e.sourceUri.toString()}`; + return `${provider.contextValue}/${group.id}/${element.sourceUri.toString()}`; } else { - const provider = e.provider; - return `${provider.contextValue}/${e.id}`; + const provider = element.provider; + return `${provider.contextValue}/${element.id}`; } } -}; +} interface IGroupItem { readonly group: ISCMResourceGroup; readonly resources: ISCMResource[]; - readonly tree: ResourceTree; + readonly tree: ResourceTree; readonly disposable: IDisposable; } @@ -361,7 +375,7 @@ function groupItemAsTreeElement(item: IGroupItem, mode: ViewModelMode): ICompres return { element: item.group, children, incompressible: true }; } -function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { +function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { if (ResourceTree.isBranchNode(node)) { return { element: node, @@ -406,7 +420,7 @@ class ViewModel { const itemsToInsert: IGroupItem[] = []; for (const group of toInsert) { - const tree = new ResourceTree(group.provider.rootUri || URI.file('/')); + const tree = new ResourceTree(group, group.provider.rootUri || URI.file('/')); const resources: ISCMResource[] = [...group.elements]; const disposable = combinedDisposable( group.onDidChange(() => this.tree.refilter()), @@ -464,18 +478,6 @@ class ViewModel { } } - getResourceGroupOf(node: IBranchNode): ISCMResourceGroup | undefined { - const root = ResourceTree.getRoot(node); - - for (const item of this.items) { - if (item.tree.root === root) { - return item.group; - } - } - - return undefined; - } - dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -656,6 +658,7 @@ export class RepositoryPanel extends ViewletPanel { const filter = new SCMTreeFilter(); const sorter = new SCMTreeSorter(); const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); + const identityProvider = new SCMResourceIdentityProvider(); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleObjectTree, @@ -664,7 +667,7 @@ export class RepositoryPanel extends ViewletPanel { delegate, renderers, { - identityProvider: scmResourceIdentityProvider, + identityProvider, horizontalScrolling: false, filter, sorter, @@ -786,11 +789,7 @@ export class RepositoryPanel extends ViewletPanel { let actions: IAction[] = []; if (ResourceTree.isBranchNode(element)) { - const group = this.viewModel.getResourceGroupOf(element); - - if (group) { - actions = this.menus.getResourceFolderContextActions(group); - } + actions = this.menus.getResourceFolderContextActions(element.context); } else if (isSCMResource(element)) { actions = this.menus.getResourceContextActions(element); } else {