mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
SCM Graph - add branch picker (#227949)
* WIP - saving my work * Extract HistoryItemRef picker * Extract Repository picker * Improve history item ref picker rendering * Refactor color map * Refresh the graph when the filter changes * Push minor fix
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode';
|
||||
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryChangeEvent, SourceControlHistoryItemRef, l10n } from 'vscode';
|
||||
import { Repository, Resource } from './repository';
|
||||
import { IDisposable, dispose } from './util';
|
||||
import { toGitUri } from './uri';
|
||||
@@ -17,6 +17,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter<void>();
|
||||
readonly onDidChangeCurrentHistoryItemGroup: Event<void> = this._onDidChangeCurrentHistoryItemGroup.event;
|
||||
|
||||
private readonly _onDidChangeHistory = new EventEmitter<SourceControlHistoryChangeEvent>();
|
||||
readonly onDidChangeHistory: Event<SourceControlHistoryChangeEvent> = this._onDidChangeHistory.event;
|
||||
|
||||
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
|
||||
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
|
||||
|
||||
@@ -28,12 +31,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
}
|
||||
|
||||
private historyItemDecorations = new Map<string, FileDecoration>();
|
||||
private historyItemLabels = new Map<string, ThemeIcon>([
|
||||
['HEAD -> refs/heads/', new ThemeIcon('target')],
|
||||
['tag: refs/tags/', new ThemeIcon('tag')],
|
||||
['refs/heads/', new ThemeIcon('git-branch')],
|
||||
['refs/remotes/', new ThemeIcon('cloud')],
|
||||
]);
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
@@ -85,6 +82,51 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`);
|
||||
}
|
||||
|
||||
async provideHistoryItemRefs(): Promise<SourceControlHistoryItemRef[]> {
|
||||
const refs = await this.repository.getRefs();
|
||||
|
||||
const branches: SourceControlHistoryItemRef[] = [];
|
||||
const remoteBranches: SourceControlHistoryItemRef[] = [];
|
||||
const tags: SourceControlHistoryItemRef[] = [];
|
||||
|
||||
for (const ref of refs) {
|
||||
switch (ref.type) {
|
||||
case RefType.RemoteHead:
|
||||
remoteBranches.push({
|
||||
id: `refs/remotes/${ref.remote}/${ref.name}`,
|
||||
name: ref.name ?? '',
|
||||
description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined,
|
||||
revision: ref.commit,
|
||||
icon: new ThemeIcon('cloud'),
|
||||
category: l10n.t('remote branches')
|
||||
});
|
||||
break;
|
||||
case RefType.Tag:
|
||||
tags.push({
|
||||
id: `refs/tags/${ref.name}`,
|
||||
name: ref.name ?? '',
|
||||
description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined,
|
||||
revision: ref.commit,
|
||||
icon: new ThemeIcon('tag'),
|
||||
category: l10n.t('tags')
|
||||
});
|
||||
break;
|
||||
default:
|
||||
branches.push({
|
||||
id: `refs/heads/${ref.name}`,
|
||||
name: ref.name ?? '',
|
||||
description: ref.commit ? ref.commit.substring(0, 8) : undefined,
|
||||
revision: ref.commit,
|
||||
icon: new ThemeIcon('git-branch'),
|
||||
category: l10n.t('branches')
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [...branches, ...remoteBranches, ...tags];
|
||||
}
|
||||
|
||||
async provideHistoryItems(options: SourceControlHistoryOptions): Promise<SourceControlHistoryItem[]> {
|
||||
if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) {
|
||||
return [];
|
||||
@@ -115,7 +157,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
await ensureEmojis();
|
||||
|
||||
return commits.map(commit => {
|
||||
const labels = this.resolveHistoryItemLabels(commit);
|
||||
const references = this.resolveHistoryItemRefs(commit);
|
||||
|
||||
return {
|
||||
id: commit.hash,
|
||||
@@ -126,7 +168,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
displayId: commit.hash.substring(0, 8),
|
||||
timestamp: commit.authorDate?.getTime(),
|
||||
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
|
||||
labels: labels.length !== 0 ? labels : undefined
|
||||
references: references.length !== 0 ? references : undefined
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -208,19 +250,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
return this.historyItemDecorations.get(uri.toString());
|
||||
}
|
||||
|
||||
private resolveHistoryItemLabels(commit: Commit): SourceControlHistoryItemLabel[] {
|
||||
const labels: SourceControlHistoryItemLabel[] = [];
|
||||
private resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
|
||||
const references: SourceControlHistoryItemRef[] = [];
|
||||
|
||||
for (const label of commit.refNames) {
|
||||
for (const [key, value] of this.historyItemLabels) {
|
||||
if (label.startsWith(key)) {
|
||||
labels.push({ title: label.substring(key.length), icon: value });
|
||||
for (const ref of commit.refNames) {
|
||||
switch (true) {
|
||||
case ref.startsWith('HEAD -> refs/heads/'):
|
||||
references.push({
|
||||
id: ref.substring('HEAD -> '.length),
|
||||
name: ref.substring('HEAD -> refs/heads/'.length),
|
||||
revision: commit.hash,
|
||||
icon: new ThemeIcon('target')
|
||||
});
|
||||
break;
|
||||
case ref.startsWith('tag: refs/tags/'):
|
||||
references.push({
|
||||
id: ref.substring('tag: '.length),
|
||||
name: ref.substring('tag: refs/tags/'.length),
|
||||
revision: commit.hash,
|
||||
icon: new ThemeIcon('tag')
|
||||
});
|
||||
break;
|
||||
case ref.startsWith('refs/heads/'):
|
||||
references.push({
|
||||
id: ref,
|
||||
name: ref.substring('refs/heads/'.length),
|
||||
revision: commit.hash,
|
||||
icon: new ThemeIcon('git-branch')
|
||||
});
|
||||
break;
|
||||
case ref.startsWith('refs/remotes/'):
|
||||
references.push({
|
||||
id: ref,
|
||||
name: ref.substring('refs/remotes/'.length),
|
||||
revision: commit.hash,
|
||||
icon: new ThemeIcon('cloud')
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
return references;
|
||||
}
|
||||
|
||||
private async resolveHEADMergeBase(): Promise<Branch | undefined> {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Barrier } from '../../../base/common/async.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { Event, Emitter } from '../../../base/common/event.js';
|
||||
import { observableValue, observableValueOpts } from '../../../base/common/observable.js';
|
||||
import { derivedOpts, observableValue, observableValueOpts } from '../../../base/common/observable.js';
|
||||
import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js';
|
||||
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js';
|
||||
@@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js';
|
||||
import { ThemeIcon } from '../../../base/common/themables.js';
|
||||
import { IMarkdownString } from '../../../base/common/htmlContent.js';
|
||||
import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js';
|
||||
import { ResourceTree } from '../../../base/common/resourceTree.js';
|
||||
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js';
|
||||
@@ -28,6 +28,8 @@ import { ITextModelContentProvider, ITextModelService } from '../../../editor/co
|
||||
import { Schemas } from '../../../base/common/network.js';
|
||||
import { ITextModel } from '../../../editor/common/model.js';
|
||||
import { structuralEquals } from '../../../base/common/equals.js';
|
||||
import { Codicon } from '../../../base/common/codicons.js';
|
||||
import { historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote } from '../../contrib/scm/browser/scmHistory.js';
|
||||
|
||||
function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined {
|
||||
if (iconDto === undefined) {
|
||||
@@ -43,15 +45,15 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da
|
||||
}
|
||||
|
||||
function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem {
|
||||
const labels = historyItemDto.labels?.map(l => ({
|
||||
title: l.title, icon: getIconFromIconDto(l.icon)
|
||||
const references = historyItemDto.references?.map(r => ({
|
||||
...r, icon: getIconFromIconDto(r.icon)
|
||||
}));
|
||||
|
||||
const newLineIndex = historyItemDto.message.indexOf('\n');
|
||||
const subject = newLineIndex === -1 ?
|
||||
historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`;
|
||||
|
||||
return { ...historyItemDto, subject, labels };
|
||||
return { ...historyItemDto, subject, references };
|
||||
}
|
||||
|
||||
class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider {
|
||||
@@ -171,12 +173,62 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
|
||||
}, undefined);
|
||||
get currentHistoryItemGroup() { return this._currentHistoryItemGroup; }
|
||||
|
||||
readonly currentHistoryItemRef = derivedOpts<ISCMHistoryItemRef | undefined>({
|
||||
owner: this,
|
||||
equalsFn: structuralEquals
|
||||
}, reader => {
|
||||
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);
|
||||
|
||||
return currentHistoryItemGroup ? {
|
||||
id: currentHistoryItemGroup.id ?? '',
|
||||
name: currentHistoryItemGroup.name,
|
||||
revision: currentHistoryItemGroup.revision,
|
||||
color: historyItemGroupLocal,
|
||||
icon: Codicon.target,
|
||||
} : undefined;
|
||||
});
|
||||
|
||||
readonly currentHistoryItemRemoteRef = derivedOpts<ISCMHistoryItemRef | undefined>({
|
||||
owner: this,
|
||||
equalsFn: structuralEquals
|
||||
}, reader => {
|
||||
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);
|
||||
|
||||
return currentHistoryItemGroup?.remote ? {
|
||||
id: currentHistoryItemGroup.remote.id ?? '',
|
||||
name: currentHistoryItemGroup.remote.name,
|
||||
revision: currentHistoryItemGroup.remote.revision,
|
||||
color: historyItemGroupRemote,
|
||||
icon: Codicon.cloud,
|
||||
} : undefined;
|
||||
});
|
||||
|
||||
readonly currentHistoryItemBaseRef = derivedOpts<ISCMHistoryItemRef | undefined>({
|
||||
owner: this,
|
||||
equalsFn: structuralEquals
|
||||
}, reader => {
|
||||
const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader);
|
||||
|
||||
return currentHistoryItemGroup?.base ? {
|
||||
id: currentHistoryItemGroup.base.id ?? '',
|
||||
name: currentHistoryItemGroup.base.name,
|
||||
revision: currentHistoryItemGroup.base.revision,
|
||||
color: historyItemGroupBase,
|
||||
icon: Codicon.cloud,
|
||||
} : undefined;
|
||||
});
|
||||
|
||||
constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { }
|
||||
|
||||
async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise<string | undefined> {
|
||||
return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupIds, CancellationToken.None);
|
||||
}
|
||||
|
||||
async provideHistoryItemRefs(): Promise<ISCMHistoryItemRef[] | undefined> {
|
||||
const historyItemRefs = await this.proxy.$provideHistoryItemRefs(this.handle, CancellationToken.None);
|
||||
return historyItemRefs?.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) }));
|
||||
}
|
||||
|
||||
async provideHistoryItems(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined> {
|
||||
const historyItems = await this.proxy.$provideHistoryItems(this.handle, options, CancellationToken.None);
|
||||
return historyItems?.map(historyItem => toISCMHistoryItem(historyItem));
|
||||
|
||||
@@ -1550,6 +1550,15 @@ export interface SCMHistoryItemGroupDto {
|
||||
readonly remote?: Omit<Omit<SCMHistoryItemGroupDto, 'base'>, 'remote'>;
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemRefDto {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly revision?: string;
|
||||
readonly category?: string;
|
||||
readonly description?: string;
|
||||
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemDto {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
@@ -1562,10 +1571,7 @@ export interface SCMHistoryItemDto {
|
||||
readonly insertions: number;
|
||||
readonly deletions: number;
|
||||
};
|
||||
readonly labels?: {
|
||||
readonly title: string;
|
||||
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
|
||||
}[];
|
||||
readonly references?: SCMHistoryItemRefDto[];
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemChangeDto {
|
||||
@@ -2358,6 +2364,7 @@ export interface ExtHostSCMShape {
|
||||
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise<void>;
|
||||
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
|
||||
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
|
||||
$provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise<SCMHistoryItemRefDto[] | undefined>;
|
||||
$provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined>;
|
||||
$provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise<SCMHistoryItemChangeDto[] | undefined>;
|
||||
$resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise<string | undefined>;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { debounce } from '../../../base/common/decorators.js';
|
||||
import { DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { asPromise } from '../../../base/common/async.js';
|
||||
import { ExtHostCommands } from './extHostCommands.js';
|
||||
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol.js';
|
||||
import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemRefDto } from './extHost.protocol.js';
|
||||
import { sortedDiff, equals } from '../../../base/common/arrays.js';
|
||||
import { comparePaths } from '../../../base/common/comparers.js';
|
||||
import type * as vscode from 'vscode';
|
||||
@@ -72,11 +72,11 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc
|
||||
}
|
||||
|
||||
function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto {
|
||||
const labels = historyItem.labels?.map(l => ({
|
||||
title: l.title, icon: getHistoryItemIconDto(l.icon)
|
||||
const references = historyItem.references?.map(r => ({
|
||||
...r, icon: getHistoryItemIconDto(r.icon)
|
||||
}));
|
||||
|
||||
return { ...historyItem, labels };
|
||||
return { ...historyItem, references };
|
||||
}
|
||||
|
||||
function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number {
|
||||
@@ -982,6 +982,13 @@ export class ExtHostSCM implements ExtHostSCMShape {
|
||||
return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupIds, token) ?? undefined;
|
||||
}
|
||||
|
||||
async $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise<SCMHistoryItemRefDto[] | undefined> {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
const historyItemRefs = await historyProvider?.provideHistoryItemRefs(token);
|
||||
|
||||
return historyItemRefs?.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })) ?? undefined;
|
||||
}
|
||||
|
||||
async $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise<SCMHistoryItemDto[] | undefined> {
|
||||
const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider;
|
||||
const historyItems = await historyProvider?.provideHistoryItems(options, token);
|
||||
|
||||
@@ -495,13 +495,15 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-toolbar .action-label.scm-graph-repository-picker {
|
||||
.monaco-toolbar .action-label.scm-graph-repository-picker,
|
||||
.monaco-toolbar .action-label.scm-graph-history-item-picker {
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.monaco-toolbar .action-label.scm-graph-repository-picker .codicon {
|
||||
.monaco-toolbar .action-label.scm-graph-repository-picker .codicon,
|
||||
.monaco-toolbar .action-label.scm-graph-history-item-picker .codicon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -558,7 +560,7 @@
|
||||
|
||||
.scm-history-view .history-item-load-more .history-item-placeholder.shimmer .monaco-icon-label-container {
|
||||
height: 18px;
|
||||
background: var(--vscode-scm-historyItemDefaultLabelBackground);
|
||||
background: var(--vscode-scmGraph-historyItemHoverDefaultLabelBackground);
|
||||
border-radius: 2px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export const colorRegistry: ColorIdentifier[] = [
|
||||
registerColor('scmGraph.foreground3', chartsYellow, localize('scmGraphForeground3', "Source control graph foreground color (3).")),
|
||||
];
|
||||
|
||||
function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map<string, ColorIdentifier>): ColorIdentifier | undefined {
|
||||
for (const label of historyItem.labels ?? []) {
|
||||
const colorIndex = colorMap.get(label.title);
|
||||
if (colorIndex !== undefined) {
|
||||
return colorIndex;
|
||||
function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map<string, ColorIdentifier | undefined>): ColorIdentifier | undefined {
|
||||
for (const ref of historyItem.references ?? []) {
|
||||
const colorIdentifier = colorMap.get(ref.id);
|
||||
if (colorIdentifier !== undefined) {
|
||||
return colorIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
|
||||
} else {
|
||||
// HEAD
|
||||
// TODO@lszomoru - implement a better way to determine if the commit is HEAD
|
||||
if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) {
|
||||
if (historyItem.references?.some(ref => ThemeIcon.isThemeIcon(ref.icon) && ref.icon.id === 'target')) {
|
||||
const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor);
|
||||
svg.append(outerCircle);
|
||||
}
|
||||
@@ -246,7 +246,7 @@ export function renderSCMHistoryGraphPlaceholder(columns: ISCMHistoryItemGraphNo
|
||||
return elements.root;
|
||||
}
|
||||
|
||||
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map<string, string>()): ISCMHistoryItemViewModel[] {
|
||||
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map<string, ColorIdentifier | undefined>()): ISCMHistoryItemViewModel[] {
|
||||
let colorIndex = -1;
|
||||
const viewModels: ISCMHistoryItemViewModel[] = [];
|
||||
|
||||
@@ -302,11 +302,11 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[],
|
||||
});
|
||||
}
|
||||
|
||||
// Add colors to labels
|
||||
const labels = (historyItem.labels ?? [])
|
||||
.map(label => {
|
||||
let color = colorMap.get(label.title);
|
||||
if (!color && colorMap.has('*')) {
|
||||
// Add colors to references
|
||||
const references = (historyItem.references ?? [])
|
||||
.map(ref => {
|
||||
let color = colorMap.get(ref.id);
|
||||
if (colorMap.has(ref.id) && color === undefined) {
|
||||
// Find the history item in the input swimlanes
|
||||
const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id);
|
||||
|
||||
@@ -318,13 +318,13 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[],
|
||||
circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal;
|
||||
}
|
||||
|
||||
return { ...label, color };
|
||||
return { ...ref, color };
|
||||
});
|
||||
|
||||
viewModels.push({
|
||||
historyItem: {
|
||||
...historyItem,
|
||||
labels
|
||||
references
|
||||
},
|
||||
inputSwimlanes,
|
||||
outputSwimlanes,
|
||||
|
||||
@@ -34,7 +34,7 @@ import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../
|
||||
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
|
||||
import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js';
|
||||
import { isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemGroup, ISCMHistoryItemViewModel, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js';
|
||||
import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js';
|
||||
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
|
||||
import { stripIcons } from '../../../../base/common/iconLabels.js';
|
||||
@@ -45,10 +45,10 @@ import { Sequencer, Throttler } from '../../../../base/common/async.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js';
|
||||
import { tail } from '../../../../base/common/arrays.js';
|
||||
import { delta, groupBy, tail } from '../../../../base/common/arrays.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { IProgressService } from '../../../../platform/progress/common/progress.js';
|
||||
import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent } from '../../../../base/common/observableInternal/utils.js';
|
||||
import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observableInternal/utils.js';
|
||||
import { ContextKeys } from './scmViewPane.js';
|
||||
import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';
|
||||
import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
|
||||
@@ -60,6 +60,7 @@ import { Iterable } from '../../../../base/common/iterator.js';
|
||||
import { clamp } from '../../../../base/common/numbers.js';
|
||||
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
|
||||
import { structuralEquals } from '../../../../base/common/equals.js';
|
||||
import { compare } from '../../../../base/common/strings.js';
|
||||
|
||||
type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement;
|
||||
|
||||
@@ -76,10 +77,31 @@ class SCMRepositoryActionViewItem extends ActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
class SCMHistoryItemRefsActionViewItem extends ActionViewItem {
|
||||
constructor(private readonly _historyItemsFilter: HistoryItemRefsFilter, action: IAction, options?: IDropdownMenuActionViewItemOptions) {
|
||||
super(null, action, { ...options, icon: false, label: true });
|
||||
}
|
||||
|
||||
protected override updateLabel(): void {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.classList.add('scm-graph-history-item-picker');
|
||||
if (this._historyItemsFilter === 'all') {
|
||||
reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('all', "All")}`));
|
||||
} else if (this._historyItemsFilter === 'auto') {
|
||||
reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('auto', "Auto")}`));
|
||||
} else if (this._historyItemsFilter.length === 1) {
|
||||
reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter[0].name}`));
|
||||
} else {
|
||||
reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter.length} ${localize('items', "Items")}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends ViewAction<SCMHistoryViewPane> {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.scm.action.repository',
|
||||
id: 'workbench.scm.graph.action.pickRepository',
|
||||
title: '',
|
||||
viewId: HISTORY_VIEW_PANE_ID,
|
||||
f1: false,
|
||||
@@ -97,6 +119,28 @@ registerAction2(class extends ViewAction<SCMHistoryViewPane> {
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends ViewAction<SCMHistoryViewPane> {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.scm.graph.action.pickHistoryItemRefs',
|
||||
title: '',
|
||||
icon: Codicon.gitBranch,
|
||||
viewId: HISTORY_VIEW_PANE_ID,
|
||||
f1: false,
|
||||
menu: {
|
||||
id: MenuId.SCMHistoryTitle,
|
||||
group: 'navigation',
|
||||
order: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async runInView(_: ServicesAccessor, view: SCMHistoryViewPane): Promise<void> {
|
||||
view.pickHistoryItemRef();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends ViewAction<SCMHistoryViewPane> {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -241,36 +285,36 @@ class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemViewModelTreeEl
|
||||
const [matches, descriptionMatches] = this.processMatches(historyItemViewModel, node.filterData);
|
||||
templateData.label.setLabel(historyItem.subject, historyItem.author, { matches, descriptionMatches, extraClasses });
|
||||
|
||||
this._renderLabels(historyItem, templateData);
|
||||
this._renderBadges(historyItem, templateData);
|
||||
}
|
||||
|
||||
private _renderLabels(historyItem: ISCMHistoryItem, templateData: HistoryItemTemplate): void {
|
||||
private _renderBadges(historyItem: ISCMHistoryItem, templateData: HistoryItemTemplate): void {
|
||||
templateData.elementDisposables.add(autorun(reader => {
|
||||
const labelConfig = this._badgesConfig.read(reader);
|
||||
|
||||
templateData.labelContainer.textContent = '';
|
||||
const firstColoredLabel = historyItem.labels?.find(label => label.color);
|
||||
const firstColoredRef = historyItem.references?.find(ref => ref.color);
|
||||
|
||||
for (const label of historyItem.labels ?? []) {
|
||||
if (!label.color && labelConfig === 'filter') {
|
||||
for (const ref of historyItem.references ?? []) {
|
||||
if (!ref.color && labelConfig === 'filter') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (label.icon && ThemeIcon.isThemeIcon(label.icon)) {
|
||||
if (ref.icon && ThemeIcon.isThemeIcon(ref.icon)) {
|
||||
const elements = h('div.label', {
|
||||
style: {
|
||||
color: label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground),
|
||||
backgroundColor: label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground)
|
||||
color: ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground),
|
||||
backgroundColor: ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground)
|
||||
}
|
||||
}, [
|
||||
h('div.icon@icon'),
|
||||
h('div.description@description')
|
||||
]);
|
||||
|
||||
elements.icon.classList.add(...ThemeIcon.asClassNameArray(label.icon));
|
||||
elements.icon.classList.add(...ThemeIcon.asClassNameArray(ref.icon));
|
||||
|
||||
elements.description.textContent = label.title;
|
||||
elements.description.style.display = label === firstColoredLabel ? '' : 'none';
|
||||
elements.description.textContent = ref.name;
|
||||
elements.description.style.display = ref === firstColoredRef ? '' : 'none';
|
||||
|
||||
append(templateData.labelContainer, elements.root);
|
||||
}
|
||||
@@ -320,15 +364,15 @@ class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemViewModelTreeEl
|
||||
}
|
||||
}
|
||||
|
||||
if ((historyItem.labels ?? []).length > 0) {
|
||||
if ((historyItem.references ?? []).length > 0) {
|
||||
markdown.appendMarkdown(`\n\n---\n\n`);
|
||||
markdown.appendMarkdown((historyItem.labels ?? []).map(label => {
|
||||
const labelIconId = ThemeIcon.isThemeIcon(label.icon) ? label.icon.id : '';
|
||||
markdown.appendMarkdown((historyItem.references ?? []).map(ref => {
|
||||
const labelIconId = ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : '';
|
||||
|
||||
const labelBackgroundColor = label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground);
|
||||
const labelForegroundColor = label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground);
|
||||
const labelBackgroundColor = ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground);
|
||||
const labelForegroundColor = ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground);
|
||||
|
||||
return `<span style="color:${labelForegroundColor};background-color:${labelBackgroundColor};border-radius:2px;"> $(${labelIconId}) ${label.title} </span>`;
|
||||
return `<span style="color:${labelForegroundColor};background-color:${labelBackgroundColor};border-radius:2px;"> $(${labelIconId}) ${ref.name} </span>`;
|
||||
}).join(' '));
|
||||
}
|
||||
|
||||
@@ -510,8 +554,6 @@ class SCMHistoryTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati
|
||||
}
|
||||
}
|
||||
|
||||
type HistoryItemState = { currentHistoryItemGroup: ISCMHistoryItemGroup; items: ISCMHistoryItem[]; loadMore: boolean };
|
||||
|
||||
class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource<SCMHistoryViewModel, TreeElement> {
|
||||
|
||||
async getChildren(inputOrElement: SCMHistoryViewModel | TreeElement): Promise<Iterable<TreeElement>> {
|
||||
@@ -543,6 +585,9 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource<SC
|
||||
}
|
||||
}
|
||||
|
||||
type HistoryItemRefsFilter = 'all' | 'auto' | ISCMHistoryItemRef[];
|
||||
type HistoryItemState = { historyItemRefs: ISCMHistoryItemRef[]; items: ISCMHistoryItem[]; loadMore: boolean };
|
||||
|
||||
class SCMHistoryViewModel extends Disposable {
|
||||
|
||||
private readonly _closedRepository = observableFromEvent(
|
||||
@@ -574,33 +619,7 @@ class SCMHistoryViewModel extends Disposable {
|
||||
* values are updated in the same transaction (or during the initial read of the observable value).
|
||||
*/
|
||||
readonly repository = latestChangedValue(this, [this._firstRepository, this._graphRepository]);
|
||||
|
||||
private readonly _historyItemGroupFilter = observableValue<'all' | 'auto' | string[]>(this, 'auto');
|
||||
|
||||
readonly historyItemGroupFilter = derived<string[]>(reader => {
|
||||
const filter = this._historyItemGroupFilter.read(reader);
|
||||
if (Array.isArray(filter)) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
if (filter === 'all') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const repository = this.repository.get();
|
||||
const historyProvider = repository?.provider.historyProvider.get();
|
||||
const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get();
|
||||
|
||||
if (!currentHistoryItemGroup) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
currentHistoryItemGroup.revision ?? currentHistoryItemGroup.id,
|
||||
...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.revision ?? currentHistoryItemGroup.remote.id] : [],
|
||||
...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.revision ?? currentHistoryItemGroup.base.id] : [],
|
||||
];
|
||||
});
|
||||
readonly historyItemsFilter = observableValue<HistoryItemRefsFilter>(this, 'auto');
|
||||
|
||||
private readonly _state = new Map<ISCMRepository, HistoryItemState>();
|
||||
|
||||
@@ -652,24 +671,44 @@ class SCMHistoryViewModel extends Disposable {
|
||||
|
||||
let state = this._state.get(repository);
|
||||
const historyProvider = repository.provider.historyProvider.get();
|
||||
const currentHistoryItemGroup = state?.currentHistoryItemGroup ?? historyProvider?.currentHistoryItemGroup.get();
|
||||
|
||||
if (!historyProvider || !currentHistoryItemGroup) {
|
||||
if (!historyProvider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!state || state.loadMore) {
|
||||
const existingHistoryItems = state?.items ?? [];
|
||||
let historyItemRefs = state?.historyItemRefs;
|
||||
|
||||
if (!historyItemRefs) {
|
||||
const historyItemsFilter = this.historyItemsFilter.get();
|
||||
|
||||
switch (historyItemsFilter) {
|
||||
case 'all':
|
||||
historyItemRefs = await historyProvider.provideHistoryItemRefs() ?? [];
|
||||
break;
|
||||
case 'auto':
|
||||
historyItemRefs = [
|
||||
historyProvider.currentHistoryItemRef.get(),
|
||||
historyProvider.currentHistoryItemRemoteRef.get(),
|
||||
historyProvider.currentHistoryItemBaseRef.get(),
|
||||
].filter(ref => !!ref);
|
||||
break;
|
||||
default:
|
||||
historyItemRefs = historyItemsFilter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const historyItemGroupIds = this.historyItemGroupFilter.get();
|
||||
const limit = clamp(this._configurationService.getValue<number>('scm.graph.pageSize'), 1, 1000);
|
||||
const historyItemGroupIds = historyItemRefs.map(ref => ref.revision ?? ref.id);
|
||||
|
||||
const historyItems = await historyProvider.provideHistoryItems({
|
||||
historyItemGroupIds, limit, skip: existingHistoryItems.length
|
||||
}) ?? [];
|
||||
|
||||
state = {
|
||||
currentHistoryItemGroup,
|
||||
historyItemRefs,
|
||||
items: [...existingHistoryItems, ...historyItems],
|
||||
loadMore: false
|
||||
};
|
||||
@@ -678,7 +717,7 @@ class SCMHistoryViewModel extends Disposable {
|
||||
}
|
||||
|
||||
// Create the color map
|
||||
const colorMap = this._getGraphColorMap(currentHistoryItemGroup);
|
||||
const colorMap = this._getGraphColorMap(state.historyItemRefs);
|
||||
|
||||
return toISCMHistoryItemViewModelArray(state.items, colorMap)
|
||||
.map(historyItemViewModel => ({
|
||||
@@ -692,18 +731,34 @@ class SCMHistoryViewModel extends Disposable {
|
||||
this._selectedRepository.set(repository, undefined);
|
||||
}
|
||||
|
||||
private _getGraphColorMap(currentHistoryItemGroup: ISCMHistoryItemGroup): Map<string, ColorIdentifier> {
|
||||
const colorMap = new Map<string, ColorIdentifier>([
|
||||
[currentHistoryItemGroup.name, historyItemGroupLocal]
|
||||
]);
|
||||
if (currentHistoryItemGroup.remote) {
|
||||
colorMap.set(currentHistoryItemGroup.remote.name, historyItemGroupRemote);
|
||||
setHistoryItemsFilter(filter: 'all' | 'auto' | ISCMHistoryItemRef[]): void {
|
||||
this.historyItemsFilter.set(filter, undefined);
|
||||
}
|
||||
|
||||
private _getGraphColorMap(historyItemRefs: ISCMHistoryItemRef[]): Map<string, ColorIdentifier | undefined> {
|
||||
const repository = this.repository.get();
|
||||
const historyProvider = repository?.provider.historyProvider.get();
|
||||
const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get();
|
||||
|
||||
const colorMap = new Map<string, ColorIdentifier | undefined>();
|
||||
if (currentHistoryItemGroup) {
|
||||
colorMap.set(currentHistoryItemGroup.id, historyItemGroupLocal);
|
||||
if (currentHistoryItemGroup.remote) {
|
||||
colorMap.set(currentHistoryItemGroup.remote.id, historyItemGroupRemote);
|
||||
}
|
||||
if (currentHistoryItemGroup.base) {
|
||||
colorMap.set(currentHistoryItemGroup.base.id, historyItemGroupBase);
|
||||
}
|
||||
}
|
||||
if (currentHistoryItemGroup.base) {
|
||||
colorMap.set(currentHistoryItemGroup.base.name, historyItemGroupBase);
|
||||
}
|
||||
if (this._historyItemGroupFilter.get() === 'all') {
|
||||
colorMap.set('*', '');
|
||||
|
||||
// Add the remaining history item references to the color map
|
||||
// if not already present. These history item references will
|
||||
// be colored using the color of the history item to which they
|
||||
// point to.
|
||||
for (const ref of historyItemRefs) {
|
||||
if (!colorMap.has(ref.id)) {
|
||||
colorMap.set(ref.id, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
return colorMap;
|
||||
@@ -715,6 +770,166 @@ class SCMHistoryViewModel extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository };
|
||||
|
||||
class RepositoryPicker extends Disposable {
|
||||
private readonly _autoQuickPickItem: RepositoryQuickPickItem = {
|
||||
label: localize('auto', "Auto"),
|
||||
description: localize('activeRepository', "Show the source control graph for the active repository"),
|
||||
repository: 'auto'
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _scmViewService: ISCMViewService,
|
||||
private readonly _quickInputService: IQuickInputService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async pickRepository(): Promise<RepositoryQuickPickItem | undefined> {
|
||||
const picks: (RepositoryQuickPickItem | IQuickPickSeparator)[] = [
|
||||
this._autoQuickPickItem,
|
||||
{ type: 'separator' }];
|
||||
|
||||
picks.push(...this._scmViewService.repositories.map(r => ({
|
||||
label: r.provider.name,
|
||||
description: r.provider.rootUri?.fsPath,
|
||||
iconClass: ThemeIcon.asClassName(Codicon.repo),
|
||||
repository: r
|
||||
})));
|
||||
|
||||
return this._quickInputService.pick(picks, {
|
||||
placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type HistoryItemRefQuickPickItem = IQuickPickItem & { historyItemRef: 'all' | 'auto' | ISCMHistoryItemRef };
|
||||
|
||||
class HistoryItemRefPicker extends Disposable {
|
||||
private readonly _allQuickPickItem: HistoryItemRefQuickPickItem = {
|
||||
id: 'all',
|
||||
label: localize('all', "All"),
|
||||
description: localize('allHistoryItemRefs', "Show all history item references"),
|
||||
historyItemRef: 'all'
|
||||
};
|
||||
|
||||
private readonly _autoQuickPickItem: HistoryItemRefQuickPickItem = {
|
||||
id: 'auto',
|
||||
label: localize('auto', "Auto"),
|
||||
description: localize('currentHistoryItemRef', "Show the current history item reference"),
|
||||
historyItemRef: 'auto'
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _historyProvider: ISCMHistoryProvider,
|
||||
private readonly _historyItemsFilter: 'all' | 'auto' | ISCMHistoryItemRef[],
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async pickHistoryItemRef(): Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined> {
|
||||
const quickPick = this._quickInputService.createQuickPick<HistoryItemRefQuickPickItem>({ useSeparators: true });
|
||||
this._store.add(quickPick);
|
||||
|
||||
quickPick.placeholder = localize('scmGraphHistoryItemRef', "Select one/more history item references to view, type to filter");
|
||||
quickPick.canSelectMany = true;
|
||||
quickPick.hideCheckAll = true;
|
||||
quickPick.busy = true;
|
||||
quickPick.show();
|
||||
|
||||
quickPick.items = await this._createQuickPickItems();
|
||||
quickPick.busy = false;
|
||||
|
||||
// Set initial selection
|
||||
let selectedItems: HistoryItemRefQuickPickItem[] = [];
|
||||
if (this._historyItemsFilter === 'all') {
|
||||
selectedItems.push(this._allQuickPickItem);
|
||||
quickPick.selectedItems = [this._allQuickPickItem];
|
||||
} else if (this._historyItemsFilter === 'auto') {
|
||||
selectedItems.push(this._autoQuickPickItem);
|
||||
quickPick.selectedItems = [this._autoQuickPickItem];
|
||||
} else {
|
||||
for (const item of quickPick.items) {
|
||||
if (item.type === 'separator') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._historyItemsFilter.some(ref => ref.id === item.id)) {
|
||||
selectedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
quickPick.selectedItems = selectedItems;
|
||||
}
|
||||
|
||||
return new Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined>(resolve => {
|
||||
this._store.add(quickPick.onDidChangeSelection(items => {
|
||||
const { added } = delta(selectedItems, items, (a, b) => compare(a.id ?? '', b.id ?? ''));
|
||||
if (added.length > 0) {
|
||||
if (added[0].historyItemRef === 'all' || added[0].historyItemRef === 'auto') {
|
||||
quickPick.selectedItems = [added[0]];
|
||||
} else {
|
||||
// Remove 'all' and 'auto' items if present
|
||||
quickPick.selectedItems = [...quickPick.selectedItems
|
||||
.filter(i => i.historyItemRef !== 'all' && i.historyItemRef !== 'auto')];
|
||||
}
|
||||
}
|
||||
|
||||
selectedItems = [...quickPick.selectedItems];
|
||||
}));
|
||||
|
||||
this._store.add(quickPick.onDidAccept(() => {
|
||||
if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'all') {
|
||||
resolve('all');
|
||||
} else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'auto') {
|
||||
resolve('auto');
|
||||
} else {
|
||||
resolve(selectedItems.map(item => item.historyItemRef) as ISCMHistoryItemRef[]);
|
||||
}
|
||||
|
||||
quickPick.hide();
|
||||
}));
|
||||
|
||||
this._store.add(quickPick.onDidHide(() => {
|
||||
resolve(undefined);
|
||||
this.dispose();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async _createQuickPickItems(): Promise<(HistoryItemRefQuickPickItem | IQuickPickSeparator)[]> {
|
||||
const picks: (HistoryItemRefQuickPickItem | IQuickPickSeparator)[] = [
|
||||
this._allQuickPickItem, this._autoQuickPickItem
|
||||
];
|
||||
|
||||
const historyItemRefs = await this._historyProvider.provideHistoryItemRefs() ?? [];
|
||||
const historyItemRefsByCategory = groupBy(historyItemRefs, (a, b) => compare(a.category ?? '', b.category ?? ''));
|
||||
|
||||
for (const refs of historyItemRefsByCategory) {
|
||||
if (refs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
picks.push({ type: 'separator', label: refs[0].category });
|
||||
|
||||
picks.push(...refs.map(ref => {
|
||||
return {
|
||||
id: ref.id,
|
||||
label: ref.name,
|
||||
description: ref.description,
|
||||
iconClass: ThemeIcon.isThemeIcon(ref.icon) ?
|
||||
ThemeIcon.asClassName(ref.icon) : undefined,
|
||||
historyItemRef: ref
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
return picks;
|
||||
}
|
||||
}
|
||||
|
||||
export class SCMHistoryViewPane extends ViewPane {
|
||||
|
||||
private _treeContainer!: HTMLElement;
|
||||
@@ -736,9 +951,8 @@ export class SCMHistoryViewPane extends ViewPane {
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@ISCMViewService private readonly _scmViewService: ISCMViewService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IProgressService private readonly _progressService: IProgressService,
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@@ -871,6 +1085,11 @@ export class SCMHistoryViewPane extends ViewPane {
|
||||
}
|
||||
}));
|
||||
|
||||
// HistoryItemRefs filter changed
|
||||
store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => {
|
||||
this.refresh();
|
||||
}));
|
||||
|
||||
if (changeSummary.refresh) {
|
||||
this.refresh();
|
||||
}
|
||||
@@ -891,11 +1110,16 @@ export class SCMHistoryViewPane extends ViewPane {
|
||||
}
|
||||
|
||||
override getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined {
|
||||
if (action.id === 'workbench.scm.action.repository') {
|
||||
if (action.id === 'workbench.scm.graph.action.pickRepository') {
|
||||
const repository = this._treeViewModel?.repository.get();
|
||||
if (repository) {
|
||||
return new SCMRepositoryActionViewItem(repository, action, options);
|
||||
}
|
||||
} else if (action.id === 'workbench.scm.graph.action.pickHistoryItemRefs') {
|
||||
const historyItemsFilter = this._treeViewModel?.historyItemsFilter.get();
|
||||
if (historyItemsFilter) {
|
||||
return new SCMHistoryItemRefsActionViewItem(historyItemsFilter, action, options);
|
||||
}
|
||||
}
|
||||
|
||||
return super.getActionViewItem(action, options);
|
||||
@@ -910,33 +1134,31 @@ export class SCMHistoryViewPane extends ViewPane {
|
||||
}
|
||||
|
||||
async pickRepository(): Promise<void> {
|
||||
const picks: (IQuickPickItem & { repository: 'auto' | ISCMRepository } | IQuickPickSeparator)[] = [
|
||||
{
|
||||
label: localize('auto', "Auto"),
|
||||
description: localize('activeRepository', "Show the source control graph for the active repository"),
|
||||
repository: 'auto'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
];
|
||||
|
||||
picks.push(...this._scmViewService.repositories.map(r => ({
|
||||
label: r.provider.name,
|
||||
description: r.provider.rootUri?.fsPath,
|
||||
iconClass: ThemeIcon.asClassName(Codicon.repo),
|
||||
repository: r
|
||||
})));
|
||||
|
||||
const result = await this._quickInputService.pick(picks, {
|
||||
placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories")
|
||||
});
|
||||
const picker = this._instantiationService.createInstance(RepositoryPicker);
|
||||
const result = await picker.pickRepository();
|
||||
|
||||
if (result) {
|
||||
this._treeViewModel.setRepository(result.repository);
|
||||
}
|
||||
}
|
||||
|
||||
async pickHistoryItemRef(): Promise<void> {
|
||||
const repository = this._treeViewModel.repository.get();
|
||||
const historyProvider = repository?.provider.historyProvider.get();
|
||||
const historyItemsFilter = this._treeViewModel.historyItemsFilter.get();
|
||||
|
||||
if (!historyProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const picker = this._instantiationService.createInstance(HistoryItemRefPicker, historyProvider, historyItemsFilter);
|
||||
const result = await picker.pickHistoryItemRef();
|
||||
|
||||
if (result) {
|
||||
this._treeViewModel.setHistoryItemsFilter(result);
|
||||
}
|
||||
}
|
||||
|
||||
private _createTree(container: HTMLElement): void {
|
||||
this._treeIdentityProvider = new SCMHistoryTreeIdentityProvider();
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface ISCMHistoryProviderMenus {
|
||||
export interface ISCMHistoryProvider {
|
||||
readonly currentHistoryItemGroup: IObservable<ISCMHistoryItemGroup | undefined>;
|
||||
|
||||
readonly currentHistoryItemRef: IObservable<ISCMHistoryItemRef | undefined>;
|
||||
readonly currentHistoryItemRemoteRef: IObservable<ISCMHistoryItemRef | undefined>;
|
||||
readonly currentHistoryItemBaseRef: IObservable<ISCMHistoryItemRef | undefined>;
|
||||
|
||||
provideHistoryItemRefs(): Promise<ISCMHistoryItemRef[] | undefined>;
|
||||
provideHistoryItems(options: ISCMHistoryOptions): Promise<ISCMHistoryItem[] | undefined>;
|
||||
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise<ISCMHistoryItemChange[] | undefined>;
|
||||
resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise<string | undefined>;
|
||||
@@ -43,10 +48,14 @@ export interface ISCMHistoryItemStatistics {
|
||||
readonly deletions: number;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItemLabel {
|
||||
readonly title: string;
|
||||
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
|
||||
export interface ISCMHistoryItemRef {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly revision?: string;
|
||||
readonly category?: string;
|
||||
readonly description?: string;
|
||||
readonly color?: ColorIdentifier;
|
||||
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItem {
|
||||
@@ -58,7 +67,7 @@ export interface ISCMHistoryItem {
|
||||
readonly author?: string;
|
||||
readonly timestamp?: number;
|
||||
readonly statistics?: ISCMHistoryItemStatistics;
|
||||
readonly labels?: ISCMHistoryItemLabel[];
|
||||
readonly references?: ISCMHistoryItemRef[];
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItemGraphNode {
|
||||
|
||||
@@ -7,10 +7,10 @@ import * as assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { ColorIdentifier } from '../../../../../platform/theme/common/colorUtils.js';
|
||||
import { colorRegistry, historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemLabel } from '../../common/history.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemRef } from '../../common/history.js';
|
||||
|
||||
function toSCMHistoryItem(id: string, parentIds: string[], labels?: ISCMHistoryItemLabel[]): ISCMHistoryItem {
|
||||
return { id, parentIds, subject: '', message: '', labels } satisfies ISCMHistoryItem;
|
||||
function toSCMHistoryItem(id: string, parentIds: string[], references?: ISCMHistoryItemRef[]): ISCMHistoryItem {
|
||||
return { id, parentIds, subject: '', message: '', references } satisfies ISCMHistoryItem;
|
||||
}
|
||||
|
||||
suite('toISCMHistoryItemViewModelArray', () => {
|
||||
@@ -517,12 +517,12 @@ suite('toISCMHistoryItemViewModelArray', () => {
|
||||
*/
|
||||
test('graph with color map', () => {
|
||||
const models = [
|
||||
toSCMHistoryItem('a', ['b'], [{ title: 'topic' }]),
|
||||
toSCMHistoryItem('a', ['b'], [{ id: 'topic', name: 'topic' }]),
|
||||
toSCMHistoryItem('b', ['c']),
|
||||
toSCMHistoryItem('c', ['d'], [{ title: 'origin/topic' }]),
|
||||
toSCMHistoryItem('c', ['d'], [{ id: 'origin/topic', name: 'origin/topic' }]),
|
||||
toSCMHistoryItem('d', ['e']),
|
||||
toSCMHistoryItem('e', ['f', 'g']),
|
||||
toSCMHistoryItem('g', ['h'], [{ title: 'origin/main' }])
|
||||
toSCMHistoryItem('g', ['h'], [{ id: 'origin/main', name: 'origin/main' }])
|
||||
];
|
||||
|
||||
const colorMap = new Map<string, ColorIdentifier>([
|
||||
|
||||
+18
-14
@@ -20,10 +20,11 @@ declare module 'vscode' {
|
||||
onDidChangeCurrentHistoryItemGroup: Event<void>;
|
||||
|
||||
/**
|
||||
* Fires when the history item groups change (ex: commit, push, fetch)
|
||||
* Fires when history item refs change
|
||||
*/
|
||||
// onDidChangeHistoryItemGroups: Event<SourceControlHistoryChangeEvent>;
|
||||
onDidChangeHistory: Event<SourceControlHistoryChangeEvent>;
|
||||
|
||||
provideHistoryItemRefs(token: CancellationToken): ProviderResult<SourceControlHistoryItemRef[]>;
|
||||
provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult<SourceControlHistoryItem[]>;
|
||||
provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult<SourceControlHistoryItemChange[]>;
|
||||
|
||||
@@ -51,11 +52,6 @@ declare module 'vscode' {
|
||||
readonly deletions: number;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemLabel {
|
||||
readonly title: string;
|
||||
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItem {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
@@ -64,7 +60,16 @@ declare module 'vscode' {
|
||||
readonly author?: string;
|
||||
readonly timestamp?: number;
|
||||
readonly statistics?: SourceControlHistoryItemStatistics;
|
||||
readonly labels?: SourceControlHistoryItemLabel[];
|
||||
readonly references?: SourceControlHistoryItemRef[];
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemRef {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly revision?: string;
|
||||
readonly category?: string;
|
||||
readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemChange {
|
||||
@@ -74,10 +79,9 @@ declare module 'vscode' {
|
||||
readonly renameUri: Uri | undefined;
|
||||
}
|
||||
|
||||
// export interface SourceControlHistoryChangeEvent {
|
||||
// readonly added: Iterable<SourceControlHistoryItemGroup>;
|
||||
// readonly removed: Iterable<SourceControlHistoryItemGroup>;
|
||||
// readonly modified: Iterable<SourceControlHistoryItemGroup>;
|
||||
// }
|
||||
|
||||
export interface SourceControlHistoryChangeEvent {
|
||||
readonly added: Iterable<SourceControlHistoryItemRef>;
|
||||
readonly removed: Iterable<SourceControlHistoryItemRef>;
|
||||
readonly modified: Iterable<SourceControlHistoryItemRef>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user