diff --git a/README.md b/README.md index 636b8b2..af06d9d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Provides Git CodeLens information (most recent commit, # of authors), on-demand inline blame annotations, status bar blame information, file and blame history explorers, and commands to compare changes with the working tree or previous versions. ---- ## Features - Provides (optional) **CodeLens** on code blocks: diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 678af94..6e171da 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -99,6 +99,13 @@ export default class BlameAnnotationController extends Disposable { return provider.provideBlameAnnotation(shaOrLine); } + isAnnotating(editor: TextEditor): boolean { + if (!editor || !editor.document) return false; + if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; + + return !!this._annotationProviders.get(editor.viewColumn || -1); + } + async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise { if (!editor || !editor.document) return false; if (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor)) return false; diff --git a/src/blameAnnotationFormatter.ts b/src/blameAnnotationFormatter.ts index daca0d7..514eb3d 100644 --- a/src/blameAnnotationFormatter.ts +++ b/src/blameAnnotationFormatter.ts @@ -9,16 +9,22 @@ export const defaultRelativeDateLength = 13; export const defaultAuthorLength = 16; export const defaultMessageLength = 32; -export let cssEllipse = '\\2026'; -export let cssIndent = '\\2759'; -export let cssSeparator = '\\2022'; -export let cssPadding = '\\00a0'; +export let cssEllipse = '\\002026'; +export let cssIndent = '\\002759'; +export let cssSeparator = '\\002022'; +export let cssPadding = '\\0000a0'; + +let cssEllipseLength: number = 1; + +const cssUnicodeMatcher = /\\[0-9a-fA-F]{1,6}/; export function configureCssCharacters(config: IBlameConfig) { cssEllipse = config.annotation.characters.ellipse || cssEllipse; cssIndent = config.annotation.characters.indent || cssIndent; cssPadding = config.annotation.characters.padding || cssPadding; cssSeparator = config.annotation.characters.separator || cssSeparator; + + cssEllipseLength = cssUnicodeMatcher.test(cssEllipse) ? 1 : cssEllipse.length; } export enum BlameAnnotationFormat { @@ -35,10 +41,10 @@ export default class BlameAnnotationFormatter { if (format === BlameAnnotationFormat.Unconstrained) { const authorAndDate = this.getAuthorAndDate(config, commit, 'MMMM Do, YYYY h:MMa'); if (config.annotation.sha) { - message = `${sha}${(authorAndDate ? ` ${cssSeparator} ${authorAndDate}` : '')}${(message ? ` ${cssSeparator} ${message}` : '')}`; + message = `${sha}${(authorAndDate ? `${cssPadding}${cssSeparator}${cssPadding} ${authorAndDate}` : '')}${(message ? `${cssPadding}${cssSeparator}${cssPadding} ${message}` : '')}`; } else if (config.annotation.author || config.annotation.date) { - message = `${authorAndDate}${(message ? ` ${cssSeparator} ${message}` : '')}`; + message = `${authorAndDate}${(message ? `${cssPadding}${cssSeparator}${cssPadding} ${message}` : '')}`; } return message; @@ -47,13 +53,13 @@ export default class BlameAnnotationFormatter { const author = this.getAuthor(config, commit, defaultAuthorLength); const date = this.getDate(config, commit, 'MM/DD/YYYY', true); if (config.annotation.sha) { - message = `${sha}${(author ? ` ${cssSeparator} ${author}` : '')}${(date ? ` ${cssSeparator} ${date}` : '')}${(message ? ` ${cssSeparator} ${message}` : '')}`; + message = `${sha}${(author ? `${cssPadding}${cssSeparator}${cssPadding} ${author}` : '')}${(date ? `${cssPadding}${cssSeparator}${cssPadding} ${date}` : '')}${(message ? `${cssPadding}${cssSeparator}${cssPadding} ${message}` : '')}`; } else if (config.annotation.author) { - message = `${author}${(date ? ` ${cssSeparator} ${date}` : '')}${(message ? ` ${cssSeparator} ${message}` : '')}`; + message = `${author}${(date ? `${cssPadding}${cssSeparator}${cssPadding} ${date}` : '')}${(message ? `${cssPadding}${cssSeparator}${cssPadding} ${message}` : '')}`; } else if (config.annotation.date) { - message = `${date}${(message ? ` ${cssSeparator} ${message}` : '')}`; + message = `${date}${(message ? `${cssPadding}${cssSeparator}${cssPadding} ${message}` : '')}`; } return message; @@ -62,7 +68,7 @@ export default class BlameAnnotationFormatter { static getAnnotationHover(config: IBlameConfig, line: IGitCommitLine, commit: GitCommit): string | Array { const message = commit.message.replace(/\n/g, '\n\n'); if (commit.isUncommitted) { - return `\`${'0'.repeat(8)}\`   __Uncommitted changes__ \n\n > ${message}`; + return `\`${'0'.repeat(8)}\`   __Uncommitted changes__`; } return `\`${commit.sha}\`   __${commit.author}__, ${moment(commit.date).fromNow()} _(${moment(commit.date).format('MMMM Do, YYYY h:MMa')})_ \n\n > ${message}`; @@ -85,11 +91,11 @@ export default class BlameAnnotationFormatter { static getAuthor(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) { if (!force && !config.annotation.author) return ''; - const author = commit.isUncommitted ? 'Uncommitted' : commit.author; + const author = commit.isUncommitted ? 'Uncommited' : commit.author; if (!truncateTo) return author; if (author.length > truncateTo) { - return `${author.substring(0, truncateTo - cssEllipse.length)}${cssEllipse}`; + return `${author.substring(0, truncateTo - cssEllipseLength)}${cssEllipse}`; } if (force) return author; // Don't pad when just asking for the value @@ -106,7 +112,7 @@ export default class BlameAnnotationFormatter { const truncateTo = config.annotation.date === 'relative' ? defaultRelativeDateLength : defaultAbsoluteDateLength; if (date.length > truncateTo) { - return `${date.substring(0, truncateTo - cssEllipse.length)}${cssEllipse}`; + return `${date.substring(0, truncateTo - cssEllipseLength)}${cssEllipse}`; } if (force) return date; // Don't pad when just asking for the value @@ -116,9 +122,9 @@ export default class BlameAnnotationFormatter { static getMessage(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) { if (!force && !config.annotation.message) return ''; - let message = commit.message; + let message = commit.isUncommitted ? 'Uncommited change' : commit.message; if (truncateTo && message.length > truncateTo) { - return `${message.substring(0, truncateTo - cssEllipse.length)}${cssEllipse}`; + return `${message.substring(0, truncateTo - cssEllipseLength)}${cssEllipse}`; } return message; diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts index 6287538..80ec7d9 100644 --- a/src/blameStatusBarController.ts +++ b/src/blameStatusBarController.ts @@ -1,6 +1,7 @@ 'use strict'; -import { Objects } from './system'; +import { Functions, Objects } from './system'; import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import BlameAnnotationController from './blameAnnotationController'; import BlameAnnotationFormatter, { BlameAnnotationFormat } from './blameAnnotationFormatter'; import { TextDocumentComparer, TextEditorComparer } from './comparers'; import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; @@ -19,15 +20,19 @@ export default class BlameStatusBarController extends Disposable { private _activeEditorLineDisposable: Disposable | undefined; private _blame: Promise | undefined; private _config: IConfig; + private _currentLine: number = -1; private _disposable: Disposable; private _editor: TextEditor | undefined; + private _showBlameDebounced: (line: number, editor: TextEditor) => Promise; private _statusBarItem: StatusBarItem | undefined; private _uri: GitUri; private _useCaching: boolean; - constructor(context: ExtensionContext, private git: GitProvider) { + constructor(context: ExtensionContext, private git: GitProvider, private annotationController: BlameAnnotationController) { super(() => this.dispose()); + this._showBlameDebounced = Functions.debounce(this._showBlame, 50); + this._onConfigure(); const subscriptions: Disposable[] = []; @@ -104,7 +109,9 @@ export default class BlameStatusBarController extends Disposable { this._onActiveTextEditorChanged(window.activeTextEditor); } - private async _onActiveTextEditorChanged(e: TextEditor): Promise { + private _onActiveTextEditorChanged(e: TextEditor) { + this._currentLine = -1; + const previousEditor = this._editor; previousEditor && previousEditor.setDecorations(activeLineDecoration, []); @@ -129,21 +136,26 @@ export default class BlameStatusBarController extends Disposable { this._blame = undefined; } - return await this._showBlame(e.selection.active.line, e); + this._showBlame(e.selection.active.line, e); } - private async _onEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { + private _onEditorSelectionChanged(e: TextEditorSelectionChangeEvent): void { // Make sure this is for the editor we are tracking if (!TextEditorComparer.equals(e.textEditor, this._editor)) return; - return await this._showBlame(e.selections[0].active.line, e.textEditor); + const line = e.selections[0].active.line; + if (line === this._currentLine) return; + this._currentLine = line; + + this._showBlameDebounced(line, e.textEditor); } - private async _onDocumentChanged(e: TextDocumentChangeEvent): Promise { + private _onDocumentChanged(e: TextDocumentChangeEvent) { // Make sure this is for the editor we are tracking if (!this._editor || !TextDocumentComparer.equals(e.document, this._editor.document)) return; + this._currentLine = -1; - return await this._showBlame(this._editor.selections[0].active.line, this._editor); + this._showBlame(this._editor.selections[0].active.line, this._editor); } private async _showBlame(line: number, editor: TextEditor) { @@ -248,11 +260,21 @@ export default class BlameStatusBarController extends Disposable { const log = await this.git.getLogForFile(this._uri.fsPath, commit.sha, this._uri.repoPath, undefined, 1); logCommit = log && log.commits.get(commit.sha); } - const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(config, blameLine, logCommit || commit); + + let hoverMessage: string | string[]; + if (activeLine !== 'inline') { + // If the messages match (or we couldn't find the log), then this is a possible duplicate annotation + const possibleDuplicate = !logCommit || logCommit.message === commit.message; + // If we don't have a possible dupe or we aren't showing annotations get the hover message + if (!possibleDuplicate || !this.annotationController.isAnnotating(editor)) { + hoverMessage = BlameAnnotationFormatter.getAnnotationHover(config, blameLine, logCommit || commit); + } + } let decorationOptions: DecorationOptions; switch (activeLine) { case 'both': + case 'inline': decorationOptions = { range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), hoverMessage: hoverMessage, @@ -265,18 +287,6 @@ export default class BlameStatusBarController extends Disposable { } as DecorationOptions; break; - case 'inline': - decorationOptions = { - range: editor.document.validateRange(new Range(blameLine.line + offset, 1000000, blameLine.line + offset, 1000000)), - renderOptions: { - after: { - color: 'rgba(153, 153, 153, 0.3)', - contentText: annotation - } - } as DecorationInstanceRenderOptions - } as DecorationOptions; - break; - case 'hover': decorationOptions = { range: editor.document.validateRange(new Range(blameLine.line + offset, 0, blameLine.line + offset, 1000000)), diff --git a/src/extension.ts b/src/extension.ts index 2671377..86abe6c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,7 +62,7 @@ export async function activate(context: ExtensionContext) { const annotationController = new BlameAnnotationController(context, git); context.subscriptions.push(annotationController); - const statusBarController = new BlameStatusBarController(context, git); + const statusBarController = new BlameStatusBarController(context, git, annotationController); context.subscriptions.push(statusBarController); context.subscriptions.push(new DiffWithWorkingCommand(git)); diff --git a/src/system/function.ts b/src/system/function.ts index c6c4182..2d9ea5b 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -4,11 +4,11 @@ const _once = require('lodash.once'); export interface IDeferred { cancel(): void; - flush(): void; + flush(...args: any[]): void; } export namespace Functions { - export function debounce(fn: T, wait?: number, options?: any): T & IDeferred { + export function debounce(fn: T, wait?: number, options?: { leading?: boolean, maxWait?: number, trailing?: boolean }): T & IDeferred { return _debounce(fn, wait, options); }