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)",
|
"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": [
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user