mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 04:09:28 +00:00
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:
@@ -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": [
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 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(artifactGroup), primary => {
|
||||
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 => ({
|
||||
// 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'
|
||||
}));
|
||||
} else if (isSCMArtifactTreeElement(inputOrElement)) {
|
||||
return [];
|
||||
} else {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
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,10 +518,8 @@ 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);
|
||||
@@ -479,6 +539,21 @@ export class SCMRepositoriesViewPane extends ViewPane {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user