SCM - refactor history item hover for the graph (#278778)

* SCM - refactor history item tooltip

* Extract the hover code into a separate file

* SCM - add references into the hover

* Pull request feedback

* Fix compilation errors
This commit is contained in:
Ladislau Szomoru
2025-11-21 15:48:43 +00:00
committed by GitHub
parent ffc4b9540b
commit a73588288a
12 changed files with 294 additions and 202 deletions

View File

@@ -15,7 +15,7 @@ import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation }
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
import { AvatarQuery, AvatarQueryCommit } from './api/git'; import { AvatarQuery, AvatarQueryCommit } from './api/git';
import { LRUCache } from './cache'; import { LRUCache } from './cache';
import { AVATAR_SIZE, getHistoryItemHover, getHistoryItemHoverCommitHashCommands, processHistoryItemRemoteHoverCommands } from './historyProvider'; import { AVATAR_SIZE, getCommitHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean {
return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive);
@@ -251,8 +251,8 @@ export class GitBlameController {
// Commands // Commands
const commands: Command[][] = [ const commands: Command[][] = [
getHistoryItemHoverCommitHashCommands(documentUri, hash), getHoverCommitHashCommands(documentUri, hash),
processHistoryItemRemoteHoverCommands(remoteHoverCommands, hash) processHoverRemoteCommands(remoteHoverCommands, hash)
]; ];
commands.push([{ commands.push([{
@@ -262,7 +262,7 @@ export class GitBlameController {
arguments: ['git.blame'] arguments: ['git.blame']
}] satisfies Command[]); }] satisfies Command[]);
return getHistoryItemHover(commitAvatar, authorName, authorEmail, authorDate, message, commitInformation?.shortStat, undefined, commands); return getCommitHover(commitAvatar, authorName, authorEmail, authorDate, message, commitInformation?.shortStat, commands);
} }
private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void {

View File

@@ -4,29 +4,26 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, MarkdownString, Command, commands } from 'vscode'; import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, Command, commands } from 'vscode';
import { Repository, Resource } from './repository'; import { Repository, Resource } from './repository';
import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, fromNow, getCommitShortHash, subject, truncate } from './util'; import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, subject, truncate } from './util';
import { toMultiFileDiffEditorUris } from './uri'; import { toMultiFileDiffEditorUris } from './uri';
import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git';
import { emojify, ensureEmojis } from './emoji'; import { emojify, ensureEmojis } from './emoji';
import { Commit, CommitShortStat } from './git'; import { Commit } from './git';
import { OperationKind, OperationResult } from './operation'; import { OperationKind, OperationResult } from './operation';
import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
import { throttle } from './decorators'; import { throttle } from './decorators';
import { getHistoryItemHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
type SourceControlHistoryItemRefWithRenderOptions = SourceControlHistoryItemRef & { function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number {
backgroundColor?: string; const getOrder = (ref: SourceControlHistoryItemRef): number => {
};
function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRefWithRenderOptions, ref2: SourceControlHistoryItemRefWithRenderOptions): number {
const getOrder = (ref: SourceControlHistoryItemRefWithRenderOptions): number => {
if (ref.id.startsWith('refs/heads/')) { if (ref.id.startsWith('refs/heads/')) {
return ref.backgroundColor ? 1 : 5; return 1;
} else if (ref.id.startsWith('refs/remotes/')) { } else if (ref.id.startsWith('refs/remotes/')) {
return ref.backgroundColor ? 2 : 15; return 2;
} else if (ref.id.startsWith('refs/tags/')) { } else if (ref.id.startsWith('refs/tags/')) {
return ref.backgroundColor ? 3 : 25; return 3;
} }
return 99; return 99;
@@ -308,11 +305,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
const references = this._resolveHistoryItemRefs(commit); const references = this._resolveHistoryItemRefs(commit);
const commands: Command[][] = [ const commands: Command[][] = [
getHistoryItemHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash), getHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash),
processHistoryItemRemoteHoverCommands(remoteHoverCommands, commit.hash) processHoverRemoteCommands(remoteHoverCommands, commit.hash)
]; ];
const tooltip = getHistoryItemHover(avatarUrl, commit.authorName, commit.authorEmail, commit.authorDate ?? commit.commitDate, messageWithLinks, commit.shortStat, references, commands); const tooltip = getHistoryItemHover(avatarUrl, commit.authorName, commit.authorEmail, commit.authorDate ?? commit.commitDate, messageWithLinks, commit.shortStat, commands);
historyItems.push({ historyItems.push({
id: commit.hash, id: commit.hash,
@@ -489,8 +486,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
return this.historyItemDecorations.get(uri.toString()); return this.historyItemDecorations.get(uri.toString());
} }
private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRefWithRenderOptions[] { private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
const references: SourceControlHistoryItemRefWithRenderOptions[] = []; const references: SourceControlHistoryItemRef[] = [];
for (const ref of commit.refNames) { for (const ref of commit.refNames) {
if (ref === 'refs/remotes/origin/HEAD') { if (ref === 'refs/remotes/origin/HEAD') {
@@ -504,8 +501,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
name: ref.substring('HEAD -> refs/heads/'.length), name: ref.substring('HEAD -> refs/heads/'.length),
revision: commit.hash, revision: commit.hash,
category: l10n.t('branches'), category: l10n.t('branches'),
icon: new ThemeIcon('target'), icon: new ThemeIcon('target')
backgroundColor: `--vscode-scmGraph-historyItemRefColor`
}); });
break; break;
case ref.startsWith('refs/heads/'): case ref.startsWith('refs/heads/'):
@@ -523,12 +519,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
name: ref.substring('refs/remotes/'.length), name: ref.substring('refs/remotes/'.length),
revision: commit.hash, revision: commit.hash,
category: l10n.t('remote branches'), category: l10n.t('remote branches'),
icon: new ThemeIcon('cloud'), icon: new ThemeIcon('cloud')
backgroundColor: ref === this.currentHistoryItemRemoteRef?.id
? `--vscode-scmGraph-historyItemRemoteRefColor`
: ref === this.currentHistoryItemBaseRef?.id
? `--vscode-scmGraph-historyItemBaseRefColor`
: undefined
}); });
break; break;
case ref.startsWith('tag: refs/tags/'): case ref.startsWith('tag: refs/tags/'):
@@ -537,10 +528,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
name: ref.substring('tag: refs/tags/'.length), name: ref.substring('tag: refs/tags/'.length),
revision: commit.hash, revision: commit.hash,
category: l10n.t('tags'), category: l10n.t('tags'),
icon: new ThemeIcon('tag'), icon: new ThemeIcon('tag')
backgroundColor: ref === this.currentHistoryItemRef?.id
? `--vscode-scmGraph-historyItemRefColor`
: undefined
}); });
break; break;
} }
@@ -621,127 +609,3 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
dispose(this.disposables); dispose(this.disposables);
} }
} }
export const AVATAR_SIZE = 20;
export function getHistoryItemHoverCommitHashCommands(documentUri: Uri, hash: string): Command[] {
return [{
title: `$(git-commit) ${getCommitShortHash(documentUri, hash)}`,
tooltip: l10n.t('Open Commit'),
command: 'git.viewCommit',
arguments: [documentUri, hash, documentUri]
}, {
title: `$(copy)`,
tooltip: l10n.t('Copy Commit Hash'),
command: 'git.copyContentToClipboard',
arguments: [hash]
}] satisfies Command[];
}
export function processHistoryItemRemoteHoverCommands(commands: Command[], hash: string): Command[] {
return commands.map(command => ({
...command,
arguments: [...command.arguments ?? [], hash]
} satisfies Command));
}
export function getHistoryItemHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, references: SourceControlHistoryItemRefWithRenderOptions[] | undefined, commands: Command[][] | undefined): MarkdownString {
const markdownString = new MarkdownString('', true);
markdownString.isTrusted = {
enabledCommands: commands?.flat().map(c => c.command) ?? []
};
// Author
if (authorName) {
// Avatar
if (authorAvatar) {
markdownString.appendMarkdown('![');
markdownString.appendText(authorName);
markdownString.appendMarkdown('](');
markdownString.appendText(authorAvatar);
markdownString.appendMarkdown(`|width=${AVATAR_SIZE},height=${AVATAR_SIZE})`);
} else {
markdownString.appendMarkdown('$(account)');
}
// Email
if (authorEmail) {
markdownString.appendMarkdown(' [**');
markdownString.appendText(authorName);
markdownString.appendMarkdown('**](mailto:');
markdownString.appendText(authorEmail);
markdownString.appendMarkdown(')');
} else {
markdownString.appendMarkdown(' **');
markdownString.appendText(authorName);
markdownString.appendMarkdown('**');
}
// Date
if (authorDate && !isNaN(new Date(authorDate).getTime())) {
const dateString = new Date(authorDate).toLocaleString(undefined, {
year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'
});
markdownString.appendMarkdown(', $(history)');
markdownString.appendText(` ${fromNow(authorDate, true, true)} (${dateString})`);
}
markdownString.appendMarkdown('\n\n');
}
// Subject | Message (escape image syntax)
markdownString.appendMarkdown(`${emojify(message.replace(/!\[/g, '&#33;&#91;').replace(/\r\n|\r|\n/g, '\n\n'))}\n\n`);
markdownString.appendMarkdown(`---\n\n`);
// Short stats
if (shortStats) {
markdownString.appendMarkdown(`<span>${shortStats.files === 1 ?
l10n.t('{0} file changed', shortStats.files) :
l10n.t('{0} files changed', shortStats.files)}</span>`);
if (shortStats.insertions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverAdditionsForeground);">${shortStats.insertions === 1 ?
l10n.t('{0} insertion{1}', shortStats.insertions, '(+)') :
l10n.t('{0} insertions{1}', shortStats.insertions, '(+)')}</span>`);
}
if (shortStats.deletions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverDeletionsForeground);">${shortStats.deletions === 1 ?
l10n.t('{0} deletion{1}', shortStats.deletions, '(-)') :
l10n.t('{0} deletions{1}', shortStats.deletions, '(-)')}</span>`);
}
markdownString.appendMarkdown(`\n\n---\n\n`);
}
// References
if (references && references.length > 0) {
for (const reference of references) {
const labelIconId = reference.icon instanceof ThemeIcon ? reference.icon.id : '';
const backgroundColor = `var(${reference.backgroundColor ?? '--vscode-scmGraph-historyItemHoverDefaultLabelBackground'})`;
const color = reference.backgroundColor ? `var(--vscode-scmGraph-historyItemHoverLabelForeground)` : `var(--vscode-scmGraph-historyItemHoverDefaultLabelForeground)`;
markdownString.appendMarkdown(`<span style="color:${color};background-color:${backgroundColor};border-radius:10px;">&nbsp;$(${labelIconId})&nbsp;`);
markdownString.appendText(reference.name);
markdownString.appendMarkdown(`&nbsp;&nbsp;</span>`);
}
markdownString.appendMarkdown(`\n\n---\n\n`);
}
// Commands
if (commands && commands.length > 0) {
for (let index = 0; index < commands.length; index++) {
if (index !== 0) {
markdownString.appendMarkdown('&nbsp;&nbsp;|&nbsp;&nbsp;');
}
const commandsMarkdown = commands[index]
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`);
markdownString.appendMarkdown(commandsMarkdown.join('&nbsp;'));
}
}
return markdownString;
}

161
extensions/git/src/hover.ts Normal file
View File

@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command, l10n, MarkdownString, Uri } from 'vscode';
import { fromNow, getCommitShortHash } from './util';
import { emojify } from './emoji';
import { CommitShortStat } from './git';
export const AVATAR_SIZE = 20;
export function getCommitHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, commands: Command[][] | undefined): MarkdownString {
const markdownString = new MarkdownString('', true);
markdownString.isTrusted = {
enabledCommands: commands?.flat().map(c => c.command) ?? []
};
// Author, Subject | Message (escape image syntax)
appendContent(markdownString, authorAvatar, authorName, authorEmail, authorDate, message);
// Short stats
if (shortStats) {
appendShortStats(markdownString, shortStats);
}
// Commands
if (commands && commands.length > 0) {
appendCommands(markdownString, commands);
}
return markdownString;
}
export function getHistoryItemHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, commands: Command[][] | undefined): MarkdownString[] {
const hoverContent: MarkdownString[] = [];
// Author, Subject | Message (escape image syntax)
const authorMarkdownString = new MarkdownString('', true);
appendContent(authorMarkdownString, authorAvatar, authorName, authorEmail, authorDate, message);
hoverContent.push(authorMarkdownString);
// Short stats
if (shortStats) {
const shortStatsMarkdownString = new MarkdownString('', true);
shortStatsMarkdownString.supportHtml = true;
appendShortStats(shortStatsMarkdownString, shortStats);
hoverContent.push(shortStatsMarkdownString);
}
// Commands
if (commands && commands.length > 0) {
const commandsMarkdownString = new MarkdownString('', true);
commandsMarkdownString.isTrusted = {
enabledCommands: commands?.flat().map(c => c.command) ?? []
};
appendCommands(commandsMarkdownString, commands);
hoverContent.push(commandsMarkdownString);
}
return hoverContent;
}
function appendContent(markdownString: MarkdownString, authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string): void {
// Author
if (authorName) {
// Avatar
if (authorAvatar) {
markdownString.appendMarkdown('![');
markdownString.appendText(authorName);
markdownString.appendMarkdown('](');
markdownString.appendText(authorAvatar);
markdownString.appendMarkdown(`|width=${AVATAR_SIZE},height=${AVATAR_SIZE})`);
} else {
markdownString.appendMarkdown('$(account)');
}
// Email
if (authorEmail) {
markdownString.appendMarkdown(' [**');
markdownString.appendText(authorName);
markdownString.appendMarkdown('**](mailto:');
markdownString.appendText(authorEmail);
markdownString.appendMarkdown(')');
} else {
markdownString.appendMarkdown(' **');
markdownString.appendText(authorName);
markdownString.appendMarkdown('**');
}
// Date
if (authorDate && !isNaN(new Date(authorDate).getTime())) {
const dateString = new Date(authorDate).toLocaleString(undefined, {
year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'
});
markdownString.appendMarkdown(', $(history)');
markdownString.appendText(` ${fromNow(authorDate, true, true)} (${dateString})`);
}
markdownString.appendMarkdown('\n\n');
}
// Subject | Message (escape image syntax)
markdownString.appendMarkdown(`${emojify(message.replace(/!\[/g, '&#33;&#91;').replace(/\r\n|\r|\n/g, '\n\n'))}`);
markdownString.appendMarkdown(`\n\n---\n\n`);
}
function appendShortStats(markdownString: MarkdownString, shortStats: { files: number; insertions: number; deletions: number }): void {
// Short stats
markdownString.appendMarkdown(`<span>${shortStats.files === 1 ?
l10n.t('{0} file changed', shortStats.files) :
l10n.t('{0} files changed', shortStats.files)}</span>`);
if (shortStats.insertions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverAdditionsForeground);">${shortStats.insertions === 1 ?
l10n.t('{0} insertion{1}', shortStats.insertions, '(+)') :
l10n.t('{0} insertions{1}', shortStats.insertions, '(+)')}</span>`);
}
if (shortStats.deletions) {
markdownString.appendMarkdown(`,&nbsp;<span style="color:var(--vscode-scmGraph-historyItemHoverDeletionsForeground);">${shortStats.deletions === 1 ?
l10n.t('{0} deletion{1}', shortStats.deletions, '(-)') :
l10n.t('{0} deletions{1}', shortStats.deletions, '(-)')}</span>`);
}
markdownString.appendMarkdown(`\n\n---\n\n`);
}
function appendCommands(markdownString: MarkdownString, commands: Command[][]): void {
for (let index = 0; index < commands.length; index++) {
if (index !== 0) {
markdownString.appendMarkdown('&nbsp;&nbsp;|&nbsp;&nbsp;');
}
const commandsMarkdown = commands[index]
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`);
markdownString.appendMarkdown(commandsMarkdown.join('&nbsp;'));
}
}
export function getHoverCommitHashCommands(documentUri: Uri, hash: string): Command[] {
return [{
title: `$(git-commit) ${getCommitShortHash(documentUri, hash)}`,
tooltip: l10n.t('Open Commit'),
command: 'git.viewCommit',
arguments: [documentUri, hash, documentUri]
}, {
title: `$(copy)`,
tooltip: l10n.t('Copy Commit Hash'),
command: 'git.copyContentToClipboard',
arguments: [hash]
}] satisfies Command[];
}
export function processHoverRemoteCommands(commands: Command[], hash: string): Command[] {
return commands.map(command => ({
...command,
arguments: [...command.arguments ?? [], hash]
} satisfies Command));
}

View File

@@ -13,7 +13,7 @@ import { OperationKind, OperationResult } from './operation';
import { truncate } from './util'; import { truncate } from './util';
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
import { AvatarQuery, AvatarQueryCommit } from './api/git'; import { AvatarQuery, AvatarQueryCommit } from './api/git';
import { getHistoryItemHover, getHistoryItemHoverCommitHashCommands, processHistoryItemRemoteHoverCommands } from './historyProvider'; import { getCommitHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
export class GitTimelineItem extends TimelineItem { export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem { static is(item: TimelineItem): item is GitTimelineItem {
@@ -198,11 +198,11 @@ export class GitTimelineProvider implements TimelineProvider {
const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message;
const commands: Command[][] = [ const commands: Command[][] = [
getHistoryItemHoverCommitHashCommands(uri, c.hash), getHoverCommitHashCommands(uri, c.hash),
processHistoryItemRemoteHoverCommands(commitRemoteSourceCommands, c.hash) processHoverRemoteCommands(commitRemoteSourceCommands, c.hash)
]; ];
item.tooltip = getHistoryItemHover(avatars?.get(c.hash), c.authorName, c.authorEmail, date, messageWithLinks, c.shortStat, undefined, commands); item.tooltip = getCommitHover(avatars?.get(c.hash), c.authorName, c.authorEmail, date, messageWithLinks, c.shortStat, commands);
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) { if (cmd) {
@@ -227,7 +227,7 @@ export class GitTimelineProvider implements TimelineProvider {
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe? // TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new ThemeIcon('git-commit'); item.iconPath = new ThemeIcon('git-commit');
item.description = ''; item.description = '';
item.tooltip = getHistoryItemHover(undefined, you, undefined, date, Resource.getStatusText(index.type), undefined, undefined, undefined); item.tooltip = getCommitHover(undefined, you, undefined, date, Resource.getStatusText(index.type), undefined, undefined);
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) { if (cmd) {
@@ -249,7 +249,7 @@ export class GitTimelineProvider implements TimelineProvider {
const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
item.iconPath = new ThemeIcon('circle-outline'); item.iconPath = new ThemeIcon('circle-outline');
item.description = ''; item.description = '';
item.tooltip = getHistoryItemHover(undefined, you, undefined, date, Resource.getStatusText(working.type), undefined, undefined, undefined); item.tooltip = getCommitHover(undefined, you, undefined, date, Resource.getStatusText(working.type), undefined, undefined);
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) { if (cmd) {

View File

@@ -1713,7 +1713,7 @@ export interface SCMHistoryItemDto {
readonly deletions: number; readonly deletions: number;
}; };
readonly references?: SCMHistoryItemRefDto[]; readonly references?: SCMHistoryItemRefDto[];
readonly tooltip?: string | IMarkdownString | undefined; readonly tooltip?: IMarkdownString | Array<IMarkdownString> | undefined;
} }
export interface SCMHistoryItemChangeDto { export interface SCMHistoryItemChangeDto {

View File

@@ -75,7 +75,9 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc
function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto {
const authorIcon = getHistoryItemIconDto(historyItem.authorIcon); const authorIcon = getHistoryItemIconDto(historyItem.authorIcon);
const tooltip = MarkdownString.fromStrict(historyItem.tooltip); const tooltip = Array.isArray(historyItem.tooltip)
? MarkdownString.fromMany(historyItem.tooltip)
: historyItem.tooltip ? MarkdownString.from(historyItem.tooltip) : undefined;
const references = historyItem.references?.map(r => ({ const references = historyItem.references?.map(r => ({
...r, icon: getHistoryItemIconDto(r.icon) ...r, icon: getHistoryItemIconDto(r.icon)

View File

@@ -42,6 +42,7 @@ import { FileKind, IFileService } from '../../../../platform/files/common/files.
import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILabelService } from '../../../../platform/label/common/label.js';
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener/common/opener.js'; import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener/common/opener.js';
import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../browser/dnd.js'; import { fillEditorsDragData } from '../../../browser/dnd.js';
@@ -52,6 +53,7 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer
import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';
import { CellUri } from '../../notebook/common/notebookCommon.js'; import { CellUri } from '../../notebook/common/notebookCommon.js';
import { INotebookService } from '../../notebook/common/notebookService.js'; import { INotebookService } from '../../notebook/common/notebookService.js';
import { toHistoryItemHoverContent } from '../../scm/browser/scmHistory.js';
import { getHistoryItemEditorTitle } from '../../scm/browser/util.js'; import { getHistoryItemEditorTitle } from '../../scm/browser/util.js';
import { ITerminalService } from '../../terminal/browser/terminal.js'; import { ITerminalService } from '../../terminal/browser/terminal.js';
import { IChatContentReference } from '../common/chatService.js'; import { IChatContentReference } from '../common/chatService.js';
@@ -913,6 +915,7 @@ export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget
container: HTMLElement, container: HTMLElement,
contextResourceLabels: ResourceLabels, contextResourceLabels: ResourceLabels,
@ICommandService commandService: ICommandService, @ICommandService commandService: ICommandService,
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
@IHoverService hoverService: IHoverService, @IHoverService hoverService: IHoverService,
@IOpenerService openerService: IOpenerService, @IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService @IThemeService themeService: IThemeService
@@ -924,12 +927,12 @@ export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget
this.element.style.cursor = 'pointer'; this.element.style.cursor = 'pointer';
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
const historyItem = attachment.historyItem; const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
const hoverContent = historyItem.tooltip ?? historyItem.message;
this._store.add(hoverService.setupDelayedHover(this.element, { this._store.add(hoverService.setupDelayedHover(this.element, {
...commonHoverOptions, ...commonHoverOptions,
content: hoverContent, content,
}, commonHoverLifecycleOptions)); }, commonHoverLifecycleOptions));
this._store.add(disposables);
this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => { this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, true); dom.EventHelper.stop(e, true);
@@ -963,6 +966,7 @@ export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachment
contextResourceLabels: ResourceLabels, contextResourceLabels: ResourceLabels,
@ICommandService commandService: ICommandService, @ICommandService commandService: ICommandService,
@IHoverService hoverService: IHoverService, @IHoverService hoverService: IHoverService,
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
@IOpenerService openerService: IOpenerService, @IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@IEditorService private readonly editorService: IEditorService, @IEditorService private readonly editorService: IEditorService,
@@ -974,12 +978,11 @@ export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachment
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
const historyItem = attachment.historyItem; const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
const hoverContent = historyItem.tooltip ?? historyItem.message;
this._store.add(hoverService.setupDelayedHover(this.element, { this._store.add(hoverService.setupDelayedHover(this.element, {
...commonHoverOptions, ...commonHoverOptions, content,
content: hoverContent,
}, commonHoverLifecycleOptions)); }, commonHoverLifecycleOptions));
this._store.add(disposables);
this.addResourceOpenHandlers(attachment.value, undefined); this.addResourceOpenHandlers(attachment.value, undefined);
this.attachClearButton(); this.attachClearButton();

View File

@@ -587,62 +587,53 @@
/* History item hover */ /* History item hover */
.monaco-hover.history-item-hover p:first-child { .monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:first-child > p {
margin-top: 4px; margin-top: 4px;
} }
.monaco-hover.history-item-hover p:last-child { .monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:last-child p {
margin-bottom: 2px !important; margin-bottom: 2px !important;
} }
.monaco-hover.history-item-hover p:last-child span:not(.codicon) { .monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:last-child p span:not(.codicon) {
padding: 2px 0; padding: 2px 0;
} }
.monaco-hover.history-item-hover hr { .monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown hr {
margin-top: 4px; margin-top: 4px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.monaco-hover.history-item-hover hr + p { .monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown > p {
margin: 4px 0; margin: 4px 0;
} }
.monaco-hover.history-item-hover hr:nth-of-type(2):nth-last-of-type(2) + p { .monaco-hover.history-item-hover .history-item-hover-container div:nth-of-type(3):nth-last-of-type(2) > p {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
} }
.monaco-hover.history-item-hover span:not(.codicon) { .monaco-hover.history-item-hover .history-item-hover-container span:not(.codicon) {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.monaco-hover.history-item-hover p > span > span.codicon.codicon-git-branch { .monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-git-branch {
font-size: 12px; font-size: 12px;
margin-bottom: 2px !important; margin-bottom: 2px !important;
} }
.monaco-hover.history-item-hover p > span > span.codicon.codicon-tag, .monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-tag,
.monaco-hover.history-item-hover p > span > span.codicon.codicon-target { .monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-target {
font-size: 14px; font-size: 14px;
margin-bottom: 2px !important; margin-bottom: 2px !important;
} }
.monaco-hover.history-item-hover p > span > span.codicon.codicon-cloud { .monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-cloud {
font-size: 14px; font-size: 14px;
margin-bottom: 1px !important; margin-bottom: 1px !important;
} }
.monaco-hover.history-item-hover .hover-row.status-bar .action {
display: flex;
align-items: center;
}
.monaco-hover.history-item-hover .hover-row.status-bar .action .codicon {
color: inherit;
}
/* Graph */ /* Graph */
.pane-header .scm-graph-view-badge-container { .pane-header .scm-graph-view-badge-container {

View File

@@ -9,8 +9,12 @@ import { badgeBackground, chartsBlue, chartsPurple, foreground } from '../../../
import { asCssVariable, ColorIdentifier, registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { asCssVariable, ColorIdentifier, registerColor } from '../../../../platform/theme/common/colorUtils.js';
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js';
import { rot } from '../../../../base/common/numbers.js'; import { rot } from '../../../../base/common/numbers.js';
import { svgElem } from '../../../../base/browser/dom.js'; import { $, svgElem } from '../../../../base/browser/dom.js';
import { PANEL_BACKGROUND } from '../../../common/theme.js'; import { PANEL_BACKGROUND } from '../../../common/theme.js';
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { IMarkdownString, isEmptyMarkdownString, isMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
export const SWIMLANE_HEIGHT = 22; export const SWIMLANE_HEIGHT = 22;
export const SWIMLANE_WIDTH = 11; export const SWIMLANE_WIDTH = 11;
@@ -528,3 +532,52 @@ export function compareHistoryItemRefs(
return ref1Order - ref2Order; return ref1Order - ref2Order;
} }
export function toHistoryItemHoverContent(markdownRendererService: IMarkdownRendererService, historyItem: ISCMHistoryItem, includeReferences: boolean): { content: string | IMarkdownString | HTMLElement; disposables: IDisposable } {
const disposables = new DisposableStore();
if (historyItem.tooltip === undefined) {
return { content: historyItem.message, disposables };
}
if (isMarkdownString(historyItem.tooltip)) {
return { content: historyItem.tooltip, disposables };
}
// References as "injected" into the hover here since the extension does
// not know that color used in the graph to render the history item at which
// the reference is pointing to. They are being added before the last element
// of the array which is assumed to contain the hover commands.
const tooltipSections = historyItem.tooltip.slice();
if (includeReferences && historyItem.references?.length) {
const markdownString = new MarkdownString('', { supportHtml: true, supportThemeIcons: true });
for (const reference of historyItem.references) {
const labelIconId = ThemeIcon.isThemeIcon(reference.icon) ? reference.icon.id : '';
const labelBackgroundColor = reference.color ? asCssVariable(reference.color) : asCssVariable(historyItemHoverDefaultLabelBackground);
const labelForegroundColor = reference.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground);
markdownString.appendMarkdown(`<span style="color:${labelForegroundColor};background-color:${labelBackgroundColor};border-radius:10px;">&nbsp;$(${labelIconId})&nbsp;`);
markdownString.appendText(reference.name);
markdownString.appendMarkdown('&nbsp;&nbsp;</span>');
}
markdownString.appendMarkdown(`\n\n---\n\n`);
tooltipSections.splice(tooltipSections.length - 1, 0, markdownString);
}
// Render tooltip content
const hoverContainer = $('.history-item-hover-container');
for (const markdownString of tooltipSections) {
if (isEmptyMarkdownString(markdownString)) {
continue;
}
const renderedContent = markdownRendererService.render(markdownString);
hoverContainer.appendChild(renderedContent.element);
disposables.add(renderedContent);
}
return { content: hoverContainer, disposables };
}

View File

@@ -5,7 +5,7 @@
import './media/scm.css'; import './media/scm.css';
import { $, append, h, reset } from '../../../../base/browser/dom.js'; import { $, append, h, reset } from '../../../../base/browser/dom.js';
import { IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverOptions, IManagedHoverContent } from '../../../../base/browser/ui/hover/hover.js';
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js';
import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
@@ -28,7 +28,7 @@ import { asCssVariable, ColorIdentifier, foreground } from '../../../../platform
import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js'; import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js';
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverLabelForeground, historyItemHoverDefaultLabelBackground, getHistoryItemIndex } from './scmHistory.js'; import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverLabelForeground, historyItemHoverDefaultLabelBackground, getHistoryItemIndex, toHistoryItemHoverContent } from './scmHistory.js';
import { getHistoryItemEditorTitle, getProviderKey, isSCMHistoryItemChangeNode, isSCMHistoryItemChangeViewModelTreeElement, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; import { getHistoryItemEditorTitle, getProviderKey, isSCMHistoryItemChangeNode, isSCMHistoryItemChangeViewModelTreeElement, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js';
import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js';
import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService, ViewMode } from '../common/scm.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService, ViewMode } from '../common/scm.js';
@@ -76,6 +76,8 @@ import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/
import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js'; import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js';
import { SCMHistoryItemTransferData } from './scmHistoryChatContext.js'; import { SCMHistoryItemTransferData } from './scmHistoryChatContext.js';
import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
import { isMarkdownString } from '../../../../base/common/htmlContent.js';
const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository';
const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs';
@@ -454,6 +456,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
@IContextMenuService private readonly _contextMenuService: IContextMenuService, @IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IHoverService private readonly _hoverService: IHoverService, @IHoverService private readonly _hoverService: IHoverService,
@IKeybindingService private readonly _keybindingService: IKeybindingService, @IKeybindingService private readonly _keybindingService: IKeybindingService,
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
@IMenuService private readonly _menuService: IMenuService, @IMenuService private readonly _menuService: IMenuService,
@ITelemetryService private readonly _telemetryService: ITelemetryService @ITelemetryService private readonly _telemetryService: ITelemetryService
) { ) {
@@ -480,12 +483,10 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
const historyItemViewModel = node.element.historyItemViewModel; const historyItemViewModel = node.element.historyItemViewModel;
const historyItem = historyItemViewModel.historyItem; const historyItem = historyItemViewModel.historyItem;
const hoverContent = { const { content, disposables } = this.toHistoryItemHoverContent(historyItem);
markdown: historyItem.tooltip ?? historyItem.message, const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, content);
markdownNotSupportedFallback: historyItem.message
} satisfies IManagedHoverTooltipMarkdownString;
const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, hoverContent);
templateData.elementDisposables.add(historyItemHover); templateData.elementDisposables.add(historyItemHover);
templateData.elementDisposables.add(disposables);
templateData.graphContainer.textContent = ''; templateData.graphContainer.textContent = '';
templateData.graphContainer.classList.toggle('current', historyItemViewModel.kind === 'HEAD'); templateData.graphContainer.classList.toggle('current', historyItemViewModel.kind === 'HEAD');
@@ -589,6 +590,23 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
append(templateData.labelContainer, elements.root); append(templateData.labelContainer, elements.root);
} }
private toHistoryItemHoverContent(historyItem: ISCMHistoryItem): { content: IManagedHoverContent; disposables: IDisposable } {
// Depracte when we removed the usage of `this._hoverService.setupManagedHover`
const { content, disposables } = toHistoryItemHoverContent(this._markdownRendererService, historyItem, true);
if (isMarkdownString(content)) {
return {
content: {
markdown: content,
markdownNotSupportedFallback: historyItem.message
},
disposables
};
}
return { content, disposables };
}
private _processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { private _processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] {
if (!filterData) { if (!filterData) {
return [undefined, undefined]; return [undefined, undefined];

View File

@@ -72,7 +72,7 @@ export interface ISCMHistoryItem {
readonly timestamp?: number; readonly timestamp?: number;
readonly statistics?: ISCMHistoryItemStatistics; readonly statistics?: ISCMHistoryItemStatistics;
readonly references?: ISCMHistoryItemRef[]; readonly references?: ISCMHistoryItemRef[];
readonly tooltip?: string | IMarkdownString | undefined; readonly tooltip?: IMarkdownString | Array<IMarkdownString> | undefined;
} }
export interface ISCMHistoryItemGraphNode { export interface ISCMHistoryItemGraphNode {

View File

@@ -61,7 +61,7 @@ declare module 'vscode' {
readonly timestamp?: number; readonly timestamp?: number;
readonly statistics?: SourceControlHistoryItemStatistics; readonly statistics?: SourceControlHistoryItemStatistics;
readonly references?: SourceControlHistoryItemRef[]; readonly references?: SourceControlHistoryItemRef[];
readonly tooltip?: string | MarkdownString | undefined; readonly tooltip?: MarkdownString | Array<MarkdownString> | undefined;
} }
export interface SourceControlHistoryItemRef { export interface SourceControlHistoryItemRef {