From 20e6bfea4a58abe7327247fc6f8cf01d2e804677 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 4 Nov 2021 01:09:39 -0400 Subject: [PATCH] Adds rich hovers to the status bar --- CHANGELOG.md | 3 + README.md | 1 + package.json | 6 + src/annotations/blameAnnotationProvider.ts | 7 + src/config.ts | 1 + src/hovers/hovers.ts | 52 +++++-- src/hovers/lineHoverController.ts | 7 + src/statusbar/statusBarController.ts | 152 ++++++++++++++++----- 8 files changed, 179 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ec5975696e5..19c248c0cb231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- Adds a new rich commit details hover to the blame information in the status bar + - Adds a `gitlens.statusBar.tooltipFormat` setting to specify the format (in markdown) of hover shown over the blame information in the status bar +- Adds a new rich hover to the GitLens mode in the status bar - Adds a new _Cherry Pick without Committing_ confirmation option to the _Git Command Palette_'s _cherry-pick_ command — closes [#1693](https://github.com/eamodio/vscode-gitlens/issues/1693) - Adds new _Open File_ command (with _Open Revision_ as an `alt-click`) to files in comparisons — closes [#1710](https://github.com/eamodio/vscode-gitlens/issues/1710) diff --git a/README.md b/README.md index 01d5951b46fee..cf199e1fe07e4 100644 --- a/README.md +++ b/README.md @@ -719,6 +719,7 @@ GitLens is highly customizable and provides many configuration settings to allow | `gitlens.statusBar.format` | Specifies the format of the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs. Date formatting is controlled by the `gitlens.statusBar.dateFormat` setting | | `gitlens.statusBar.pullRequests.enabled` | Specifies whether to provide information about the Pull Request (if any) that introduced the commit in the status bar. Requires a connection to a supported remote service (e.g. GitHub) | | `gitlens.statusBar.reduceFlicker` | Specifies whether to avoid clearing the previous blame information when changing lines to reduce status bar "flashing" | +| `gitlens.statusBar.tooltipFormat` | Specifies the format (in markdown) of hover shown over the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs | ## Hover Settings [#](#hover-settings- 'Hover Settings') diff --git a/package.json b/package.json index 8ecc8c36487e0..910ca5ef9cf44 100644 --- a/package.json +++ b/package.json @@ -1723,6 +1723,12 @@ "markdownDescription": "Specifies whether to avoid clearing the previous blame information when changing lines to reduce status bar \"flashing\"", "scope": "window" }, + "gitlens.statusBar.tooltipFormat": { + "type": "string", + "default": "${avatar}  __${author}__, ${ago}${' via 'pullRequest}   _(${date})_ \n\n${message}\n\n${commands}${\n\n---\n\nfootnotes}", + "markdownDescription": "Specifies the format (in markdown) of hover shown over the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs", + "scope": "window" + }, "gitlens.strings.codeLens.unsavedChanges.recentChangeAndAuthors": { "type": "string", "default": "$(ellipsis)", diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 1a88798a25ad5..1679fb8ca5892 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -194,7 +194,14 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase logCommit ?? commit, await GitUri.fromUri(document.uri), editorLine, + Container.config.hovers.detailsMarkdownFormat, Container.config.defaultDateFormat, + { + autolinks: Container.config.hovers.autolinks.enabled, + pullRequests: { + enabled: Container.config.hovers.pullRequests.enabled, + }, + }, ); } } diff --git a/src/config.ts b/src/config.ts index 2c9b7514f5180..d593706d8e3c6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -134,6 +134,7 @@ export interface Config { pullRequests: { enabled: boolean; }; + tooltipFormat: string; }; strings: { codeLens: { diff --git a/src/hovers/hovers.ts b/src/hovers/hovers.ts index 7112c20721feb..ea8697b1ac7b6 100644 --- a/src/hovers/hovers.ts +++ b/src/hovers/hovers.ts @@ -12,6 +12,7 @@ import { GitLogCommit, GitRemote, GitRevision, + PullRequest, } from '../git/git'; import { GitUri } from '../git/gitUri'; import { Logger } from '../logger'; @@ -181,7 +182,19 @@ export namespace Hovers { commit: GitCommit, uri: GitUri, editorLine: number, + format: string, dateFormat: string | null, + options?: { + autolinks?: boolean; + pullRequests?: { + enabled: boolean; + pr?: PullRequest | Promises.CancellationError>; + }; + getBranchAndTagTips?: ( + sha: string, + options?: { compact?: boolean | undefined; icons?: boolean | undefined }, + ) => string | undefined; + }, ): Promise { if (dateFormat === null) { dateFormat = 'MMMM Do, YYYY h:mma'; @@ -192,19 +205,32 @@ export namespace Hovers { const [previousLineDiffUris, autolinkedIssuesOrPullRequests, pr, presence] = await Promise.all([ commit.isUncommitted ? commit.getPreviousLineDiffUris(uri, editorLine, uri.sha) : undefined, getAutoLinkedIssuesOrPullRequests(commit.message, remotes), - getPullRequestForCommit(commit.ref, remotes), + options?.pullRequests?.pr ?? + getPullRequestForCommit(commit.ref, remotes, { + pullRequests: + options?.pullRequests?.enabled || + CommitFormatter.has( + format, + 'pullRequest', + 'pullRequestAgo', + 'pullRequestAgoOrDate', + 'pullRequestDate', + 'pullRequestState', + ), + }), Container.vsls.maybeGetPresence(commit.email), ]); - const details = await CommitFormatter.fromTemplateAsync(Container.config.hovers.detailsMarkdownFormat, commit, { + const details = await CommitFormatter.fromTemplateAsync(format, commit, { autolinkedIssuesOrPullRequests: autolinkedIssuesOrPullRequests, dateFormat: dateFormat, editor: { line: editorLine, uri: uri, }, + getBranchAndTagTips: options?.getBranchAndTagTips, markdown: true, - messageAutolinks: Container.config.hovers.autolinks.enabled, + messageAutolinks: options?.autolinks, pullRequestOrRemote: pr, presence: presence, previousLineDiffUris: previousLineDiffUris, @@ -303,23 +329,19 @@ export namespace Hovers { } } - async function getPullRequestForCommit(ref: string, remotes: GitRemote[]) { + async function getPullRequestForCommit( + ref: string, + remotes: GitRemote[], + options?: { + pullRequests?: boolean; + }, + ) { const cc = Logger.getNewCorrelationContext('Hovers.getPullRequestForCommit'); Logger.debug(cc, `${GlyphChars.Dash} ref=${ref}`); const start = process.hrtime(); - if ( - !Container.config.hovers.pullRequests.enabled || - !CommitFormatter.has( - Container.config.hovers.detailsMarkdownFormat, - 'pullRequest', - 'pullRequestAgo', - 'pullRequestAgoOrDate', - 'pullRequestDate', - 'pullRequestState', - ) - ) { + if (!options?.pullRequests) { Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`); return undefined; diff --git a/src/hovers/lineHoverController.ts b/src/hovers/lineHoverController.ts index b1b2964c50c34..3b47bf6fecc7d 100644 --- a/src/hovers/lineHoverController.ts +++ b/src/hovers/lineHoverController.ts @@ -138,7 +138,14 @@ export class LineHoverController implements Disposable { logCommit ?? commit, trackedDocument.uri, editorLine, + Container.config.hovers.detailsMarkdownFormat, Container.config.defaultDateFormat, + { + autolinks: Container.config.hovers.autolinks.enabled, + pullRequests: { + enabled: Container.config.hovers.pullRequests.enabled, + }, + }, ); return new Hover(message, range); } diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index 5d478ccf40162..a060389c2fb30 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -4,6 +4,7 @@ import { CancellationTokenSource, ConfigurationChangeEvent, Disposable, + MarkdownString, StatusBarAlignment, StatusBarItem, TextEditor, @@ -15,12 +16,16 @@ import { configuration, FileAnnotationType, StatusBarCommand } from '../configur import { GlyphChars, isTextEditor } from '../constants'; import { Container } from '../container'; import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; +import { Hovers } from '../hovers/hovers'; import { LogCorrelationContext, Logger } from '../logger'; -import { debug, Promises } from '../system'; +import { debug, Functions, Promises } from '../system'; import { LinesChangeEvent } from '../trackers/gitLineTracker'; export class StatusBarController implements Disposable { - private _cancellation: CancellationTokenSource | undefined; + private _pullRequestCancellation: CancellationTokenSource | undefined; + private _tooltipCancellation: CancellationTokenSource | undefined; + private _tooltipDelayTimer: any | undefined; + private readonly _disposable: Disposable; private _statusBarBlame: StatusBarItem | undefined; private _statusBarMode: StatusBarItem | undefined; @@ -69,7 +74,10 @@ export class StatusBarController implements Disposable { this._statusBarMode.name = 'GitLens Modes'; this._statusBarMode.command = Commands.SwitchMode; this._statusBarMode.text = mode.statusBarItemName; - this._statusBarMode.tooltip = 'Switch GitLens Mode'; + this._statusBarMode.tooltip = new MarkdownString( + `**${mode.statusBarItemName}** ${GlyphChars.Dash} ${mode.description}\n\n---\n\nClick to Switch GitLens Mode`, + true, + ); this._statusBarMode.show(); } else { this._statusBarMode?.dispose(); @@ -148,7 +156,8 @@ export class StatusBarController implements Disposable { } clearBlame() { - this._cancellation?.cancel(); + this._pullRequestCancellation?.cancel(); + this._tooltipCancellation?.cancel(); this._statusBarBlame?.hide(); } @@ -159,30 +168,40 @@ export class StatusBarController implements Disposable { const cc = Logger.getCorrelationContext(); - // TODO: Make this configurable? - const timeout = 100; - const [getBranchAndTagTips, pr] = await Promise.all([ - CommitFormatter.has(cfg.format, 'tips') - ? Container.git.getBranchesAndTagsTipsFn(commit.repoPath) - : undefined, + const showPullRequests = cfg.pullRequests.enabled && - CommitFormatter.has( + (CommitFormatter.has( cfg.format, 'pullRequest', 'pullRequestAgo', 'pullRequestAgoOrDate', 'pullRequestDate', 'pullRequestState', - ) && - options?.pr === undefined + ) || + CommitFormatter.has( + cfg.tooltipFormat, + 'pullRequest', + 'pullRequestAgo', + 'pullRequestAgoOrDate', + 'pullRequestDate', + 'pullRequestState', + )); + + // TODO: Make this configurable? + const timeout = 100; + const [getBranchAndTagTips, pr] = await Promise.all([ + CommitFormatter.has(cfg.format, 'tips') || CommitFormatter.has(cfg.tooltipFormat, 'tips') + ? Container.git.getBranchesAndTagsTipsFn(commit.repoPath) + : undefined, + showPullRequests && options?.pr === undefined ? this.getPullRequest(commit, { timeout: timeout }) : options?.pr ?? undefined, ]); if (pr != null) { - this._cancellation?.cancel(); - this._cancellation = new CancellationTokenSource(); - void this.waitForPendingPullRequest(editor, commit, pr, this._cancellation.token, timeout, cc); + this._pullRequestCancellation?.cancel(); + this._pullRequestCancellation = new CancellationTokenSource(); + void this.waitForPendingPullRequest(editor, commit, pr, this._pullRequestCancellation.token, timeout, cc); } this._statusBarBlame.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { @@ -193,51 +212,52 @@ export class StatusBarController implements Disposable { pullRequestPendingMessage: 'PR $(loading~spin)', })}`; + let tooltip: string; switch (cfg.command) { case StatusBarCommand.CopyRemoteCommitUrl: - this._statusBarBlame.tooltip = 'Copy Remote Commit Url'; + tooltip = 'Click to Copy Remote Commit Url'; break; case StatusBarCommand.CopyRemoteFileUrl: this._statusBarBlame.command = Commands.CopyRemoteFileUrl; - this._statusBarBlame.tooltip = 'Copy Remote File Revision Url'; + tooltip = 'Click to Copy Remote File Revision Url'; break; case StatusBarCommand.DiffWithPrevious: this._statusBarBlame.command = Commands.DiffLineWithPrevious; - this._statusBarBlame.tooltip = 'Open Line Changes with Previous Revision'; + tooltip = 'Click to Open Line Changes with Previous Revision'; break; case StatusBarCommand.DiffWithWorking: this._statusBarBlame.command = Commands.DiffLineWithWorking; - this._statusBarBlame.tooltip = 'Open Line Changes with Working File'; + tooltip = 'Click to Open Line Changes with Working File'; break; case StatusBarCommand.OpenCommitOnRemote: - this._statusBarBlame.tooltip = 'Open Commit on Remote'; + tooltip = 'Click to Open Commit on Remote'; break; case StatusBarCommand.OpenFileOnRemote: - this._statusBarBlame.tooltip = 'Open Revision on Remote'; + tooltip = 'Click to Open Revision on Remote'; break; case StatusBarCommand.RevealCommitInView: - this._statusBarBlame.tooltip = 'Reveal Commit in the Side Bar'; + tooltip = 'Click to Reveal Commit in the Side Bar'; break; case StatusBarCommand.ShowCommitsInView: - this._statusBarBlame.tooltip = 'Search for Commit'; + tooltip = 'Click to Search for Commit'; break; case StatusBarCommand.ShowQuickCommitDetails: - this._statusBarBlame.tooltip = 'Show Commit'; + tooltip = 'Click to Show Commit'; break; case StatusBarCommand.ShowQuickCommitFileDetails: - this._statusBarBlame.tooltip = 'Show Commit (file)'; + tooltip = 'Click to Show Commit (file)'; break; case StatusBarCommand.ShowQuickCurrentBranchHistory: - this._statusBarBlame.tooltip = 'Show Branch History'; + tooltip = 'Click to Show Branch History'; break; case StatusBarCommand.ShowQuickFileHistory: - this._statusBarBlame.tooltip = 'Show File History'; + tooltip = 'Click to Show File History'; break; case StatusBarCommand.ToggleCodeLens: - this._statusBarBlame.tooltip = 'Toggle Git CodeLens'; + tooltip = 'Click to Toggle Git CodeLens'; break; case StatusBarCommand.ToggleFileBlame: - this._statusBarBlame.tooltip = 'Toggle File Blame'; + tooltip = 'Click to Toggle File Blame'; break; case StatusBarCommand.ToggleFileChanges: { this._statusBarBlame.command = command<[Uri, ToggleFileChangesAnnotationCommandArgs]>({ @@ -251,7 +271,7 @@ export class StatusBarController implements Disposable { }, ], }); - this._statusBarBlame.tooltip = 'Toggle File Changes'; + tooltip = 'Click to Toggle File Changes'; break; } case StatusBarCommand.ToggleFileChangesOnly: { @@ -266,18 +286,42 @@ export class StatusBarController implements Disposable { }, ], }); - this._statusBarBlame.tooltip = 'Toggle File Changes'; + tooltip = 'Click to Toggle File Changes'; break; } case StatusBarCommand.ToggleFileHeatmap: - this._statusBarBlame.tooltip = 'Toggle File Heatmap'; + tooltip = 'Click to Toggle File Heatmap'; break; } + this._statusBarBlame.tooltip = tooltip; + + clearTimeout(this._tooltipDelayTimer); + this._tooltipCancellation?.cancel(); + + this._tooltipDelayTimer = setTimeout(() => { + this._tooltipCancellation = new CancellationTokenSource(); + + void this.updateCommitTooltip( + this._statusBarBlame!, + commit, + tooltip, + getBranchAndTagTips, + { + enabled: showPullRequests || pr != null, + pr: pr, + }, + this._tooltipCancellation.token, + ); + }, 500); + this._statusBarBlame.show(); } - private async getPullRequest(commit: GitBlameCommit, { timeout }: { timeout?: number } = {}) { + private async getPullRequest( + commit: GitBlameCommit, + { timeout }: { timeout?: number } = {}, + ): Promise> | undefined> { const remote = await Container.git.getRichRemoteProvider(commit.repoPath); if (remote?.provider == null) return undefined; @@ -285,10 +329,48 @@ export class StatusBarController implements Disposable { try { return await Container.git.getPullRequestForCommit(commit.ref, provider, { timeout: timeout }); } catch (ex) { - return ex; + return ex instanceof Promises.CancellationError ? ex : undefined; } } + private async updateCommitTooltip( + statusBarItem: StatusBarItem, + commit: GitBlameCommit, + actionTooltip: string, + getBranchAndTagTips: + | (( + sha: string, + options?: { compact?: boolean | undefined; icons?: boolean | undefined } | undefined, + ) => string | undefined) + | undefined, + pullRequests: { + enabled: boolean; + pr: PullRequest | Promises.CancellationError> | undefined | undefined; + }, + cancellationToken: CancellationToken, + ) { + if (cancellationToken.isCancellationRequested) return; + + void (await Functions.wait(10000)); + const tooltip = await Hovers.detailsMessage( + commit, + commit.toGitUri(), + commit.lines[0].line, + Container.config.statusBar.tooltipFormat, + Container.config.defaultDateFormat, + { + autolinks: true, + getBranchAndTagTips: getBranchAndTagTips, + pullRequests: pullRequests, + }, + ); + + if (cancellationToken.isCancellationRequested) return; + + tooltip.appendMarkdown(`\n\n---\n\n${actionTooltip}`); + statusBarItem.tooltip = tooltip; + } + private async waitForPendingPullRequest( editor: TextEditor, commit: GitBlameCommit,