From bcd7bcedff3844ccd3bd2b57fa5132defc0e4539 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 09:49:35 +0200 Subject: [PATCH] wip: scm folder menus --- extensions/git/package.json | 47 ++++++++ src/vs/base/common/resourceTree.ts | 15 ++- src/vs/platform/actions/common/actions.ts | 1 + .../api/common/menusExtensionPoint.ts | 3 +- src/vs/workbench/contrib/scm/browser/menus.ts | 43 ++++---- .../contrib/scm/browser/repositoryPanel.ts | 101 +++++++++++------- 6 files changed, 150 insertions(+), 60 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 66b8731c6d9..efa8d35b724 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -850,6 +850,53 @@ "group": "inline" } ], + "scm/resourceFolder/context": [ + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == merge", + "group": "1_modification" + }, + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == merge", + "group": "inline" + }, + { + "command": "git.unstage", + "when": "scmProvider == git && scmResourceGroup == index", + "group": "1_modification" + }, + { + "command": "git.unstage", + "when": "scmProvider == git && scmResourceGroup == index", + "group": "inline" + }, + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == workingTree", + "group": "1_modification" + }, + { + "command": "git.clean", + "when": "scmProvider == git && scmResourceGroup == workingTree && !gitFreshRepository", + "group": "1_modification" + }, + { + "command": "git.clean", + "when": "scmProvider == git && scmResourceGroup == workingTree && !gitFreshRepository", + "group": "inline" + }, + { + "command": "git.stage", + "when": "scmProvider == git && scmResourceGroup == workingTree", + "group": "inline" + }, + { + "command": "git.ignore", + "when": "scmProvider == git && scmResourceGroup == workingTree", + "group": "1_modification@3" + } + ], "scm/resourceState/context": [ { "command": "git.stage", diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 5821f9ce5fe..e4420c078f1 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -20,6 +20,7 @@ export interface IBranchNode { readonly name: string; readonly size: number; readonly children: Iterator>; + readonly parent: IBranchNode | undefined; get(childName: string): INode | undefined; } @@ -47,6 +48,10 @@ class BranchNode extends Node implements IBranchNode { return Iterator.fromIterableIterator(this._children.values()); } + constructor(path: string, readonly parent: IBranchNode | undefined = undefined) { + super(path); + } + get(path: string): BranchNode | LeafNode | undefined { return this._children.get(path); } @@ -75,6 +80,14 @@ export class ResourceTree> { return obj instanceof BranchNode; } + static getRoot(node: IBranchNode): IBranchNode { + while (node.parent) { + node = node.parent; + } + + return node; + } + constructor(private rootURI: URI = URI.file('/')) { } add(uri: URI, element: T): void { @@ -91,7 +104,7 @@ export class ResourceTree> { if (!child) { if (i < parts.length - 1) { - child = new BranchNode(path); + child = new BranchNode(path, node); node.set(name, child); } else { child = new LeafNode(path, element); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 7a409b98a5e..180b78f03e1 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -87,6 +87,7 @@ export const enum MenuId { ProblemsPanelContext, SCMChangeContext, SCMResourceContext, + SCMResourceFolderContext, SCMResourceGroupContext, SCMSourceControl, SCMTitle, diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index cb6cddcb57d..0dd6facb692 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -39,8 +39,9 @@ namespace schema { case 'menuBar/file': return MenuId.MenubarFileMenu; case 'scm/title': return MenuId.SCMTitle; case 'scm/sourceControl': return MenuId.SCMSourceControl; - case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext; case 'scm/resourceState/context': return MenuId.SCMResourceContext; + case 'scm/resourceFolder/context': return MenuId.SCMResourceFolderContext; + case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext; case 'scm/change/title': return MenuId.SCMChangeContext; case 'statusBar/windowIndicator': return MenuId.StatusBarWindowIndicatorMenu; case 'view/title': return MenuId.ViewTitle; diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 02a31f096d2..327801803c2 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/scmViewlet'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; @@ -20,13 +20,15 @@ function actionEquals(a: IAction, b: IAction): boolean { return a.id === b.id; } -interface ISCMResourceGroupMenuEntry extends IDisposable { +interface ISCMResourceGroupMenuEntry { readonly group: ISCMResourceGroup; + readonly disposable: IDisposable; } interface ISCMMenus { readonly resourceGroupMenu: IMenu; readonly resourceMenu: IMenu; + readonly resourceFolderMenu: IMenu; } export function getSCMResourceContextKey(resource: ISCMResourceGroup | ISCMResource): string { @@ -48,7 +50,7 @@ export class SCMMenus implements IDisposable { private readonly resourceGroupMenuEntries: ISCMResourceGroupMenuEntry[] = []; private readonly resourceGroupMenus = new Map(); - private readonly disposables: IDisposable[] = []; + private readonly disposables = new DisposableStore(); constructor( provider: ISCMProvider | undefined, @@ -68,7 +70,7 @@ export class SCMMenus implements IDisposable { } this.titleMenu = this.menuService.createMenu(MenuId.SCMTitle, this.contextKeyService); - this.disposables.push(this.titleMenu); + this.disposables.add(this.titleMenu); this.titleMenu.onDidChange(this.updateTitleActions, this, this.disposables); this.updateTitleActions(); @@ -109,6 +111,10 @@ export class SCMMenus implements IDisposable { return this.getActions(MenuId.SCMResourceContext, resource).secondary; } + getResourceFolderContextActions(group: ISCMResourceGroup): IAction[] { + return this.getActions(MenuId.SCMResourceFolderContext, group).secondary; + } + private getActions(menuId: MenuId, resource: ISCMResourceGroup | ISCMResource): { primary: IAction[]; secondary: IAction[]; } { const contextKeyService = this.contextKeyService.createScoped(); contextKeyService.createKey('scmResourceGroup', getSCMResourceContextKey(resource)); @@ -141,6 +147,14 @@ export class SCMMenus implements IDisposable { return this.resourceGroupMenus.get(group)!.resourceMenu; } + getResourceFolderMenu(group: ISCMResourceGroup): IMenu { + if (!this.resourceGroupMenus.has(group)) { + throw new Error('SCM Resource Group menu not found'); + } + + return this.resourceGroupMenus.get(group)!.resourceFolderMenu; + } + private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { const menuEntriesToInsert = toInsert.map(group => { const contextKeyService = this.contextKeyService.createScoped(); @@ -149,30 +163,23 @@ export class SCMMenus implements IDisposable { const resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService); const resourceMenu = this.menuService.createMenu(MenuId.SCMResourceContext, contextKeyService); + const resourceFolderMenu = this.menuService.createMenu(MenuId.SCMResourceFolderContext, contextKeyService); + const disposable = combinedDisposable(contextKeyService, resourceGroupMenu, resourceMenu, resourceFolderMenu); - this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu }); - - return { - group, - dispose() { - contextKeyService.dispose(); - resourceGroupMenu.dispose(); - resourceMenu.dispose(); - } - }; + this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu, resourceFolderMenu }); + return { group, disposable }; }); const deleted = this.resourceGroupMenuEntries.splice(start, deleteCount, ...menuEntriesToInsert); for (const entry of deleted) { this.resourceGroupMenus.delete(entry.group); - entry.dispose(); + entry.disposable.dispose(); } } dispose(): void { - dispose(this.disposables); - dispose(this.resourceGroupMenuEntries); - this.resourceGroupMenus.clear(); + this.disposables.dispose(); + this.resourceGroupMenuEntries.forEach(e => e.disposable.dispose()); } } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6c361cd622d..6596fb964f8 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -54,8 +54,8 @@ interface ResourceGroupTemplate { name: HTMLElement; count: CountBadge; actionBar: ActionBar; - elementDisposable: IDisposable; - dispose: () => void; + elementDisposables: IDisposable; + disposables: IDisposable; } class ResourceGroupRenderer implements ICompressibleTreeRenderer { @@ -77,18 +77,14 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer { - actionBar.dispose(); - styler.dispose(); - } - }; + return { name, count, actionBar, elementDisposables, disposables }; } renderElement(node: ITreeNode, index: number, template: ResourceGroupTemplate): void { - template.elementDisposable.dispose(); + template.elementDisposables.dispose(); const group = node.element; template.name.textContent = group.label; @@ -102,7 +98,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { @@ -110,11 +106,11 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, index: number, template: ResourceGroupTemplate): void { - template.elementDisposable.dispose(); + template.elementDisposables.dispose(); } disposeTemplate(template: ResourceGroupTemplate): void { - template.dispose(); + template.disposables.dispose(); } } @@ -124,8 +120,8 @@ interface ResourceTemplate { fileLabel: IResourceLabel; decorationIcon: HTMLElement; actionBar: ActionBar; - elementDisposable: IDisposable; - dispose: () => void; + elementDisposables: IDisposable; + disposables: IDisposable; } class MultipleSelectionActionRunner extends ActionRunner { @@ -175,18 +171,15 @@ class ResourceRenderer implements ICompressibleTreeRenderer { - actionBar.dispose(); - fileLabel.dispose(); - } - }; + return { element, name, fileLabel, decorationIcon, actionBar, elementDisposables: Disposable.None, disposables }; } renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { - template.elementDisposable.dispose(); + template.elementDisposables.dispose(); + const elementDisposables = new DisposableStore(); const resource = node.element; const theme = this.themeService.getTheme(); const icon = ResourceTree.isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); @@ -201,13 +194,18 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { - template.elementDisposable.dispose(); + template.elementDisposables.dispose(); + const disposables = new DisposableStore(); const compressed = node.element as ICompressedTreeNode>; - const resource = compressed.elements[compressed.elements.length - 1]; + const branchNode = compressed.elements[compressed.elements.length - 1]; const label = compressed.elements.map(e => e.name).join('/'); - const uri = URI.file(resource.path); + const uri = URI.file(branchNode.path); const fileKind = FileKind.FOLDER; + template.fileLabel.setResource({ resource: uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind, matches: createMatches(node.filterData) }); - template.actionBar.clear(); - template.actionBar.context = resource; - const disposables = new DisposableStore(); + template.actionBar.clear(); + template.actionBar.context = 'what'; // TODO + + const viewModel = this.viewModelProvider(); + const group = viewModel.getResourceGroupOf(branchNode); + + if (group) { + disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(group), template.actionBar)); + } template.decorationIcon.style.display = 'none'; template.decorationIcon.style.backgroundImage = ''; - template.element.setAttribute('data-tooltip', resource.path); - template.elementDisposable = disposables; - + template.element.setAttribute('data-tooltip', branchNode.path); + template.elementDisposables = disposables; } disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { - template.elementDisposable.dispose(); + template.elementDisposables.dispose(); } disposeTemplate(template: ResourceTemplate): void { - template.elementDisposable.dispose(); - template.dispose(); + template.elementDisposables.dispose(); + template.disposables.dispose(); } } @@ -459,6 +464,18 @@ 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(); @@ -766,10 +783,14 @@ export class RepositoryPanel extends ViewletPanel { } const element = e.element; - let actions: IAction[]; + let actions: IAction[] = []; if (ResourceTree.isBranchNode(element)) { - actions = []; + const group = this.viewModel.getResourceGroupOf(element); + + if (group) { + actions = this.menus.getResourceFolderContextActions(group); + } } else if (isSCMResource(element)) { actions = this.menus.getResourceContextActions(element); } else {