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)", "icon": "$(target)",
"category": "Git", "category": "Git",
"enablement": "!operationInProgress" "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": [ "continueEditSession": [
@@ -1655,6 +1667,14 @@
{ {
"command": "git.repositories.checkout", "command": "git.repositories.checkout",
"when": "false" "when": "false"
},
{
"command": "git.repositories.checkoutDetached",
"when": "false"
},
{
"command": "git.repositories.compareRef",
"when": "false"
} }
], ],
"scm/title": [ "scm/title": [
@@ -1862,6 +1882,21 @@
"command": "git.repositories.checkout", "command": "git.repositories.checkout",
"group": "inline@1", "group": "inline@1",
"when": "scmProvider == git" "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": [ "scm/resourceGroup/context": [

View File

@@ -14,7 +14,7 @@ import { Model } from './model';
import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository';
import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging'; import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging';
import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; 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 { GitTimelineItem } from './timelineProvider';
import { ApiRepository } from './api/api1'; import { ApiRepository } from './api/api1';
import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource';
@@ -124,6 +124,7 @@ class RefItem implements QuickPickItem {
get refName(): string | undefined { return this.ref.name; } get refName(): string | undefined { return this.ref.name; }
get refRemote(): string | undefined { return this.ref.remote; } get refRemote(): string | undefined { return this.ref.remote; }
get shortCommit(): string { return (this.ref.commit || '').substring(0, this.shortCommitLength); } get shortCommit(): string { return (this.ref.commit || '').substring(0, this.shortCommitLength); }
get commitMessage(): string | undefined { return this.ref.commitDetails?.message; }
private _buttons?: QuickInputButton[]; private _buttons?: QuickInputButton[];
get buttons(): QuickInputButton[] | undefined { return this._buttons; } get buttons(): QuickInputButton[] | undefined { return this._buttons; }
@@ -3115,10 +3116,13 @@ export class CommandCenter {
return; return;
} }
const title = `${repository.historyProvider.currentHistoryItemRemoteRef.name}${getHistoryItemDisplayName(historyItem)}`;
await this._openChangesBetweenRefs( await this._openChangesBetweenRefs(
repository, repository,
repository.historyProvider.currentHistoryItemRemoteRef.name, repository.historyProvider.currentHistoryItemRemoteRef.revision,
historyItem); historyItem.id,
title);
} }
@command('git.graph.compareWithMergeBase', { repository: true }) @command('git.graph.compareWithMergeBase', { repository: true })
@@ -3127,14 +3131,17 @@ export class CommandCenter {
return; return;
} }
const title = `${repository.historyProvider.currentHistoryItemBaseRef.name}${getHistoryItemDisplayName(historyItem)}`;
await this._openChangesBetweenRefs( await this._openChangesBetweenRefs(
repository, repository,
repository.historyProvider.currentHistoryItemBaseRef.name, repository.historyProvider.currentHistoryItemBaseRef.name,
historyItem); historyItem.id,
title);
} }
@command('git.graph.compareRef', { repository: true }) @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) { if (!repository || !historyItem) {
return; return;
} }
@@ -3161,39 +3168,30 @@ export class CommandCenter {
return; return;
} }
const title = `${sourceRef.ref.name}${getHistoryItemDisplayName(historyItem)}`;
await this._openChangesBetweenRefs( await this._openChangesBetweenRefs(
repository, repository,
sourceRef.ref.name, sourceRef.ref.commit,
historyItem); historyItem.id,
title);
} }
private async _openChangesBetweenRefs(repository: Repository, ref: string | undefined, historyItem: SourceControlHistoryItem | undefined): Promise<void> { private async _openChangesBetweenRefs(repository: Repository, ref1: string | undefined, ref2: string | undefined, title: string): Promise<void> {
if (!repository || !ref || !historyItem) { if (!repository || !ref1 || !ref2) {
return; return;
} }
const ref2 = historyItem.references?.length
? historyItem.references[0].name
: historyItem.id;
try { try {
const changes = await repository.diffBetween2(ref, historyItem.id); const changes = await repository.diffBetween2(ref1, ref2);
if (changes.length === 0) { 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; return;
} }
const refDisplayName = historyItem.references?.length const multiDiffSourceUri = Uri.from({ scheme: 'git-ref-compare', path: `${repository.root}/${ref1}..${ref2}` });
? historyItem.references[0].name const resources = changes.map(change => toMultiFileDiffEditorUris(change, ref1, ref2));
: `${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}`
});
await commands.executeCommand('_workbench.openMultiDiffEditor', { await commands.executeCommand('_workbench.openMultiDiffEditor', {
multiDiffSourceUri, multiDiffSourceUri,
@@ -3201,7 +3199,7 @@ export class CommandCenter {
resources resources
}); });
} catch (err) { } 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 }); 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 { private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => { const result = (...args: any[]) => {
let result: Promise<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. * 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 { dirname, normalize, sep, relative } from 'path';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { promises as fs, createReadStream } from 'fs'; import { promises as fs, createReadStream } from 'fs';
@@ -797,6 +797,12 @@ export function getCommitShortHash(scope: Uri, hash: string): string {
return hash.substring(0, shortHashLength); 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 type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint' | 'none';
export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity { 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 { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js';
import { append, $ } from '../../../../base/browser/dom.js'; import { append, $ } from '../../../../base/browser/dom.js';
import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browser/ui/list/list.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 { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js';
import { ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.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 { IViewDescriptorService } from '../../../common/views.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { RepositoryActionRunner, RepositoryRenderer } from './scmRepositoryRenderer.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 { Orientation } from '../../../../base/browser/ui/sash/sash.js';
import { Iterable } from '../../../../base/common/iterator.js'; import { Iterable } from '../../../../base/common/iterator.js';
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.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 { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { ICommandService } from '../../../../platform/commands/common/commands.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> { class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
@@ -51,7 +57,7 @@ class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
return RepositoryRenderer.TEMPLATE_ID; return RepositoryRenderer.TEMPLATE_ID;
} else if (isSCMArtifactGroupTreeElement(element)) { } else if (isSCMArtifactGroupTreeElement(element)) {
return ArtifactGroupRenderer.TEMPLATE_ID; return ArtifactGroupRenderer.TEMPLATE_ID;
} else if (isSCMArtifactTreeElement(element)) { } else if (isSCMArtifactTreeElement(element) || isSCMArtifactNode(element)) {
return ArtifactRenderer.TEMPLATE_ID; return ArtifactRenderer.TEMPLATE_ID;
} else { } else {
throw new Error('Invalid tree element'); throw new Error('Invalid tree element');
@@ -66,7 +72,7 @@ interface ArtifactGroupTemplate {
readonly templateDisposable: IDisposable; readonly templateDisposable: IDisposable;
} }
class ArtifactGroupRenderer implements ITreeRenderer<SCMArtifactGroupTreeElement, FuzzyScore, ArtifactGroupTemplate> { class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGroupTreeElement, FuzzyScore, ArtifactGroupTemplate> {
static readonly TEMPLATE_ID = 'artifactGroup'; static readonly TEMPLATE_ID = 'artifactGroup';
get templateId(): string { return ArtifactGroupRenderer.TEMPLATE_ID; } get templateId(): string { return ArtifactGroupRenderer.TEMPLATE_ID; }
@@ -106,11 +112,16 @@ class ArtifactGroupRenderer implements ITreeRenderer<SCMArtifactGroupTreeElement
templateData.actionBar.context = artifactGroup; 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 { disposeElement(element: ITreeNode<SCMArtifactGroupTreeElement, FuzzyScore>, index: number, templateData: ArtifactGroupTemplate, details?: ITreeElementRenderDetails): void {
templateData.elementDisposables.clear(); templateData.elementDisposables.clear();
} }
disposeTemplate(templateData: ArtifactGroupTemplate): void { disposeTemplate(templateData: ArtifactGroupTemplate): void {
templateData.elementDisposables.dispose();
templateData.templateDisposable.dispose(); templateData.templateDisposable.dispose();
} }
} }
@@ -122,7 +133,7 @@ interface ArtifactTemplate {
readonly templateDisposable: IDisposable; 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'; static readonly TEMPLATE_ID = 'artifact';
get templateId(): string { return ArtifactRenderer.TEMPLATE_ID; } 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) }; return { label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
} }
renderElement(node: ITreeNode<SCMArtifactTreeElement, FuzzyScore>, index: number, templateData: ArtifactTemplate): void { renderElement(nodeOrElement: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactTemplate): void {
const provider = node.element.repository.provider; const artifactOrFolder = nodeOrElement.element;
const artifact = node.element.artifact;
const artifactGroup = node.element.group; if (isSCMArtifactNode(artifactOrFolder)) {
const artifactGroupIcon = ThemeIcon.isThemeIcon(artifactGroup.icon) // Folder
? `$(${artifactGroup.icon.id}) ` : ''; 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); const artifactLabel = artifact.name.split('/').pop() ?? artifact.name;
templateData.elementDisposables.add(connectPrimaryMenu(repositoryMenus.getArtifactMenu(artifactGroup), primary => { templateData.label.setLabel(`${artifactIcon}${artifactLabel}`, artifact.description);
templateData.actionBar.setActions(primary);
}, 'inline', provider)); const provider = artifactOrFolder.repository.provider;
templateData.actionBar.context = artifact; 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(); templateData.elementDisposables.clear();
} }
disposeTemplate(templateData: ArtifactTemplate): void { disposeTemplate(templateData: ArtifactTemplate): void {
templateData.elementDisposables.dispose();
templateData.templateDisposable.dispose(); templateData.templateDisposable.dispose();
} }
} }
@@ -204,17 +236,26 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
const repository = inputOrElement.repository; const repository = inputOrElement.repository;
const artifacts = await repository.provider.artifactProvider.get()?.provideArtifacts(inputOrElement.artifactGroup.id) ?? []; const artifacts = await repository.provider.artifactProvider.get()?.provideArtifacts(inputOrElement.artifactGroup.id) ?? [];
return artifacts.map(artifact => ({ // Create resource tree for artifacts
repository, const artifactsTree = new ResourceTree<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>(inputOrElement);
group: inputOrElement.artifactGroup, for (const artifact of artifacts) {
artifact, artifactsTree.add(URI.from({
type: 'artifact' scheme: 'scm-artifact', path: artifact.name
})); }), {
} else if (isSCMArtifactTreeElement(inputOrElement)) { repository,
return []; group: inputOrElement.artifactGroup,
} else { artifact,
return []; 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 { hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean {
@@ -238,6 +279,8 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
return true; return true;
} else if (isSCMArtifactTreeElement(inputOrElement)) { } else if (isSCMArtifactTreeElement(inputOrElement)) {
return false; return false;
} else if (isSCMArtifactNode(inputOrElement)) {
return inputOrElement.childrenCount > 0;
} else { } else {
return false; return false;
} }
@@ -252,12 +295,24 @@ class RepositoryTreeIdentityProvider implements IIdentityProvider<TreeElement> {
return `artifactGroup:${element.repository.provider.id}/${element.artifactGroup.id}`; return `artifactGroup:${element.repository.provider.id}/${element.artifactGroup.id}`;
} else if (isSCMArtifactTreeElement(element)) { } else if (isSCMArtifactTreeElement(element)) {
return `artifact:${element.repository.provider.id}/${element.group.id}/${element.artifact.id}`; 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 { } else {
throw new Error('Invalid tree element'); 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 { export class SCMRepositoriesViewPane extends ViewPane {
private tree!: WorkbenchCompressibleAsyncDataTree<ISCMViewService, TreeElement>; private tree!: WorkbenchCompressibleAsyncDataTree<ISCMViewService, TreeElement>;
@@ -361,16 +416,12 @@ export class SCMRepositoriesViewPane extends ViewPane {
this.treeDataSource = this.instantiationService.createInstance(RepositoryTreeDataSource); this.treeDataSource = this.instantiationService.createInstance(RepositoryTreeDataSource);
this._register(this.treeDataSource); this._register(this.treeDataSource);
const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService);
this.tree = this.instantiationService.createInstance( this.tree = this.instantiationService.createInstance(
WorkbenchCompressibleAsyncDataTree, WorkbenchCompressibleAsyncDataTree,
'SCM Repositories', 'SCM Repositories',
container, container,
new ListDelegate(), new ListDelegate(),
{ new RepositoriesTreeCompressionDelegate(),
isIncompressible: () => true
},
[ [
this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(ArtifactGroupRenderer), this.instantiationService.createInstance(ArtifactGroupRenderer),
@@ -389,9 +440,20 @@ export class SCMRepositoriesViewPane extends ViewPane {
} }
// Explorer mode // 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; return true;
}, },
compressionEnabled: compressionEnabled.get(), compressionEnabled: true,
overrideStyles: this.getLocationBasedColors().listOverrideStyles, overrideStyles: this.getLocationBasedColors().listOverrideStyles,
multipleSelectionSupport: this.scmViewService.selectionModeConfig.get() === 'multiple', multipleSelectionSupport: this.scmViewService.selectionModeConfig.get() === 'multiple',
expandOnDoubleClick: true, expandOnDoubleClick: true,
@@ -456,29 +518,42 @@ export class SCMRepositoriesViewPane extends ViewPane {
return; return;
} }
if (!isSCMRepository(e.element)) { if (isSCMRepository(e.element)) {
return; // 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 { private onTreeSelectionChange(e: ITreeEvent<TreeElement>): void {
@@ -615,6 +690,8 @@ export class SCMRepositoriesViewPane extends ViewPane {
return e; return e;
} else if (isSCMArtifactGroupTreeElement(e) || isSCMArtifactTreeElement(e)) { } else if (isSCMArtifactGroupTreeElement(e) || isSCMArtifactTreeElement(e)) {
return e.repository; return e.repository;
} else if (isSCMArtifactNode(e)) {
return e.context.repository;
} else { } else {
throw new Error('Invalid tree element'); 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'; 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 { export function isSCMArtifactTreeElement(element: unknown): element is SCMArtifactTreeElement {
return (element as SCMArtifactTreeElement).type === 'artifact'; return (element as SCMArtifactTreeElement).type === 'artifact';
} }
@@ -104,8 +108,8 @@ export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], s
return menu.onDidChange(updateActions); return menu.onDidChange(updateActions);
} }
export function collectContextMenuActions(menu: IMenu): IAction[] { export function collectContextMenuActions(menu: IMenu, arg?: unknown): IAction[] {
return getContextMenuActions(menu.getActions({ shouldForwardArgs: true }), 'inline').secondary; return getContextMenuActions(menu.getActions({ arg, shouldForwardArgs: true }), 'inline').secondary;
} }
export class StatusBarAction extends Action { export class StatusBarAction extends Action {