From e82ab3b36635fa28c7cea5d5f10215c2bd59aab3 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:44:15 +0000 Subject: [PATCH] SCM - add more commands to the repositories view (#274352) * SCM - artifact tree improvements * Add support for compression * Add more commands --- extensions/git/package.json | 35 ++++ extensions/git/src/commands.ts | 94 ++++++--- extensions/git/src/util.ts | 8 +- .../scm/browser/scmRepositoriesViewPane.ts | 193 ++++++++++++------ src/vs/workbench/contrib/scm/browser/util.ts | 8 +- 5 files changed, 251 insertions(+), 87 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 76843999334..87e7dcae439 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1028,6 +1028,18 @@ "icon": "$(target)", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.checkoutDetached", + "title": "%command.graphCheckoutDetached%", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.compareRef", + "title": "%command.graphCompareRef%", + "category": "Git", + "enablement": "!operationInProgress" } ], "continueEditSession": [ @@ -1655,6 +1667,14 @@ { "command": "git.repositories.checkout", "when": "false" + }, + { + "command": "git.repositories.checkoutDetached", + "when": "false" + }, + { + "command": "git.repositories.compareRef", + "when": "false" } ], "scm/title": [ @@ -1862,6 +1882,21 @@ "command": "git.repositories.checkout", "group": "inline@1", "when": "scmProvider == git" + }, + { + "command": "git.repositories.checkout", + "group": "1_checkout@1", + "when": "scmProvider == git" + }, + { + "command": "git.repositories.checkoutDetached", + "group": "1_checkout@2", + "when": "scmProvider == git" + }, + { + "command": "git.repositories.compareRef", + "group": "2_compare@1", + "when": "scmProvider == git" } ], "scm/resourceGroup/context": [ diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 5e60b8de373..7054410ee05 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -14,7 +14,7 @@ import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { DiagnosticSeverityConfig, dispose, fromNow, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util'; +import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -124,6 +124,7 @@ class RefItem implements QuickPickItem { get refName(): string | undefined { return this.ref.name; } get refRemote(): string | undefined { return this.ref.remote; } get shortCommit(): string { return (this.ref.commit || '').substring(0, this.shortCommitLength); } + get commitMessage(): string | undefined { return this.ref.commitDetails?.message; } private _buttons?: QuickInputButton[]; get buttons(): QuickInputButton[] | undefined { return this._buttons; } @@ -3115,10 +3116,13 @@ export class CommandCenter { return; } + const title = `${repository.historyProvider.currentHistoryItemRemoteRef.name} ↔ ${getHistoryItemDisplayName(historyItem)}`; + await this._openChangesBetweenRefs( repository, - repository.historyProvider.currentHistoryItemRemoteRef.name, - historyItem); + repository.historyProvider.currentHistoryItemRemoteRef.revision, + historyItem.id, + title); } @command('git.graph.compareWithMergeBase', { repository: true }) @@ -3127,14 +3131,17 @@ export class CommandCenter { return; } + const title = `${repository.historyProvider.currentHistoryItemBaseRef.name} ↔ ${getHistoryItemDisplayName(historyItem)}`; + await this._openChangesBetweenRefs( repository, repository.historyProvider.currentHistoryItemBaseRef.name, - historyItem); + historyItem.id, + title); } @command('git.graph.compareRef', { repository: true }) - async compareBranch(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { + async compareRef(repository: Repository, historyItem?: SourceControlHistoryItem): Promise { if (!repository || !historyItem) { return; } @@ -3161,39 +3168,30 @@ export class CommandCenter { return; } + const title = `${sourceRef.ref.name} ↔ ${getHistoryItemDisplayName(historyItem)}`; + await this._openChangesBetweenRefs( repository, - sourceRef.ref.name, - historyItem); + sourceRef.ref.commit, + historyItem.id, + title); } - private async _openChangesBetweenRefs(repository: Repository, ref: string | undefined, historyItem: SourceControlHistoryItem | undefined): Promise { - if (!repository || !ref || !historyItem) { + private async _openChangesBetweenRefs(repository: Repository, ref1: string | undefined, ref2: string | undefined, title: string): Promise { + if (!repository || !ref1 || !ref2) { return; } - const ref2 = historyItem.references?.length - ? historyItem.references[0].name - : historyItem.id; - try { - const changes = await repository.diffBetween2(ref, historyItem.id); + const changes = await repository.diffBetween2(ref1, ref2); if (changes.length === 0) { - window.showInformationMessage(l10n.t('There are no changes between "{0}" and "{1}".', ref, ref2)); + window.showInformationMessage(l10n.t('There are no changes between "{0}" and "{1}".', ref1, ref2)); return; } - const refDisplayName = historyItem.references?.length - ? historyItem.references[0].name - : `${historyItem.displayId || historyItem.id} - ${historyItem.subject}`; - - const resources = changes.map(change => toMultiFileDiffEditorUris(change, ref, ref2)); - const title = `${ref} ↔ ${refDisplayName}`; - const multiDiffSourceUri = Uri.from({ - scheme: 'git-ref-compare', - path: `${repository.root}/${ref}..${ref2}` - }); + const multiDiffSourceUri = Uri.from({ scheme: 'git-ref-compare', path: `${repository.root}/${ref1}..${ref2}` }); + const resources = changes.map(change => toMultiFileDiffEditorUris(change, ref1, ref2)); await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, @@ -3201,7 +3199,7 @@ export class CommandCenter { resources }); } catch (err) { - window.showErrorMessage(l10n.t('Failed to open changes between "{0}" and "{1}": {2}', ref, ref2, err.message)); + window.showErrorMessage(l10n.t('Failed to open changes between "{0}" and "{1}": {2}', ref1, ref2, err.message)); } } @@ -5191,6 +5189,50 @@ export class CommandCenter { await this._checkout(repository, { treeish: artifact.name }); } + @command('git.repositories.checkoutDetached', { repository: true }) + async artifactCheckoutDetached(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + await this._checkout(repository, { treeish: artifact.name, detached: true }); + } + + @command('git.repositories.compareRef', { repository: true }) + async artifactCompareWith(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + + const getRefPicks = async () => { + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); + const processors = [ + new RefProcessor(RefType.Head, BranchItem), + new RefProcessor(RefType.RemoteHead, BranchItem), + new RefProcessor(RefType.Tag, BranchItem) + ]; + + const itemsProcessor = new RefItemsProcessor(repository, processors); + return itemsProcessor.processRefs(refs); + }; + + const placeHolder = l10n.t('Select a reference to compare with'); + const sourceRef = await this.pickRef(getRefPicks(), placeHolder); + + if (!(sourceRef instanceof BranchItem) || !sourceRef.ref.commit) { + return; + } + + await this._openChangesBetweenRefs( + repository, + sourceRef.ref.commit, + artifact.id, + `${sourceRef.ref.name} ↔ ${artifact.name}`); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 7600a2e46ac..fcc820c8cd4 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env, SourceControlHistoryItem } from 'vscode'; import { dirname, normalize, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -797,6 +797,12 @@ export function getCommitShortHash(scope: Uri, hash: string): string { return hash.substring(0, shortHashLength); } +export function getHistoryItemDisplayName(historyItem: SourceControlHistoryItem): string { + return historyItem.references?.length + ? historyItem.references[0].name + : historyItem.displayId ?? historyItem.id; +} + export type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint' | 'none'; export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity { diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 307ae6c5579..55a39c09022 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -8,7 +8,7 @@ import { localize } from '../../../../nls.js'; import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browser/ui/list/list.js'; -import { IAsyncDataSource, ITreeEvent, ITreeContextMenuEvent, ITreeNode, ITreeRenderer, ITreeElementRenderDetails } from '../../../../base/browser/ui/tree/tree.js'; +import { IAsyncDataSource, ITreeEvent, ITreeContextMenuEvent, ITreeNode, ITreeElementRenderDetails } from '../../../../base/browser/ui/tree/tree.js'; import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -21,7 +21,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IViewDescriptorService } from '../../../common/views.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { RepositoryActionRunner, RepositoryRenderer } from './scmRepositoryRenderer.js'; -import { collectContextMenuActions, connectPrimaryMenu, getActionViewItemProvider, isSCMArtifactGroupTreeElement, isSCMArtifactTreeElement, isSCMRepository } from './util.js'; +import { collectContextMenuActions, connectPrimaryMenu, getActionViewItemProvider, isSCMArtifactGroupTreeElement, isSCMArtifactNode, isSCMArtifactTreeElement, isSCMRepository } from './util.js'; import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -37,8 +37,14 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IResourceNode, ResourceTree } from '../../../../base/common/resourceTree.js'; +import { URI } from '../../../../base/common/uri.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js'; +import { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; +import { ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js'; -type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement; +type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement | IResourceNode; class ListDelegate implements IListVirtualDelegate { @@ -51,7 +57,7 @@ class ListDelegate implements IListVirtualDelegate { return RepositoryRenderer.TEMPLATE_ID; } else if (isSCMArtifactGroupTreeElement(element)) { return ArtifactGroupRenderer.TEMPLATE_ID; - } else if (isSCMArtifactTreeElement(element)) { + } else if (isSCMArtifactTreeElement(element) || isSCMArtifactNode(element)) { return ArtifactRenderer.TEMPLATE_ID; } else { throw new Error('Invalid tree element'); @@ -66,7 +72,7 @@ interface ArtifactGroupTemplate { readonly templateDisposable: IDisposable; } -class ArtifactGroupRenderer implements ITreeRenderer { +class ArtifactGroupRenderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'artifactGroup'; get templateId(): string { return ArtifactGroupRenderer.TEMPLATE_ID; } @@ -106,11 +112,16 @@ class ArtifactGroupRenderer implements ITreeRenderer, FuzzyScore>, index: number, templateData: ArtifactGroupTemplate, details?: ITreeElementRenderDetails): void { + throw new Error('Should never happen since node is incompressible'); + } + disposeElement(element: ITreeNode, index: number, templateData: ArtifactGroupTemplate, details?: ITreeElementRenderDetails): void { templateData.elementDisposables.clear(); } disposeTemplate(templateData: ArtifactGroupTemplate): void { + templateData.elementDisposables.dispose(); templateData.templateDisposable.dispose(); } } @@ -122,7 +133,7 @@ interface ArtifactTemplate { readonly templateDisposable: IDisposable; } -class ArtifactRenderer implements ITreeRenderer { +class ArtifactRenderer implements ICompressibleTreeRenderer, FuzzyScore, ArtifactTemplate> { static readonly TEMPLATE_ID = 'artifact'; get templateId(): string { return ArtifactRenderer.TEMPLATE_ID; } @@ -147,28 +158,49 @@ class ArtifactRenderer implements ITreeRenderer, index: number, templateData: ArtifactTemplate): void { - const provider = node.element.repository.provider; - const artifact = node.element.artifact; + renderElement(nodeOrElement: ITreeNode, FuzzyScore>, index: number, templateData: ArtifactTemplate): void { + const artifactOrFolder = nodeOrElement.element; - const artifactGroup = node.element.group; - const artifactGroupIcon = ThemeIcon.isThemeIcon(artifactGroup.icon) - ? `$(${artifactGroup.icon.id}) ` : ''; + if (isSCMArtifactNode(artifactOrFolder)) { + // Folder + templateData.label.setLabel(`$(folder) ${basename(artifactOrFolder.uri)}`); - templateData.label.setLabel(`${artifactGroupIcon}${artifact.name}`, artifact.description); + templateData.actionBar.setActions([]); + templateData.actionBar.context = undefined; + } else { + // Artifact + const artifact = artifactOrFolder.artifact; + const artifactIcon = ThemeIcon.isThemeIcon(artifactOrFolder.group.icon) + ? `$(${artifactOrFolder.group.icon.id}) ` + : ''; - const repositoryMenus = this._scmViewService.menus.getRepositoryMenus(provider); - templateData.elementDisposables.add(connectPrimaryMenu(repositoryMenus.getArtifactMenu(artifactGroup), primary => { - templateData.actionBar.setActions(primary); - }, 'inline', provider)); - templateData.actionBar.context = artifact; + const artifactLabel = artifact.name.split('/').pop() ?? artifact.name; + templateData.label.setLabel(`${artifactIcon}${artifactLabel}`, artifact.description); + + const provider = artifactOrFolder.repository.provider; + const repositoryMenus = this._scmViewService.menus.getRepositoryMenus(provider); + templateData.elementDisposables.add(connectPrimaryMenu(repositoryMenus.getArtifactMenu(artifactOrFolder.group), primary => { + templateData.actionBar.setActions(primary); + }, 'inline', provider)); + templateData.actionBar.context = artifact; + } } - disposeElement(element: ITreeNode, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void { + renderCompressedElements(node: ITreeNode>, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void { + const compressed = node.element as ICompressedTreeNode>; + const folder = compressed.elements[compressed.elements.length - 1]; + templateData.label.setLabel(`$(folder) ${folder.uri.fsPath.substring(1)}`); + + templateData.actionBar.setActions([]); + templateData.actionBar.context = undefined; + } + + disposeElement(element: ITreeNode, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void { templateData.elementDisposables.clear(); } disposeTemplate(templateData: ArtifactTemplate): void { + templateData.elementDisposables.dispose(); templateData.templateDisposable.dispose(); } } @@ -204,17 +236,26 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource ({ - repository, - group: inputOrElement.artifactGroup, - artifact, - type: 'artifact' - })); - } else if (isSCMArtifactTreeElement(inputOrElement)) { - return []; - } else { - return []; - } + // Create resource tree for artifacts + const artifactsTree = new ResourceTree(inputOrElement); + for (const artifact of artifacts) { + artifactsTree.add(URI.from({ + scheme: 'scm-artifact', path: artifact.name + }), { + repository, + group: inputOrElement.artifactGroup, + artifact, + type: 'artifact' + }); + } + + return Iterable.map(artifactsTree.root.children, node => node.element ?? node); + } else if (isSCMArtifactNode(inputOrElement)) { + return Iterable.map(inputOrElement.children, + node => node.element && node.childrenCount === 0 ? node.element : node); + } else if (isSCMArtifactTreeElement(inputOrElement)) { } + + return []; } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { @@ -238,6 +279,8 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource 0; } else { return false; } @@ -252,12 +295,24 @@ class RepositoryTreeIdentityProvider implements IIdentityProvider { return `artifactGroup:${element.repository.provider.id}/${element.artifactGroup.id}`; } else if (isSCMArtifactTreeElement(element)) { return `artifact:${element.repository.provider.id}/${element.group.id}/${element.artifact.id}`; + } else if (isSCMArtifactNode(element)) { + return `artifactFolder:${element.context.repository.provider.id}/${element.context.artifactGroup.id}/${element.uri.fsPath}`; } else { throw new Error('Invalid tree element'); } } } +class RepositoriesTreeCompressionDelegate implements ITreeCompressionDelegate { + isIncompressible(element: TreeElement): boolean { + if (ResourceTree.isResourceNode(element)) { + return element.childrenCount === 0 || !element.parent || !element.parent.parent; + } + + return true; + } +} + export class SCMRepositoriesViewPane extends ViewPane { private tree!: WorkbenchCompressibleAsyncDataTree; @@ -361,16 +416,12 @@ export class SCMRepositoriesViewPane extends ViewPane { this.treeDataSource = this.instantiationService.createInstance(RepositoryTreeDataSource); this._register(this.treeDataSource); - const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService); - this.tree = this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, 'SCM Repositories', container, new ListDelegate(), - { - isIncompressible: () => true - }, + new RepositoriesTreeCompressionDelegate(), [ this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ArtifactGroupRenderer), @@ -389,9 +440,20 @@ export class SCMRepositoriesViewPane extends ViewPane { } // Explorer mode + // Expand artifact folders with one child only + if (isSCMArtifactNode(e)) { + if (e.childrenCount !== 1) { + return true; + } + + // Check if the only child is a leaf node + const firstChild = Iterable.first(e.children); + return firstChild?.element !== undefined; + } + return true; }, - compressionEnabled: compressionEnabled.get(), + compressionEnabled: true, overrideStyles: this.getLocationBasedColors().listOverrideStyles, multipleSelectionSupport: this.scmViewService.selectionModeConfig.get() === 'multiple', expandOnDoubleClick: true, @@ -456,29 +518,42 @@ export class SCMRepositoriesViewPane extends ViewPane { return; } - if (!isSCMRepository(e.element)) { - return; + if (isSCMRepository(e.element)) { + // Repository + const provider = e.element.provider; + const menus = this.scmViewService.menus.getRepositoryMenus(provider); + const menu = menus.getRepositoryContextMenu(e.element); + const actions = collectContextMenuActions(menu); + + const disposables = new DisposableStore(); + const actionRunner = new RepositoryActionRunner(() => { + return this.getTreeSelection(); + }); + disposables.add(actionRunner); + disposables.add(actionRunner.onWillRun(() => this.tree.domFocus())); + + this.contextMenuService.showContextMenu({ + actionRunner, + getAnchor: () => e.anchor, + getActions: () => actions, + getActionsContext: () => provider, + onHide: () => disposables.dispose() + }); + } else if (isSCMArtifactTreeElement(e.element)) { + // Artifact + const provider = e.element.repository.provider; + const artifact = e.element.artifact; + + const menus = this.scmViewService.menus.getRepositoryMenus(provider); + const menu = menus.getArtifactMenu(e.element.group); + const actions = collectContextMenuActions(menu, provider); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + getActionsContext: () => artifact + }); } - - const provider = e.element.provider; - const menus = this.scmViewService.menus.getRepositoryMenus(provider); - const menu = menus.getRepositoryContextMenu(e.element); - const actions = collectContextMenuActions(menu); - - const disposables = new DisposableStore(); - const actionRunner = new RepositoryActionRunner(() => { - return this.getTreeSelection(); - }); - disposables.add(actionRunner); - disposables.add(actionRunner.onWillRun(() => this.tree.domFocus())); - - this.contextMenuService.showContextMenu({ - actionRunner, - getAnchor: () => e.anchor, - getActions: () => actions, - getActionsContext: () => provider, - onHide: () => disposables.dispose() - }); } private onTreeSelectionChange(e: ITreeEvent): void { @@ -615,6 +690,8 @@ export class SCMRepositoriesViewPane extends ViewPane { return e; } else if (isSCMArtifactGroupTreeElement(e) || isSCMArtifactTreeElement(e)) { return e.repository; + } else if (isSCMArtifactNode(e)) { + return e.context.repository; } else { throw new Error('Invalid tree element'); } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 1783c012ef4..50670663afc 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -70,6 +70,10 @@ export function isSCMArtifactGroupTreeElement(element: unknown): element is SCMA return (element as SCMArtifactGroupTreeElement).type === 'artifactGroup'; } +export function isSCMArtifactNode(element: unknown): element is IResourceNode { + return ResourceTree.isResourceNode(element) && isSCMArtifactGroupTreeElement(element.context); +} + export function isSCMArtifactTreeElement(element: unknown): element is SCMArtifactTreeElement { return (element as SCMArtifactTreeElement).type === 'artifact'; } @@ -104,8 +108,8 @@ export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], s return menu.onDidChange(updateActions); } -export function collectContextMenuActions(menu: IMenu): IAction[] { - return getContextMenuActions(menu.getActions({ shouldForwardArgs: true }), 'inline').secondary; +export function collectContextMenuActions(menu: IMenu, arg?: unknown): IAction[] { + return getContextMenuActions(menu.getActions({ arg, shouldForwardArgs: true }), 'inline').secondary; } export class StatusBarAction extends Action {