mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-19 17:58:39 +00:00
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:
@@ -15,7 +15,7 @@ import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation }
|
||||
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
|
||||
import { AvatarQuery, AvatarQueryCommit } from './api/git';
|
||||
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 {
|
||||
return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive);
|
||||
@@ -251,8 +251,8 @@ export class GitBlameController {
|
||||
|
||||
// Commands
|
||||
const commands: Command[][] = [
|
||||
getHistoryItemHoverCommitHashCommands(documentUri, hash),
|
||||
processHistoryItemRemoteHoverCommands(remoteHoverCommands, hash)
|
||||
getHoverCommitHashCommands(documentUri, hash),
|
||||
processHoverRemoteCommands(remoteHoverCommands, hash)
|
||||
];
|
||||
|
||||
commands.push([{
|
||||
@@ -262,7 +262,7 @@ export class GitBlameController {
|
||||
arguments: ['git.blame']
|
||||
}] 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 {
|
||||
|
||||
@@ -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 { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, fromNow, getCommitShortHash, subject, truncate } from './util';
|
||||
import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, subject, truncate } from './util';
|
||||
import { toMultiFileDiffEditorUris } from './uri';
|
||||
import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git';
|
||||
import { emojify, ensureEmojis } from './emoji';
|
||||
import { Commit, CommitShortStat } from './git';
|
||||
import { Commit } from './git';
|
||||
import { OperationKind, OperationResult } from './operation';
|
||||
import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
|
||||
import { throttle } from './decorators';
|
||||
import { getHistoryItemHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
|
||||
|
||||
type SourceControlHistoryItemRefWithRenderOptions = SourceControlHistoryItemRef & {
|
||||
backgroundColor?: string;
|
||||
};
|
||||
|
||||
function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRefWithRenderOptions, ref2: SourceControlHistoryItemRefWithRenderOptions): number {
|
||||
const getOrder = (ref: SourceControlHistoryItemRefWithRenderOptions): number => {
|
||||
function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number {
|
||||
const getOrder = (ref: SourceControlHistoryItemRef): number => {
|
||||
if (ref.id.startsWith('refs/heads/')) {
|
||||
return ref.backgroundColor ? 1 : 5;
|
||||
return 1;
|
||||
} else if (ref.id.startsWith('refs/remotes/')) {
|
||||
return ref.backgroundColor ? 2 : 15;
|
||||
return 2;
|
||||
} else if (ref.id.startsWith('refs/tags/')) {
|
||||
return ref.backgroundColor ? 3 : 25;
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 99;
|
||||
@@ -308,11 +305,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
const references = this._resolveHistoryItemRefs(commit);
|
||||
|
||||
const commands: Command[][] = [
|
||||
getHistoryItemHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash),
|
||||
processHistoryItemRemoteHoverCommands(remoteHoverCommands, commit.hash)
|
||||
getHoverCommitHashCommands(Uri.file(this.repository.root), 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({
|
||||
id: commit.hash,
|
||||
@@ -489,8 +486,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
return this.historyItemDecorations.get(uri.toString());
|
||||
}
|
||||
|
||||
private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRefWithRenderOptions[] {
|
||||
const references: SourceControlHistoryItemRefWithRenderOptions[] = [];
|
||||
private _resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] {
|
||||
const references: SourceControlHistoryItemRef[] = [];
|
||||
|
||||
for (const ref of commit.refNames) {
|
||||
if (ref === 'refs/remotes/origin/HEAD') {
|
||||
@@ -504,8 +501,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
name: ref.substring('HEAD -> refs/heads/'.length),
|
||||
revision: commit.hash,
|
||||
category: l10n.t('branches'),
|
||||
icon: new ThemeIcon('target'),
|
||||
backgroundColor: `--vscode-scmGraph-historyItemRefColor`
|
||||
icon: new ThemeIcon('target')
|
||||
});
|
||||
break;
|
||||
case ref.startsWith('refs/heads/'):
|
||||
@@ -523,12 +519,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
name: ref.substring('refs/remotes/'.length),
|
||||
revision: commit.hash,
|
||||
category: l10n.t('remote branches'),
|
||||
icon: new ThemeIcon('cloud'),
|
||||
backgroundColor: ref === this.currentHistoryItemRemoteRef?.id
|
||||
? `--vscode-scmGraph-historyItemRemoteRefColor`
|
||||
: ref === this.currentHistoryItemBaseRef?.id
|
||||
? `--vscode-scmGraph-historyItemBaseRefColor`
|
||||
: undefined
|
||||
icon: new ThemeIcon('cloud')
|
||||
});
|
||||
break;
|
||||
case ref.startsWith('tag: refs/tags/'):
|
||||
@@ -537,10 +528,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
name: ref.substring('tag: refs/tags/'.length),
|
||||
revision: commit.hash,
|
||||
category: l10n.t('tags'),
|
||||
icon: new ThemeIcon('tag'),
|
||||
backgroundColor: ref === this.currentHistoryItemRef?.id
|
||||
? `--vscode-scmGraph-historyItemRefColor`
|
||||
: undefined
|
||||
icon: new ThemeIcon('tag')
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -621,127 +609,3 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
|
||||
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(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, '![').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(`, <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(`, <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;"> $(${labelIconId}) `);
|
||||
markdownString.appendText(reference.name);
|
||||
markdownString.appendMarkdown(` </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(' | ');
|
||||
}
|
||||
|
||||
const commandsMarkdown = commands[index]
|
||||
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`);
|
||||
markdownString.appendMarkdown(commandsMarkdown.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
return markdownString;
|
||||
}
|
||||
|
||||
161
extensions/git/src/hover.ts
Normal file
161
extensions/git/src/hover.ts
Normal 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(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, '![').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(`, <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(`, <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(' | ');
|
||||
}
|
||||
|
||||
const commandsMarkdown = commands[index]
|
||||
.map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`);
|
||||
markdownString.appendMarkdown(commandsMarkdown.join(' '));
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { OperationKind, OperationResult } from './operation';
|
||||
import { truncate } from './util';
|
||||
import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider';
|
||||
import { AvatarQuery, AvatarQueryCommit } from './api/git';
|
||||
import { getHistoryItemHover, getHistoryItemHoverCommitHashCommands, processHistoryItemRemoteHoverCommands } from './historyProvider';
|
||||
import { getCommitHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover';
|
||||
|
||||
export class GitTimelineItem extends TimelineItem {
|
||||
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 commands: Command[][] = [
|
||||
getHistoryItemHoverCommitHashCommands(uri, c.hash),
|
||||
processHistoryItemRemoteHoverCommands(commitRemoteSourceCommands, c.hash)
|
||||
getHoverCommitHashCommands(uri, 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);
|
||||
if (cmd) {
|
||||
@@ -227,7 +227,7 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
|
||||
item.iconPath = new ThemeIcon('git-commit');
|
||||
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);
|
||||
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');
|
||||
item.iconPath = new ThemeIcon('circle-outline');
|
||||
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);
|
||||
if (cmd) {
|
||||
|
||||
@@ -1713,7 +1713,7 @@ export interface SCMHistoryItemDto {
|
||||
readonly deletions: number;
|
||||
};
|
||||
readonly references?: SCMHistoryItemRefDto[];
|
||||
readonly tooltip?: string | IMarkdownString | undefined;
|
||||
readonly tooltip?: IMarkdownString | Array<IMarkdownString> | undefined;
|
||||
}
|
||||
|
||||
export interface SCMHistoryItemChangeDto {
|
||||
|
||||
@@ -75,7 +75,9 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc
|
||||
|
||||
function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto {
|
||||
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 => ({
|
||||
...r, icon: getHistoryItemIconDto(r.icon)
|
||||
|
||||
@@ -42,6 +42,7 @@ import { FileKind, IFileService } from '../../../../platform/files/common/files.
|
||||
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.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 { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.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 { CellUri } from '../../notebook/common/notebookCommon.js';
|
||||
import { INotebookService } from '../../notebook/common/notebookService.js';
|
||||
import { toHistoryItemHoverContent } from '../../scm/browser/scmHistory.js';
|
||||
import { getHistoryItemEditorTitle } from '../../scm/browser/util.js';
|
||||
import { ITerminalService } from '../../terminal/browser/terminal.js';
|
||||
import { IChatContentReference } from '../common/chatService.js';
|
||||
@@ -913,6 +915,7 @@ export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget
|
||||
container: HTMLElement,
|
||||
contextResourceLabels: ResourceLabels,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@IThemeService themeService: IThemeService
|
||||
@@ -924,12 +927,12 @@ export class SCMHistoryItemAttachmentWidget extends AbstractChatAttachmentWidget
|
||||
this.element.style.cursor = 'pointer';
|
||||
this.element.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name);
|
||||
|
||||
const historyItem = attachment.historyItem;
|
||||
const hoverContent = historyItem.tooltip ?? historyItem.message;
|
||||
const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
|
||||
this._store.add(hoverService.setupDelayedHover(this.element, {
|
||||
...commonHoverOptions,
|
||||
content: hoverContent,
|
||||
content,
|
||||
}, commonHoverLifecycleOptions));
|
||||
this._store.add(disposables);
|
||||
|
||||
this._store.add(dom.addDisposableListener(this.element, dom.EventType.CLICK, (e: MouseEvent) => {
|
||||
dom.EventHelper.stop(e, true);
|
||||
@@ -963,6 +966,7 @@ export class SCMHistoryItemChangeAttachmentWidget extends AbstractChatAttachment
|
||||
contextResourceLabels: ResourceLabels,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@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);
|
||||
|
||||
const historyItem = attachment.historyItem;
|
||||
const hoverContent = historyItem.tooltip ?? historyItem.message;
|
||||
const { content, disposables } = toHistoryItemHoverContent(markdownRendererService, attachment.historyItem, false);
|
||||
this._store.add(hoverService.setupDelayedHover(this.element, {
|
||||
...commonHoverOptions,
|
||||
content: hoverContent,
|
||||
...commonHoverOptions, content,
|
||||
}, commonHoverLifecycleOptions));
|
||||
this._store.add(disposables);
|
||||
|
||||
this.addResourceOpenHandlers(attachment.value, undefined);
|
||||
this.attachClearButton();
|
||||
|
||||
@@ -587,62 +587,53 @@
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.monaco-hover.history-item-hover hr {
|
||||
.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown hr {
|
||||
margin-top: 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-wrap: wrap;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.monaco-hover.history-item-hover 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-tag,
|
||||
.monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-target {
|
||||
font-size: 14px;
|
||||
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;
|
||||
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 */
|
||||
|
||||
.pane-header .scm-graph-view-badge-container {
|
||||
|
||||
@@ -9,8 +9,12 @@ import { badgeBackground, chartsBlue, chartsPurple, foreground } from '../../../
|
||||
import { asCssVariable, ColorIdentifier, registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.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 { 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_WIDTH = 11;
|
||||
@@ -528,3 +532,52 @@ export function compareHistoryItemRefs(
|
||||
|
||||
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;"> $(${labelIconId}) `);
|
||||
markdownString.appendText(reference.name);
|
||||
markdownString.appendMarkdown(' </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 };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import './media/scm.css';
|
||||
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 { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.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 { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.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 { 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';
|
||||
@@ -76,6 +76,8 @@ import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/
|
||||
import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js';
|
||||
import { SCMHistoryItemTransferData } from './scmHistoryChatContext.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_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs';
|
||||
@@ -454,6 +456,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IHoverService private readonly _hoverService: IHoverService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
|
||||
@IMenuService private readonly _menuService: IMenuService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
@@ -480,12 +483,10 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
|
||||
const historyItemViewModel = node.element.historyItemViewModel;
|
||||
const historyItem = historyItemViewModel.historyItem;
|
||||
|
||||
const hoverContent = {
|
||||
markdown: historyItem.tooltip ?? historyItem.message,
|
||||
markdownNotSupportedFallback: historyItem.message
|
||||
} satisfies IManagedHoverTooltipMarkdownString;
|
||||
const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, hoverContent);
|
||||
const { content, disposables } = this.toHistoryItemHoverContent(historyItem);
|
||||
const historyItemHover = this._hoverService.setupManagedHover(this.hoverDelegate, templateData.element, content);
|
||||
templateData.elementDisposables.add(historyItemHover);
|
||||
templateData.elementDisposables.add(disposables);
|
||||
|
||||
templateData.graphContainer.textContent = '';
|
||||
templateData.graphContainer.classList.toggle('current', historyItemViewModel.kind === 'HEAD');
|
||||
@@ -589,6 +590,23 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
|
||||
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] {
|
||||
if (!filterData) {
|
||||
return [undefined, undefined];
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface ISCMHistoryItem {
|
||||
readonly timestamp?: number;
|
||||
readonly statistics?: ISCMHistoryItemStatistics;
|
||||
readonly references?: ISCMHistoryItemRef[];
|
||||
readonly tooltip?: string | IMarkdownString | undefined;
|
||||
readonly tooltip?: IMarkdownString | Array<IMarkdownString> | undefined;
|
||||
}
|
||||
|
||||
export interface ISCMHistoryItemGraphNode {
|
||||
|
||||
@@ -61,7 +61,7 @@ declare module 'vscode' {
|
||||
readonly timestamp?: number;
|
||||
readonly statistics?: SourceControlHistoryItemStatistics;
|
||||
readonly references?: SourceControlHistoryItemRef[];
|
||||
readonly tooltip?: string | MarkdownString | undefined;
|
||||
readonly tooltip?: MarkdownString | Array<MarkdownString> | undefined;
|
||||
}
|
||||
|
||||
export interface SourceControlHistoryItemRef {
|
||||
|
||||
Reference in New Issue
Block a user