From dfacc2bf6e6ed5272445ca10d83650506883ea94 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 6 Feb 2017 13:14:38 -0500 Subject: [PATCH] Adds new trailing annotation mode Adds message setting to annotations Adds active line annotations & setting --- package.json | 36 ++++-- src/blameAnnotationFormatter.ts | 129 ++++++++++++++++++++++ src/blameAnnotationProvider.ts | 182 ++++++++++++++++++------------- src/blameStatusBarController.ts | 187 ++++++++++++++++++++++---------- src/configuration.ts | 11 +- 5 files changed, 396 insertions(+), 149 deletions(-) create mode 100644 src/blameAnnotationFormatter.ts diff --git a/package.json b/package.json index c34d449..0468073 100644 --- a/package.json +++ b/package.json @@ -47,24 +47,40 @@ "default": "expanded", "enum": [ "compact", - "expanded" + "expanded", + "trailing" ], - "description": "Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation on every line" + "description": "Specifies the style of the blame annotations. `compact` - groups annotations to limit the repetition and also adds author and date when possible. `expanded` - shows an annotation before every line. `trailing` - shows an annotation after every line" }, "gitlens.blame.annotation.sha": { "type": "boolean", "default": true, - "description": "Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` annotation style" + "description": "Specifies whether the commit sha will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" }, "gitlens.blame.annotation.author": { "type": "boolean", "default": true, - "description": "Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` annotation style" + "description": "Specifies whether the committer will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" }, "gitlens.blame.annotation.date": { + "type": "string", + "default": "off", + "enum": [ + "off", + "relative", + "absolute" + ], + "description": "Specifies whether and how the commit date will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" + }, + "gitlens.blame.annotation.message": { "type": "boolean", "default": false, - "description": "Specifies whether the commit date will be shown in the blame annotations. Applies only to the `expanded` annotation style" + "description": "Specifies whether the commit message will be shown in the blame annotations. Applies only to the `expanded` & `trailing` annotation styles" + }, + "gitlens.blame.annotation.activeLine.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether to show a trailing blame annotation (sha and commit message) of the active line" }, "gitlens.codeLens.visibility": { "type": "string", @@ -183,6 +199,11 @@ ], "description": "Specifies the command executed when the authors CodeLens is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `gitlens.showQuickFileHistory` - shows a file history picker" }, + "gitlens.menus.diff.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether diff commands will be added to the context menus" + }, "gitlens.statusBar.enabled": { "type": "boolean", "default": true, @@ -201,11 +222,6 @@ ], "description": "Specifies the command executed when the blame status bar item is clicked. `gitlens.toggleBlame` - toggles blame annotations. `gitlens.showBlameHistory` - opens the blame history explorer. `gitlens.showFileHistory` - opens the file history explorer. `gitlens.diffWithPrevious` - compares the current checked-in file with the previous commit. `gitlens.showQuickFileHistory` - shows a file history picker" }, - "gitlens.menus.diff.enabled": { - "type": "boolean", - "default": true, - "description": "Specifies whether diff commands will be added to the context menus" - }, "gitlens.advanced.caching.enabled": { "type": "boolean", "default": true, diff --git a/src/blameAnnotationFormatter.ts b/src/blameAnnotationFormatter.ts new file mode 100644 index 0000000..87d5258 --- /dev/null +++ b/src/blameAnnotationFormatter.ts @@ -0,0 +1,129 @@ +'use strict'; +import { IBlameConfig } from './configuration'; +import { GitCommit, IGitBlame, IGitCommitLine } from './gitProvider'; +import * as moment from 'moment'; + +export const defaultShaLength = 8; +export const defaultAbsoluteDateLength = 10; +export const defaultRelativeDateLength = 13; +export const defaultAuthorLength = 16; +export const defaultMessageLength = 32; + +export enum BlameAnnotationFormat { + Constrained, + Unconstrained +} + +export default class BlameAnnotationFormatter { + + static getAnnotation(config: IBlameConfig, commit: GitCommit, format: BlameAnnotationFormat) { + const sha = commit.sha.substring(0, defaultShaLength); + const message = this.getMessage(config, commit, format === BlameAnnotationFormat.Unconstrained ? 0 : defaultMessageLength); + + if (format === BlameAnnotationFormat.Unconstrained) { + const authorAndDate = this.getAuthorAndDate(config, commit, 'MMMM Do, YYYY h:MMa'); + if (config.annotation.sha) { + return `${sha}${(authorAndDate ? `\\00a0\\2022\\00a0 ${authorAndDate}` : '')}${(message ? `\\00a0\\2022\\00a0 ${message}` : '')}`; + } + + if (config.annotation.author || config.annotation.date) { + return `${authorAndDate}${(message ? `\\00a0\\2022\\00a0 ${message}` : '')}`; + } + + return message; + } + + const author = this.getAuthor(config, commit, defaultAuthorLength); + const date = this.getDate(config, commit, 'MM/DD/YYYY', true); + if (config.annotation.sha) { + return `${sha}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(message ? `\\00a0\\2022\\00a0 ${message}` : '')}`; + } + + if (config.annotation.author) { + return `${author}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(message ? `\\00a0\\2022\\00a0 ${message}` : '')}`; + } + + if (config.annotation.date) { + return `${date}${(message ? `\\00a0\\2022\\00a0 ${message}` : '')}`; + } + + return message; + } + + static getAnnotationHover(config: IBlameConfig, line: IGitCommitLine, commit: GitCommit, blame?: IGitBlame): string | Array { + if (commit.isUncommitted) { + let previous = blame && blame.commits.get(commit.previousSha); + if (previous) { + return [ + 'Uncommitted changes', + `_${previous.sha}_ - ${previous.message}`, + `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MMa')}` + ]; + } + + return [ + 'Uncommitted changes', + `_${line.previousSha}_` + ]; + } + + return [ + `_${commit.sha}_ - ${commit.message}`, + `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MMa')}` + ]; + } + + static getAuthorAndDate(config: IBlameConfig, commit: GitCommit, format?: string/*, truncate: boolean = false*/, force: boolean = false) { + if (!force && !config.annotation.author && (!config.annotation.date || config.annotation.date === 'off')) return ''; + + if (!config.annotation.author) { + return this.getDate(config, commit, format); //, truncate); + } + + if (!config.annotation.date || config.annotation.date === 'off') { + return this.getAuthor(config, commit); //, truncate ? defaultAuthorLength : 0); + } + + return `${this.getAuthor(config, commit)}, ${this.getDate(config, commit, format)}`; + } + + 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; + if (!truncateTo) return author; + + if (author.length > truncateTo) { + return `${author.substring(0, truncateTo - 1)}\\2026`; + } + + return author + '\\00a0'.repeat(truncateTo - author.length); + } + + static getDate(config: IBlameConfig, commit: GitCommit, format?: string, truncate: boolean = false, force: boolean = false) { + if (!force && (!config.annotation.date || config.annotation.date === 'off')) return ''; + + const date = config.annotation.date === 'relative' + ? moment(commit.date).fromNow() + : moment(commit.date).format(format); + if (!truncate) return date; + + const truncateTo = config.annotation.date === 'relative' ? defaultRelativeDateLength : defaultAbsoluteDateLength; + if (date.length > truncateTo) { + return `${date.substring(0, truncateTo - 1)}\\2026`; + } + + return date + '\\00a0'.repeat(truncateTo - date.length); + } + + static getMessage(config: IBlameConfig, commit: GitCommit, truncateTo: number = 0, force: boolean = false) { + if (!force && !config.annotation.message) return ''; + + let message = commit.message; + if (truncateTo && message.length > truncateTo) { + return `${message.substring(0, truncateTo - 1)}\\2026`; + } + + return message; + } +} \ No newline at end of file diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index cf8a700..7c7cb51 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -1,17 +1,20 @@ 'use strict'; import { Iterables } from './system'; -import { DecorationOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { DecorationInstanceRenderOptions, DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, OverviewRulerLane, Range, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import BlameAnnotationFormatter, { BlameAnnotationFormat, defaultShaLength, defaultAuthorLength } from './blameAnnotationFormatter'; import { TextDocumentComparer } from './comparers'; import { BlameAnnotationStyle, IBlameConfig } from './configuration'; -import GitProvider, { GitCommit, GitUri, IGitBlame } from './gitProvider'; +import GitProvider, { GitUri, IGitBlame } from './gitProvider'; import WhitespaceController from './whitespaceController'; -import * as moment from 'moment'; const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ before: { margin: '0 1.75em 0 0' + }, + after: { + margin: '0 0 0 4em' } -}); +} as DecorationRenderOptions); let highlightDecoration: TextEditorDecorationType; @@ -87,7 +90,9 @@ export class BlameAnnotationProvider extends Disposable { if (!blame || !blame.lines.length) return false; // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off) - this.whitespaceController && await this.whitespaceController.override(); + if (this._config.annotation.style !== BlameAnnotationStyle.Trailing) { + this.whitespaceController && await this.whitespaceController.override(); + } let blameDecorationOptions: DecorationOptions[] | undefined; switch (this._config.annotation.style) { @@ -95,7 +100,10 @@ export class BlameAnnotationProvider extends Disposable { blameDecorationOptions = this._getCompactGutterDecorations(blame); break; case BlameAnnotationStyle.Expanded: - blameDecorationOptions = this._getExpandedGutterDecorations(blame); + blameDecorationOptions = this._getExpandedGutterDecorations(blame, false); + break; + case BlameAnnotationStyle.Trailing: + blameDecorationOptions = this._getExpandedGutterDecorations(blame, true); break; } @@ -149,21 +157,17 @@ export class BlameAnnotationProvider extends Disposable { let count = 0; let lastSha: string; return blame.lines.map(l => { - let color = l.previousSha ? '#999999' : '#6b6b6b'; let commit = blame.commits.get(l.sha); - let hoverMessage: string | Array = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MMa')}`]; + let color: string; if (commit.isUncommitted) { color = 'rgba(0, 188, 242, 0.6)'; - - let previous = blame.commits.get(commit.previousSha); - if (previous) { - hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MMa')}`]; - } - else { - hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`]; - } } + else { + color = l.previousSha ? '#999999' : '#6b6b6b'; + } + + const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit, blame); let gutter = ''; if (lastSha !== l.sha) { @@ -174,13 +178,13 @@ export class BlameAnnotationProvider extends Disposable { if (!isEmptyOrWhitespace) { switch (++count) { case 0: - gutter = commit.sha.substring(0, 8); + gutter = commit.sha.substring(0, defaultShaLength); break; case 1: - gutter = `\\02759\\00a0 ${this._getAuthor(commit, 17, true)}`; + gutter = `\\02759\\00a0 ${BlameAnnotationFormatter.getAuthor(this._config, commit, defaultAuthorLength, true)}`; break; case 2: - gutter = `\\02759\\00a0 ${this._getDate(commit, true)}`; + gutter = `\\02759\\00a0 ${BlameAnnotationFormatter.getDate(this._config, commit, 'MM/DD/YYYY', true, true)}`; break; default: gutter = '\\02759'; @@ -188,94 +192,118 @@ export class BlameAnnotationProvider extends Disposable { } } + // Escape single quotes because for some reason that breaks the ::before or ::after element + gutter = gutter.replace(/\'/g, '\\\''); + lastSha = l.sha; return { - range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 0)), + range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)), hoverMessage: hoverMessage, - renderOptions: { before: { color: color, contentText: gutter, width: '11em' } } + renderOptions: { + before: { + color: color, + contentText: gutter, + width: '11em' + } + } } as DecorationOptions; }); } - private _getExpandedGutterDecorations(blame: IGitBlame): DecorationOptions[] { + private _getExpandedGutterDecorations(blame: IGitBlame, trailing: boolean = false): DecorationOptions[] { const offset = this._uri.offset; let width = 0; - if (this._config.annotation.sha) { - width += 5; - } - if (this._config.annotation.date) { - if (width > 0) { - width += 7; + if (!trailing) { + if (this._config.annotation.sha) { + width += 5; } - else { - width += 6; + if (this._config.annotation.date && this._config.annotation.date !== 'off') { + if (width > 0) { + width += 7; + } + else { + width += 6; + } + + if (this._config.annotation.date === 'relative') { + width += 2; + } } - } - if (this._config.annotation.author) { - if (width > 5 + 6) { - width += 12; + if (this._config.annotation.author) { + if (width > 5 + 6) { + width += 12; + } + else if (width > 0) { + width += 11; + } + else { + width += 10; + } } - else if (width > 0) { - width += 11; - } - else { - width += 10; + if (this._config.annotation.message) { + if (width > 5 + 6 + 10) { + width += 21; + } + else if (width > 5 + 6) { + width += 21; + } + else if (width > 0) { + width += 21; + } + else { + width += 19; + } } } return blame.lines.map(l => { - let color = l.previousSha ? '#999999' : '#6b6b6b'; let commit = blame.commits.get(l.sha); - let hoverMessage: string | Array = [`_${l.sha}_ - ${commit.message}`, `${commit.author}, ${moment(commit.date).format('MMMM Do, YYYY h:MMa')}`]; + let color: string; if (commit.isUncommitted) { color = 'rgba(0, 188, 242, 0.6)'; - - let previous = blame.commits.get(commit.previousSha); - if (previous) { - hoverMessage = ['Uncommitted changes', `_${previous.sha}_ - ${previous.message}`, `${previous.author}, ${moment(previous.date).format('MMMM Do, YYYY h:MMa')}`]; + } + else { + if (trailing) { + color = l.previousSha ? 'rgba(153, 153, 153, 0.5)' : 'rgba(107, 107, 107, 0.5)'; } else { - hoverMessage = ['Uncommitted changes', `_${l.previousSha}_`]; + color = l.previousSha ? 'rgb(153, 153, 153)' : 'rgb(107, 107, 107)'; } } - const gutter = this._getGutter(commit); + const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(this._config, l, commit, blame); + + const format = trailing ? BlameAnnotationFormat.Unconstrained : BlameAnnotationFormat.Constrained; + // Escape single quotes because for some reason that breaks the ::before or ::after element + const gutter = BlameAnnotationFormatter.getAnnotation(this._config, commit, format).replace(/\'/g, '\\\''); + + let renderOptions: DecorationInstanceRenderOptions; + if (trailing) { + renderOptions = { + after: { + color: color, + contentText: gutter + } + } as DecorationInstanceRenderOptions; + } + else { + renderOptions = { + before: { + color: color, + contentText: gutter, + width: `${width}em` + } + } as DecorationInstanceRenderOptions; + } + return { - range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 0)), + range: this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000)), hoverMessage: hoverMessage, - renderOptions: { before: { color: color, contentText: gutter, width: `${width}em` } } + renderOptions: renderOptions } as DecorationOptions; }); } - - private _getAuthor(commit: GitCommit, max: number = 17, force: boolean = false) { - if (!force && !this._config.annotation.author) return ''; - let author = commit.isUncommitted ? 'Uncommitted' : commit.author; - if (author.length > max) { - return `${author.substring(0, max - 1)}\\2026`; - } - return author; - } - - private _getDate(commit: GitCommit, force?: boolean) { - if (!force && !this._config.annotation.date) return ''; - return moment(commit.date).format('MM/DD/YYYY'); - } - - private _getGutter(commit: GitCommit) { - const author = this._getAuthor(commit); - const date = this._getDate(commit); - if (this._config.annotation.sha) { - return `${commit.sha.substring(0, 8)}${(date ? `\\00a0\\2022\\00a0 ${date}` : '')}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; - } - else if (this._config.annotation.date) { - return `${date}${(author ? `\\00a0\\2022\\00a0 ${author}` : '')}`; - } - else { - return author; - } - } } \ No newline at end of file diff --git a/src/blameStatusBarController.ts b/src/blameStatusBarController.ts index 8747cb6..8f10465 100644 --- a/src/blameStatusBarController.ts +++ b/src/blameStatusBarController.ts @@ -1,20 +1,28 @@ 'use strict'; import { Objects } from './system'; -import { Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; -import { TextDocumentComparer } from './comparers'; -import { IConfig, StatusBarCommand } from './configuration'; -import GitProvider, { GitCommit, GitUri, IGitBlame } from './gitProvider'; +import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditorDecorationType, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import BlameAnnotationFormatter, { BlameAnnotationFormat } from './blameAnnotationFormatter'; +import { TextEditorComparer } from './comparers'; +import { IBlameConfig, IConfig, StatusBarCommand } from './configuration'; +import { DocumentSchemes } from './constants'; +import GitProvider, { GitCommit, GitUri, IGitBlame, IGitCommitLine } from './gitProvider'; import { Logger } from './logger'; import * as moment from 'moment'; +const activeLineDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ + after: { + margin: '0 0 0 4em' + } +} as DecorationRenderOptions); + export default class BlameStatusBarController extends Disposable { + private _activeEditorLineDisposable: Disposable | undefined; private _blame: Promise | undefined; private _config: IConfig; private _disposable: Disposable; - private _document: TextDocument | undefined; + private _editor: TextEditor | undefined; private _statusBarItem: StatusBarItem | undefined; - private _statusBarDisposable: Disposable | undefined; private _uri: GitUri; private _useCaching: boolean; @@ -31,7 +39,9 @@ export default class BlameStatusBarController extends Disposable { } dispose() { - this._statusBarDisposable && this._statusBarDisposable.dispose(); + this._editor && this._editor.setDecorations(activeLineDecoration, []); + + this._activeEditorLineDisposable && this._activeEditorLineDisposable.dispose(); this._statusBarItem && this._statusBarItem.dispose(); this._disposable && this._disposable.dispose(); } @@ -39,12 +49,12 @@ export default class BlameStatusBarController extends Disposable { private _onConfigure() { const config = workspace.getConfiguration('').get('gitlens'); - if (!Objects.areEquivalent(config.statusBar, this._config && this._config.statusBar)) { - this._statusBarDisposable && this._statusBarDisposable.dispose(); - this._statusBarItem && this._statusBarItem.dispose(); + let changed: boolean = false; + if (!Objects.areEquivalent(config.statusBar, this._config && this._config.statusBar)) { + changed = true; if (config.statusBar.enabled) { - this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 1000); + this._statusBarItem = this._statusBarItem || window.createStatusBarItem(StatusBarAlignment.Right, 1000); switch (config.statusBar.command) { case StatusBarCommand.ToggleCodeLens: if (config.codeLens.visibility !== 'ondemand') { @@ -53,35 +63,59 @@ export default class BlameStatusBarController extends Disposable { break; } this._statusBarItem.command = config.statusBar.command; - - const subscriptions: Disposable[] = []; - - subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); - subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); - - this._statusBarDisposable = Disposable.from(...subscriptions); } - else { - this._statusBarDisposable = undefined; + else if (!config.statusBar.enabled && this._statusBarItem) { + this._statusBarItem.dispose(); this._statusBarItem = undefined; } } + if (!Objects.areEquivalent(config.blame.annotation.activeLine, this._config && this._config.blame.annotation.activeLine)) { + changed = true; + if (!config.blame.annotation.activeLine.enabled && this._editor) { + this._editor.setDecorations(activeLineDecoration, []); + } + } + this._config = config; + if (!changed) return; + + let trackActiveLine = config.statusBar.enabled || config.blame.annotation.activeLine.enabled; + if (trackActiveLine && !this._activeEditorLineDisposable) { + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); + subscriptions.push(window.onDidChangeTextEditorSelection(this._onActiveSelectionChanged, this)); + + this._activeEditorLineDisposable = Disposable.from(...subscriptions); + } + else if (!trackActiveLine && this._activeEditorLineDisposable) { + this._activeEditorLineDisposable.dispose(); + this._activeEditorLineDisposable = undefined; + } + this._onActiveTextEditorChanged(window.activeTextEditor); } private async _onActiveTextEditorChanged(e: TextEditor): Promise { - if (!e || !e.document || e.document.isUntitled || (e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) { - this.clear(); + const previousEditor = this._editor; + previousEditor && previousEditor.setDecorations(activeLineDecoration, []); + + if (!e || !e.document || e.document.isUntitled || + (e.document.uri.scheme !== DocumentSchemes.File && e.document.uri.scheme !== DocumentSchemes.Git) || + (e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) { + this.clear(e); + + this._editor = undefined; + return; } - this._document = e.document; - this._uri = GitUri.fromUri(this._document.uri, this.git); + this._editor = e; + this._uri = GitUri.fromUri(e.document.uri, this.git); const maxLines = this._config.advanced.caching.statusBar.maxLines; - this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || this._document.lineCount <= maxLines); + this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || e.document.lineCount <= maxLines); if (this._useCaching) { this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath); } @@ -89,29 +123,31 @@ export default class BlameStatusBarController extends Disposable { this._blame = undefined; } - return this._showBlame(e.selection.active.line); + return await this._showBlame(e.selection.active.line, e); } private async _onActiveSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { - if (!TextDocumentComparer.equals(this._document, e.textEditor && e.textEditor.document)) return; + if (!TextEditorComparer.equals(e.textEditor, this._editor)) return; - return this._showBlame(e.selections[0].active.line); + return await this._showBlame(e.selections[0].active.line, e.textEditor); } - private async _showBlame(line: number) { + private async _showBlame(line: number, editor: TextEditor) { line = line - this._uri.offset; + let commitLine: IGitCommitLine; let commit: GitCommit; if (line >= 0) { if (this._useCaching) { - const blame = await this._blame; + const blame = this._blame && await this._blame; if (!blame || !blame.lines.length) { - this.clear(); + this.clear(editor); return; } try { - const sha = blame.lines[line].sha; + commitLine = blame.lines[line]; + const sha = commitLine.sha; commit = blame.commits.get(sha); } catch (ex) { @@ -121,48 +157,81 @@ export default class BlameStatusBarController extends Disposable { } else { const blameLine = await this.git.getBlameForLine(this._uri.fsPath, line, this._uri.sha, this._uri.repoPath); + commitLine = blameLine && blameLine.line; commit = blameLine && blameLine.commit; } } if (commit) { - this.show(commit); + this.show(commit, commitLine, editor); } else { - this.clear(); + this.clear(editor); } } - clear() { + clear(editor: TextEditor, previousEditor?: TextEditor) { + editor && editor.setDecorations(activeLineDecoration, []); + this._statusBarItem && this._statusBarItem.hide(); - this._document = undefined; - this._blame = undefined; } - show(commit: GitCommit) { - this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; + show(commit: GitCommit, blameLine: IGitCommitLine, editor: TextEditor) { + if (this._config.statusBar.enabled) { + this._statusBarItem.text = `$(git-commit) ${commit.author}, ${moment(commit.date).fromNow()}`; - switch (this._config.statusBar.command) { - case StatusBarCommand.BlameAnnotate: - this._statusBarItem.tooltip = 'Toggle Blame Annotations'; - break; - case StatusBarCommand.ShowBlameHistory: - this._statusBarItem.tooltip = 'Open Blame History'; - break; - case StatusBarCommand.ShowFileHistory: - this._statusBarItem.tooltip = 'Open File History'; - break; - case StatusBarCommand.DiffWithPrevious: - this._statusBarItem.tooltip = 'Compare to Previous Commit'; - break; - case StatusBarCommand.ToggleCodeLens: - this._statusBarItem.tooltip = 'Toggle Blame CodeLens'; - break; - case StatusBarCommand.ShowQuickFileHistory: - this._statusBarItem.tooltip = 'View Git File History'; - break; + switch (this._config.statusBar.command) { + case StatusBarCommand.BlameAnnotate: + this._statusBarItem.tooltip = 'Toggle Blame Annotations'; + break; + case StatusBarCommand.ShowBlameHistory: + this._statusBarItem.tooltip = 'Open Blame History'; + break; + case StatusBarCommand.ShowFileHistory: + this._statusBarItem.tooltip = 'Open File History'; + break; + case StatusBarCommand.DiffWithPrevious: + this._statusBarItem.tooltip = 'Compare to Previous Commit'; + break; + case StatusBarCommand.ToggleCodeLens: + this._statusBarItem.tooltip = 'Toggle Blame CodeLens'; + break; + case StatusBarCommand.ShowQuickFileHistory: + this._statusBarItem.tooltip = 'View Git File History'; + break; + } + + this._statusBarItem.show(); } - this._statusBarItem.show(); + if (this._config.blame.annotation.activeLine.enabled) { + const offset = this._uri.offset; + + const config = { + annotation: { + sha: true, + author: this._config.statusBar.enabled ? false : this._config.blame.annotation.author, + date: this._config.statusBar.enabled ? 'off' : this._config.blame.annotation.date, + message: true + } + } as IBlameConfig; + + // Escape single quotes because for some reason that breaks the ::before or ::after element + const annotation = BlameAnnotationFormatter.getAnnotation(config, commit, BlameAnnotationFormat.Unconstrained).replace(/\'/g, '\\\''); + const hoverMessage = BlameAnnotationFormatter.getAnnotationHover(config, blameLine, commit); + + const decorationOptions = { + range: editor.document.validateRange(new Range(blameLine.line + offset, 1000000, blameLine.line + offset, 1000000)), + hoverMessage: hoverMessage, + renderOptions: { + after: { + color: 'rgba(153, 153, 153, 0.3)', + contentText: annotation + } + } as DecorationInstanceRenderOptions + } as DecorationOptions; + + editor.setDecorations(activeLineDecoration, [decorationOptions]); + } } } \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index 4430fbb..040e4f9 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,10 +1,11 @@ 'use strict'; import { Commands } from './constants'; -export type BlameAnnotationStyle = 'compact' | 'expanded'; +export type BlameAnnotationStyle = 'compact' | 'expanded' | 'trailing'; export const BlameAnnotationStyle = { Compact: 'compact' as BlameAnnotationStyle, - Expanded: 'expanded' as BlameAnnotationStyle + Expanded: 'expanded' as BlameAnnotationStyle, + Trailing: 'trailing' as BlameAnnotationStyle }; export interface IBlameConfig { @@ -12,7 +13,11 @@ export interface IBlameConfig { style: BlameAnnotationStyle; sha: boolean; author: boolean; - date: boolean; + date: 'off' | 'relative' | 'absolute'; + message: boolean; + activeLine: { + enabled: boolean; + }; }; }