SCM - add more commands to the repositories view (#274352)

* SCM - artifact tree improvements

* Add support for compression

* Add more commands
This commit is contained in:
Ladislau Szomoru
2025-10-31 14:44:15 +00:00
committed by GitHub
parent 4b1a7b8b8c
commit e82ab3b366
5 changed files with 251 additions and 87 deletions

View File

@@ -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": [

View File

@@ -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<void> {
async compareRef(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<void> {
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<void> {
if (!repository || !ref || !historyItem) {
private async _openChangesBetweenRefs(repository: Repository, ref1: string | undefined, ref2: string | undefined, title: string): Promise<void> {
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<void> {
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<void> {
if (!repository || !artifact) {
return;
}
const config = workspace.getConfiguration('git');
const showRefDetails = config.get<boolean>('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<any>;

View File

@@ -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 {

View File

@@ -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<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>;
class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
@@ -51,7 +57,7 @@ class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
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<SCMArtifactGroupTreeElement, FuzzyScore, ArtifactGroupTemplate> {
class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGroupTreeElement, FuzzyScore, ArtifactGroupTemplate> {
static readonly TEMPLATE_ID = 'artifactGroup';
get templateId(): string { return ArtifactGroupRenderer.TEMPLATE_ID; }
@@ -106,11 +112,16 @@ class ArtifactGroupRenderer implements ITreeRenderer<SCMArtifactGroupTreeElement
templateData.actionBar.context = artifactGroup;
}
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactGroupTemplate, details?: ITreeElementRenderDetails): void {
throw new Error('Should never happen since node is incompressible');
}
disposeElement(element: ITreeNode<SCMArtifactGroupTreeElement, FuzzyScore>, 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<SCMArtifactTreeElement, FuzzyScore, ArtifactTemplate> {
class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore, ArtifactTemplate> {
static readonly TEMPLATE_ID = 'artifact';
get templateId(): string { return ArtifactRenderer.TEMPLATE_ID; }
@@ -147,28 +158,49 @@ class ArtifactRenderer implements ITreeRenderer<SCMArtifactTreeElement, FuzzySco
return { label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
}
renderElement(node: ITreeNode<SCMArtifactTreeElement, FuzzyScore>, index: number, templateData: ArtifactTemplate): void {
const provider = node.element.repository.provider;
const artifact = node.element.artifact;
renderElement(nodeOrElement: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, 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<SCMArtifactTreeElement, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void {
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>>, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void {
const compressed = node.element as ICompressedTreeNode<IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>>;
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<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, 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<IS
const repository = inputOrElement.repository;
const artifacts = await repository.provider.artifactProvider.get()?.provideArtifacts(inputOrElement.artifactGroup.id) ?? [];
return artifacts.map(artifact => ({
repository,
group: inputOrElement.artifactGroup,
artifact,
type: 'artifact'
}));
} else if (isSCMArtifactTreeElement(inputOrElement)) {
return [];
} else {
return [];
}
// Create resource tree for artifacts
const artifactsTree = new ResourceTree<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>(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<IS
return true;
} else if (isSCMArtifactTreeElement(inputOrElement)) {
return false;
} else if (isSCMArtifactNode(inputOrElement)) {
return inputOrElement.childrenCount > 0;
} else {
return false;
}
@@ -252,12 +295,24 @@ class RepositoryTreeIdentityProvider implements IIdentityProvider<TreeElement> {
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<TreeElement> {
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<ISCMViewService, TreeElement>;
@@ -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<TreeElement>): 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');
}

View File

@@ -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<SCMArtifactTreeElement, SCMArtifactGroupTreeElement> {
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 {