diff --git a/extensions/git/package.json b/extensions/git/package.json index 696286c2c9f..e1ea02f75e3 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -894,6 +894,16 @@ "icon": "$(diff-multiple)", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.copyCommitId", + "title": "%command.timelineCopyCommitId%", + "category": "Git" + }, + { + "command": "git.copyCommitMessage", + "title": "%command.timelineCopyCommitMessage%", + "category": "Git" } ], "continueEditSession": [ @@ -1429,6 +1439,14 @@ { "command": "git.pushRef", "when": "false" + }, + { + "command": "git.copyCommitId", + "when": "false" + }, + { + "command": "git.copyCommitMessage", + "when": "false" } ], "scm/title": [ @@ -1940,6 +1958,23 @@ "group": "navigation@3" } ], + "scm/historyItem/context": [ + { + "command": "git.viewCommit", + "when": "scmProvider == git && scmHistoryItemFileCount != 0 && !listMultiSelection && config.multiDiffEditor.experimental.enabled", + "group": "1_view@1" + }, + { + "command": "git.copyCommitId", + "when": "scmProvider == git && !listMultiSelection", + "group": "3_copy@1" + }, + { + "command": "git.copyCommitMessage", + "when": "scmProvider == git && !listMultiSelection", + "group": "3_copy@2" + } + ], "scm/incomingChanges": [ { "command": "git.fetchRef", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a8f537950c7..5ba9b69d6be 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4233,6 +4233,24 @@ export class CommandCenter { await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); } + @command('git.copyCommitId', { repository: true }) + async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { + return; + } + + env.clipboard.writeText(historyItem.id); + } + + @command('git.copyCommitMessage', { repository: true }) + async copyCommitMessage(repository: Repository, historyItem: SourceControlHistoryItem): Promise { + if (!repository || !historyItem) { + return; + } + + env.clipboard.writeText(historyItem.message); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index eb9a7babc27..7923ffeadec 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -112,6 +112,7 @@ export class MenuId { static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext'); static readonly SCMInputBox = new MenuId('SCMInputBox'); static readonly SCMChangesSeparator = new MenuId('SCMChangesSeparator'); + static readonly SCMChangesContext = new MenuId('SCMChangesContext'); static readonly SCMIncomingChanges = new MenuId('SCMIncomingChanges'); static readonly SCMIncomingChangesContext = new MenuId('SCMIncomingChangesContext'); static readonly SCMIncomingChangesSetting = new MenuId('SCMIncomingChangesSetting'); diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 0c9e1cd7005..33128d4d027 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -14,7 +14,7 @@ import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/c import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMMenus, ISCMProvider, ISCMRepository, ISCMRepositoryMenus, ISCMResource, ISCMResourceGroup, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; function actionEquals(a: IAction, b: IAction): boolean { @@ -256,6 +256,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDisposable { private readonly historyItemMenus = new Map(); + private readonly historyItemMenus2 = new Map(); private readonly disposables = new DisposableStore(); constructor( @@ -266,6 +267,10 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo return this.getOrCreateHistoryItemMenu(historyItem); } + getHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu { + return this.getOrCreateHistoryItemMenu2(historyItem); + } + getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { return historyItemGroup.direction === 'incoming' ? this.menuService.createMenu(MenuId.SCMIncomingChanges, this.contextKeyService) : @@ -304,6 +309,21 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo return result; } + private getOrCreateHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu { + let result = this.historyItemMenus2.get(historyItem); + + if (!result) { + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmHistoryItemFileCount', historyItem.historyItemViewModel.historyItem.statistics?.files ?? 0], + ]); + + result = this.menuService.createMenu(MenuId.SCMChangesContext, contextKeyService); + this.historyItemMenus2.set(historyItem, result); + } + + return result; + } + private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ ['scmHistoryItemGroupHasRemote', !!historyItemGroup.repository.provider.historyProvider.get()?.currentHistoryItemGroup.get()?.remote], diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index dcb51902980..656cda09f10 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -822,21 +822,27 @@ class HistoryItemGroupRenderer implements ICompressibleTreeRenderer { + protected override async runAction(action: IAction, context: SCMHistoryItemTreeElement | SCMHistoryItemViewModelTreeElement): Promise { if (!(action instanceof MenuItemAction)) { return super.runAction(action, context); } const args: (ISCMProvider | ISCMHistoryItem)[] = []; - args.push(context.historyItemGroup.repository.provider); + if (isSCMHistoryItemTreeElement(context)) { + args.push(context.historyItemGroup.repository.provider); + } else { + args.push(context.repository.provider); + } + + const historyItem = isSCMHistoryItemTreeElement(context) ? context : context.historyItemViewModel.historyItem; args.push({ - id: context.id, - parentIds: context.parentIds, - message: context.message, - author: context.author, - icon: context.icon, - timestamp: context.timestamp, - statistics: context.statistics, + id: historyItem.id, + parentIds: historyItem.parentIds, + message: historyItem.message, + author: historyItem.author, + icon: historyItem.icon, + timestamp: historyItem.timestamp, + statistics: historyItem.statistics, } satisfies ISCMHistoryItem); await action.run(...args); @@ -3507,6 +3513,13 @@ export class SCMViewPane extends ViewPane { actionRunner = new HistoryItemActionRunner(); actions = collectContextMenuActions(menu); } + } else if (isSCMHistoryItemViewModelTreeElement(element)) { + const menus = this.scmViewService.menus.getRepositoryMenus(element.repository.provider); + const menu = menus.historyProviderMenu?.getHistoryItemMenu2(element); + if (menu) { + actionRunner = new HistoryItemActionRunner(); + actions = collectContextMenuActions(menu); + } } actionRunner.onWillRun(() => this.tree.domFocus()); diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 45b38e3a6f7..7e6bc762d0a 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -15,6 +15,7 @@ export interface ISCMHistoryProviderMenus { getHistoryItemGroupContextMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; + getHistoryItemMenu2(historyItem: SCMHistoryItemViewModelTreeElement): IMenu; } export interface ISCMHistoryProvider { diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 17d4e15c4fa..38a328d89cf 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -175,6 +175,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.historyItemChanges', "The Source Control incoming/outgoing changes title menu"), proposed: 'contribSourceControlHistoryItemChangesMenu' }, + { + key: 'scm/historyItem/context', + id: MenuId.SCMChangesContext, + description: localize('menus.historyItemContext', "The Source Control history item context menu"), + proposed: 'contribSourceControlHistoryItemChangesMenu' + }, { key: 'scm/incomingChanges', id: MenuId.SCMIncomingChanges,