diff --git a/CHANGELOG.md b/CHANGELOG.md index 043d77e..b178cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] ### Added +- Adds on-demand **heatmap annotations** of the whole file -- closes [#182](https://github.com/eamodio/vscode-gitlens/issues/182) + - Displays a `heatmap` (age) indicator near the gutter, which provides an easy, at-a-glance way to tell the age of a line + - Indicator ranges from bright yellow (newer) to dark brown (older) + - Press `Escape` to quickly toggle the annotations off +- Adds `Toggle File Heatmap Annotations` command (`gitlens.toggleFileHeatmap`) to toggle the heatmap annotations on and off - Adds semi-persistent results for commit operations, via the `Show Commit Details` command (`gitlens.showQuickCommitDetails`) in the `GitLens Results` view -- closes [#237](https://github.com/eamodio/vscode-gitlens/issues/237) - Adds `Show in Results` option to the commit details quick pick menu to show the commit in the `GitLens Results` view - Adds `Compare with Index (HEAD)` command (`gitlens.explorers.compareWithHead`) to branch, remote branch, tag, and revision (commit) nodes in the `GitLens` view to compare the current selection with the current index (HEAD) in the `GitLens Results` view diff --git a/README.md b/README.md index b61198b..fdbd3cc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,15 @@ While GitLens is highly customizable and provides many [configuration settings]( - Adds a `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) to toggle the current line blame annotations on and off - Also adds a `Show Line Blame Annotations` command (`gitlens.showLineBlame`) +### Git Heatmap Annotations + +- Adds on-demand **heatmap annotations** of the whole file + - Displays a `heatmap` (age) indicator near the gutter, which provides an easy, at-a-glance way to tell the age of a line + - Indicator ranges from bright yellow (newer) to dark brown (older) + - Press `Escape` to quickly toggle the annotations off + +- Adds `Toggle File Heatmap Annotations` command (`gitlens.toggleFileHeatmap`) to toggle the heatmap annotations on and off + ### Git Recent Changes Annotations - Adds on-demand, [customizable](#file-recent-changes-annotation-settings) and [themable](#themable-colors), **recent changes annotations** of the whole file diff --git a/package.json b/package.json index 8e602b1..d28f373 100644 --- a/package.json +++ b/package.json @@ -1215,6 +1215,11 @@ } }, { + "command": "gitlens.toggleFileHeatmap", + "title": "Toggle File Heatmap Annotations", + "category": "GitLens" + }, + { "command": "gitlens.toggleFileRecentChanges", "title": "Toggle Recent File Changes Annotations", "category": "GitLens", @@ -1677,6 +1682,10 @@ "when": "false" }, { + "command": "gitlens.toggleFileHeatmap", + "when": "gitlens:activeIsBlameable" + }, + { "command": "gitlens.toggleFileRecentChanges", "when": "gitlens:activeIsTracked" }, diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts index a0c432e..baf1b46 100644 --- a/src/annotations/annotationController.ts +++ b/src/annotations/annotationController.ts @@ -7,6 +7,7 @@ import { configuration, IConfig, LineHighlightLocations } from '../configuration import { CommandContext, isTextEditor, setCommandContext } from '../constants'; import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; +import { HeatmapBlameAnnotationProvider } from './heatmapBlameAnnotationProvider'; import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard'; import { Logger } from '../logger'; @@ -15,6 +16,7 @@ import * as path from 'path'; export enum FileAnnotationType { Gutter = 'gutter', + Heatmap = 'heatmap', Hover = 'hover', RecentChanges = 'recentChanges' } @@ -343,6 +345,10 @@ export class AnnotationController extends Disposable { annotationsLabel = 'blame annotations'; break; + case FileAnnotationType.Heatmap: + annotationsLabel = 'heatmap annotations'; + break; + case FileAnnotationType.RecentChanges: annotationsLabel = 'recent changes annotations'; break; @@ -362,6 +368,10 @@ export class AnnotationController extends Disposable { provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); break; + case FileAnnotationType.Heatmap: + provider = new HeatmapBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, undefined, this.git, gitUri); + break; + case FileAnnotationType.Hover: provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); break; diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 6a26407..cfbd462 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -207,6 +207,28 @@ export class Annotations { } as IRenderOptions; } + static heatmap(commit: GitCommit, now: number, renderOptions: IRenderOptions): DecorationOptions { + const decoration = { + renderOptions: { + before: { ...renderOptions } + } as DecorationInstanceRenderOptions + } as DecorationOptions; + + Annotations.applyHeatmap(decoration, commit.date, now); + + return decoration; + } + + static heatmapRenderOptions(): IRenderOptions { + return { + borderStyle: 'solid', + borderWidth: '0 0 0 2px', + contentText: GlyphChars.ZeroWidthSpace, + height: '100%', + margin: '0 26px -1px 0' + } as IRenderOptions; + } + static hover(commit: GitCommit, renderOptions: IRenderOptions, now: number): DecorationOptions { const decoration = { renderOptions: { before: { ...renderOptions } } diff --git a/src/annotations/heatmapBlameAnnotationProvider.ts b/src/annotations/heatmapBlameAnnotationProvider.ts new file mode 100644 index 0000000..28879ce --- /dev/null +++ b/src/annotations/heatmapBlameAnnotationProvider.ts @@ -0,0 +1,63 @@ +'use strict'; +import { DecorationOptions, Range } from 'vscode'; +import { FileAnnotationType } from './annotationController'; +import { Annotations } from './annotations'; +import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; +import { GitBlameCommit } from '../gitService'; +import { Logger } from '../logger'; + +export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase { + + async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { + this.annotationType = FileAnnotationType.Heatmap; + + const blame = await this.getBlame(); + if (blame === undefined) return false; + + const start = process.hrtime(); + + const now = Date.now(); + const renderOptions = Annotations.heatmapRenderOptions(); + + this._decorations = []; + const decorationsMap: { [sha: string]: DecorationOptions | undefined } = Object.create(null); + + let commit: GitBlameCommit | undefined; + let heatmap: DecorationOptions | undefined; + + for (const l of blame.lines) { + const line = l.line; + + heatmap = decorationsMap[l.sha]; + if (heatmap !== undefined) { + heatmap = { + ...heatmap, + range: new Range(line, 0, line, 0) + } as DecorationOptions; + + this._decorations.push(heatmap); + + continue; + } + + commit = blame.commits.get(l.sha); + if (commit === undefined) continue; + + heatmap = Annotations.heatmap(commit, now, renderOptions); + heatmap.range = new Range(line, 0, line, 0); + + this._decorations.push(heatmap); + decorationsMap[l.sha] = heatmap; + } + + if (this._decorations.length) { + this.editor.setDecorations(this.decoration!, this._decorations); + } + + const duration = process.hrtime(start); + Logger.log(`${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms to compute heatmap annotations`); + + this.selection(shaOrLine, blame); + return true; + } +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index c6c8931..017b8b5 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -47,6 +47,7 @@ export * from './commands/stashDelete'; export * from './commands/stashSave'; export * from './commands/toggleCodeLens'; export * from './commands/toggleFileBlame'; +export * from './commands/toggleFileHeatmap'; export * from './commands/toggleFileRecentChanges'; export * from './commands/toggleLineBlame'; @@ -87,6 +88,7 @@ export function configureCommands( context.subscriptions.push(new Commands.ShowFileBlameCommand(annotationController)); context.subscriptions.push(new Commands.ShowLineBlameCommand(currentLineController)); context.subscriptions.push(new Commands.ToggleFileBlameCommand(annotationController)); + context.subscriptions.push(new Commands.ToggleFileHeatmapCommand(annotationController)); context.subscriptions.push(new Commands.ToggleFileRecentChangesCommand(annotationController)); context.subscriptions.push(new Commands.ToggleLineBlameCommand(currentLineController)); context.subscriptions.push(new Commands.ResetSuppressedWarningsCommand()); diff --git a/src/commands/common.ts b/src/commands/common.ts index 9a202d4..4d394f4 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -49,6 +49,7 @@ export enum Commands { StashSave = 'gitlens.stashSave', ToggleCodeLens = 'gitlens.toggleCodeLens', ToggleFileBlame = 'gitlens.toggleFileBlame', + ToggleFileHeatmap = 'gitlens.toggleFileHeatmap', ToggleFileRecentChanges = 'gitlens.toggleFileRecentChanges', ToggleLineBlame = 'gitlens.toggleLineBlame' } diff --git a/src/commands/toggleFileHeatmap.ts b/src/commands/toggleFileHeatmap.ts new file mode 100644 index 0000000..db7d2cc --- /dev/null +++ b/src/commands/toggleFileHeatmap.ts @@ -0,0 +1,35 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; +import { Commands, EditorCommand } from './common'; +import { UriComparer } from '../comparers'; +import { Logger } from '../logger'; + +export class ToggleFileHeatmapCommand extends EditorCommand { + + constructor( + private readonly annotationController: AnnotationController + ) { + super(Commands.ToggleFileHeatmap); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { + if (editor === undefined || editor.document.isDirty) return undefined; + + // Handle the case where we are focused on a non-editor editor (output, debug console) + if (uri !== undefined && !UriComparer.equals(uri, editor.document.uri)) { + const e = window.visibleTextEditors.find(e => UriComparer.equals(uri, e.document.uri)); + if (e !== undefined && !e.document.isDirty) { + editor = e; + } + } + + try { + return this.annotationController.toggleAnnotations(editor, FileAnnotationType.Heatmap); + } + catch (ex) { + Logger.error(ex, 'ToggleFileHeatmapCommand'); + return window.showErrorMessage(`Unable to toggle heatmap annotations. See output channel for more details`); + } + } +} \ No newline at end of file