Skip to content

Commit 44566b4

Browse files
committed
Revamps heatmap - median relative age & customizable cold/hot colors
Closes #419 - Make blame heatmap color configurable
1 parent 522788f commit 44566b4

File tree

12 files changed

+686
-223
lines changed

12 files changed

+686
-223
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88
### Added
9+
- Adds completely revamped **heatmap** annotations
10+
- The indicator's, now customizable, color will either be hot or cold based on the age of the most recent change (cold after 90 days by default) — closes [#419](https://github.com/eamodio/vscode-gitlens/issues/419)
11+
- The indicator's brightness ranges from bright (newer) to dim (older) based on the relative age, which is calculated from the median age of all the changes in the file
12+
- Adds `gitlens.heatmap.ageThreshold` setting to specify the age of the most recent change (in days) after which the gutter heatmap annotations will be cold rather than hot (i.e. will use `gitlens.heatmap.coldColor` instead of `gitlens.heatmap.hotColor`)
13+
- Adds `gitlens.heatmap.coldColor` setting to specify the base color of the gutter heatmap annotations when the most recent change is older (cold) than the `gitlens.heatmap.ageThreshold` setting
14+
- Adds `gitlens.heatmap.hotColor` setting to specify the base color of the gutter heatmap annotations when the most recent change is newer (hot) than the `gitlens.heatmap.ageThreshold` setting
915
- Adds new branch history node under the **Repository Status** node in the *GitLens* explorer
1016
- Adds GitLab and Visual Studio Team Services icons to the remote nodes in the *GitLens* explorer — thanks to [PR #421](https://github.com/eamodio/vscode-gitlens/pull/421) by Maxim Pekurin ([@pmaxim25](https://github.com/pmaxim25))
1117

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ An on-demand, [customizable](#gitlens-results-explorer-settings "Jump to the Git
342342

343343
- Adds on-demand, [customizable](#gutter-blame-settings "Jump to the Gutter Blame settings"), and [themable](#themable-colors "Jump to the Themable Colors"), **gutter blame annotations** for the whole file
344344
- Contains the commit message and date, by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")
345-
- Adds a **heatmap** (age) indicator on right edge (by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")) of the gutter to provide an easy, at-a-glance way to tell the age of a line ([optional](#gutter-blame-settings "Jump to the Gutter Blame settings"), on by default)
346-
- Indicator ranges from bright yellow (newer) to dark brown (older)
345+
- Adds a **heatmap** (age) indicator on right edge (by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")) of the gutter to provide an easy, at-a-glance way to tell how recently lines were changed ([optional](#gutter-blame-settings "Jump to the Gutter Blame settings"), on by default)
346+
- See the [gutter heatmap](#gutter-Heatmap "Jump to the Gutter Heatmap") section below for more details
347347
- Adds a *Toggle File Blame Annotations* command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the blame annotations on and off
348348
- Press `Escape` to turn off the annotations
349349

@@ -353,8 +353,9 @@ An on-demand, [customizable](#gitlens-results-explorer-settings "Jump to the Git
353353
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/ss-heatmap.png" alt="Gutter Heatmap" />
354354
</p>
355355

356-
- Adds an on-demand **heatmap** to the edge of the gutter to show the relative age of a line
357-
- Indicator ranges from bright yellow (newer) to dark brown (older)
356+
- Adds an on-demand **heatmap** to the edge of the gutter to show how recently lines were changed
357+
- The indicator's [customizable](#gutter-heatmap-settings "Jump to the Gutter Heatmap settings") color will either be hot or cold based on the age of the most recent change (cold after 90 days by [default](#gutter-heatmap-settings "Jump to the Gutter Heatmap settings"))
358+
- The indicator's brightness ranges from bright (newer) to dim (older) based on the relative age, which is calculated from the median age of all the changes in the file
358359
- Adds *Toggle File Heatmap Annotations* command (`gitlens.toggleFileHeatmap`) to toggle the heatmap on and off
359360
- Press `Escape` to turn off the annotations
360361

@@ -717,6 +718,9 @@ See also [Explorer Settings](#explorer-settings "Jump to the Explorer settings")
717718

718719
|Name | Description
719720
|-----|------------
721+
|`gitlens.heatmap.ageThreshold`|Specifies the age of the most recent change (in days) after which the gutter heatmap annotations will be cold rather than hot (i.e. will use `gitlens.heatmap.coldColor` instead of `gitlens.heatmap.hotColor`)
722+
|`gitlens.heatmap.coldColor`|Specifies the base color of the gutter heatmap annotations when the most recent change is older (cold) than the `gitlens.heatmap.ageThreshold` setting
723+
|`gitlens.heatmap.hotColor`|Specifies the base color of the gutter heatmap annotations when the most recent change is newer (hot) than the `gitlens.heatmap.ageThreshold` setting
720724
|`gitlens.heatmap.toggleMode`|Specifies how the gutter heatmap annotations will be toggled<br />`file` - toggle each file individually<br />`window` - toggle the window, i.e. all files at once
721725

722726
### Hover Settings

images/ss-heatmap.png

-10 Bytes
Loading

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,24 @@
500500
"description": "Specifies the starting view of the `GitLens` explorer\n `auto` - shows the last selected view, defaults to `repository`\n `history` - shows the commit history of the current file\n `repository` - shows a repository explorer",
501501
"scope": "window"
502502
},
503+
"gitlens.heatmap.ageThreshold": {
504+
"type": "string",
505+
"default": "90",
506+
"description": "Specifies the age of the most recent change (in days) after which the gutter heatmap annotations will be cold rather than hot (i.e. will use `gitlens.heatmap.coldColor` instead of `gitlens.heatmap.hotColor`)",
507+
"scope": "window"
508+
},
509+
"gitlens.heatmap.coldColor": {
510+
"type": "string",
511+
"default": "#0a60f6",
512+
"description": "Specifies the base color of the gutter heatmap annotations when the most recent change is older (cold) than the `gitlens.heatmap.ageThreshold` setting",
513+
"scope": "window"
514+
},
515+
"gitlens.heatmap.hotColor": {
516+
"type": "string",
517+
"default": "#f66a0a",
518+
"description": "Specifies the base color of the gutter heatmap annotations when the most recent change is newer (hot) than the `gitlens.heatmap.ageThreshold` setting",
519+
"scope": "window"
520+
},
503521
"gitlens.heatmap.toggleMode": {
504522
"type": "string",
505523
"default": "file",

src/annotations/annotations.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import { Dates, Objects, Strings } from '../system';
1+
import { Objects, Strings } from '../system';
22
import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions, ThemeColor } from 'vscode';
33
import { DiffWithCommand, OpenCommitInRemoteCommand, OpenFileRevisionCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand } from '../commands';
44
import { FileAnnotationType } from './../configuration';
55
import { GlyphChars } from '../constants';
66
import { Container } from '../container';
77
import { CommitFormatter, GitCommit, GitDiffChunkLine, GitRemote, GitService, GitUri, ICommitFormatOptions } from '../gitService';
8+
import { toRgba } from '../ui/shared/colors';
9+
10+
export interface ComputedHeatmap {
11+
cold: boolean;
12+
colors: { hot: string, cold: string };
13+
median: number;
14+
newest: number;
15+
oldest: number;
16+
computeAge(date: Date): number;
17+
}
818

919
interface IHeatmapConfig {
1020
enabled: boolean;
@@ -16,29 +26,45 @@ interface IRenderOptions extends DecorationInstanceRenderOptions, ThemableDecora
1626
uncommittedColor?: string | ThemeColor;
1727
}
1828

29+
const defaultHeatmapHotColor = '#f66a0a';
30+
const defaultHeatmapColdColor = '#0a60f6';
1931
const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g;
2032
// const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___';
2133

34+
let computedHeatmapColor: {
35+
color: string,
36+
rgb: string
37+
};
38+
2239
export class Annotations {
2340

24-
static applyHeatmap(decoration: DecorationOptions, date: Date, now: number) {
25-
const color = this.getHeatmapColor(now, date);
41+
static applyHeatmap(decoration: DecorationOptions, date: Date, heatmap: ComputedHeatmap) {
42+
const color = this.getHeatmapColor(date, heatmap);
2643
(decoration.renderOptions!.before! as any).borderColor = color;
2744
}
2845

29-
private static getHeatmapColor(now: number, date: Date) {
30-
const days = Dates.dateDaysFromNow(date, now);
31-
32-
if (days <= 2) return '#ffeca7';
33-
if (days <= 7) return '#ffdd8c';
34-
if (days <= 14) return '#ffdd7c';
35-
if (days <= 30) return '#fba447';
36-
if (days <= 60) return '#f68736';
37-
if (days <= 90) return '#f37636';
38-
if (days <= 180) return '#ca6632';
39-
if (days <= 365) return '#c0513f';
40-
if (days <= 730) return '#a2503a';
41-
return '#793738';
46+
private static getHeatmapColor(date: Date, heatmap: ComputedHeatmap) {
47+
const baseColor = heatmap.cold
48+
? heatmap.colors.cold
49+
: heatmap.colors.hot;
50+
51+
const age = heatmap.computeAge(date);
52+
if (age === 0) return baseColor;
53+
54+
if (computedHeatmapColor === undefined || computedHeatmapColor.color !== baseColor) {
55+
let rgba = toRgba(baseColor);
56+
if (rgba == null) {
57+
rgba = toRgba(heatmap.cold ? defaultHeatmapColdColor : defaultHeatmapHotColor)!;
58+
}
59+
60+
const [r, g, b] = rgba;
61+
computedHeatmapColor = {
62+
color: baseColor,
63+
rgb: `${r}, ${g}, ${b}`
64+
};
65+
}
66+
67+
return `rgba(${computedHeatmapColor.rgb}, ${(1 - (age / 10)).toFixed(2)})`;
4268
}
4369

4470
private static getHoverCommandBar(commit: GitCommit, hasRemote: boolean, annotationType?: FileAnnotationType, line: number = 0) {
@@ -213,14 +239,14 @@ export class Annotations {
213239
} as IRenderOptions;
214240
}
215241

216-
static heatmap(commit: GitCommit, now: number, renderOptions: IRenderOptions): DecorationOptions {
242+
static heatmap(commit: GitCommit, heatmap: ComputedHeatmap, renderOptions: IRenderOptions): DecorationOptions {
217243
const decoration = {
218244
renderOptions: {
219245
before: { ...renderOptions }
220246
} as DecorationInstanceRenderOptions
221247
} as DecorationOptions;
222248

223-
Annotations.applyHeatmap(decoration, commit.date, now);
249+
Annotations.applyHeatmap(decoration, commit.date, heatmap);
224250

225251
return decoration;
226252
}

src/annotations/blameAnnotationProvider.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Arrays, Iterables } from '../system';
33
import { CancellationToken, Disposable, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode';
44
import { AnnotationProviderBase } from './annotationProvider';
5-
import { Annotations } from './annotations';
5+
import { Annotations, ComputedHeatmap } from './annotations';
66
import { Container } from '../container';
77
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
88
import { GitBlame, GitCommit, GitUri } from '../gitService';
@@ -91,6 +91,69 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
9191
return blame;
9292
}
9393

94+
protected getComputedHeatmap(blame: GitBlame): ComputedHeatmap {
95+
const dates = [];
96+
97+
let commit;
98+
let previousSha;
99+
for (const l of blame.lines) {
100+
if (previousSha === l.sha) continue;
101+
previousSha = l.sha;
102+
103+
commit = blame.commits.get(l.sha);
104+
if (commit === undefined) continue;
105+
106+
dates.push(commit.date);
107+
}
108+
109+
dates.sort((a, b) => a.getTime() - b.getTime());
110+
111+
const half = Math.floor(dates.length / 2);
112+
const median = dates.length % 2
113+
? dates[half].getTime()
114+
: (dates[half - 1].getTime() + dates[half].getTime()) / 2.0;
115+
116+
const lookup: number[] = [];
117+
118+
const newest = dates[dates.length - 1].getTime();
119+
let step = (newest - median) / 5;
120+
for (let i = 5; i > 0; i--) {
121+
lookup.push(median + (step * i));
122+
}
123+
124+
lookup.push(median);
125+
126+
const oldest = dates[0].getTime();
127+
step = (median - oldest) / 4;
128+
for (let i = 1; i <= 4; i++) {
129+
lookup.push(median - (step * i));
130+
}
131+
132+
const d = new Date();
133+
d.setDate(d.getDate() - (Container.config.heatmap.ageThreshold || 90));
134+
135+
return {
136+
cold: newest < d.getTime(),
137+
colors: {
138+
cold: Container.config.heatmap.coldColor,
139+
hot: Container.config.heatmap.hotColor
140+
},
141+
median: median,
142+
newest: newest,
143+
oldest: oldest,
144+
computeAge: (date: Date) => {
145+
const time = date.getTime();
146+
let index = 0;
147+
for (let i = 0; i < lookup.length; i++) {
148+
index = i;
149+
if (time >= lookup[i]) break;
150+
}
151+
152+
return index;
153+
}
154+
};
155+
}
156+
94157
registerHoverProviders(providers: { details: boolean, changes: boolean }) {
95158
if (!Container.config.hovers.enabled || !Container.config.hovers.annotations.enabled || (!providers.details && !providers.changes)) return;
96159

src/annotations/fileAnnotationController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export class FileAnnotationController extends Disposable {
161161

162162
if (configuration.changed(e, configuration.name('blame').value) ||
163163
configuration.changed(e, configuration.name('recentChanges').value) ||
164+
configuration.changed(e, configuration.name('heatmap').value) ||
164165
configuration.changed(e, configuration.name('hovers').value)) {
165166
// Since the configuration has changed -- reset any visible annotations
166167
for (const provider of this._annotationProviders.values()) {
@@ -416,7 +417,7 @@ export class FileAnnotationController extends Disposable {
416417
this._keyboardScope = undefined;
417418
}
418419

419-
private async showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string}>): Promise<AnnotationProviderBase | undefined> {
420+
private async showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string }>): Promise<AnnotationProviderBase | undefined> {
420421
if (progress !== undefined) {
421422
let annotationsLabel = 'annotations';
422423
switch (type) {

src/annotations/gutterBlameAnnotationProvider.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
3333
tokenOptions: tokenOptions
3434
};
3535

36-
const now = Date.now();
3736
const avatars = cfg.avatars;
3837
const gravatarDefault = Container.config.defaultGravatarsStyle;
3938
const separateLines = cfg.separateLines;
@@ -48,6 +47,11 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
4847
let gutter: DecorationOptions | undefined;
4948
let previousSha: string | undefined;
5049

50+
let computedHeatmap;
51+
if (cfg.heatmap.enabled) {
52+
computedHeatmap = this.getComputedHeatmap(blame);
53+
}
54+
5155
for (const l of blame.lines) {
5256
const line = l.line;
5357

@@ -106,8 +110,8 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
106110

107111
gutter = Annotations.gutter(commit, cfg.format, options, renderOptions);
108112

109-
if (cfg.heatmap.enabled) {
110-
Annotations.applyHeatmap(gutter, commit.date, now);
113+
if (computedHeatmap !== undefined) {
114+
Annotations.applyHeatmap(gutter, commit.date, computedHeatmap);
111115
}
112116

113117
gutter.range = new Range(line, 0, line, 0);

src/annotations/heatmapBlameAnnotationProvider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
1717

1818
const start = process.hrtime();
1919

20-
const now = Date.now();
2120
const renderOptions = Annotations.heatmapRenderOptions();
2221

2322
this.decorations = [];
@@ -26,6 +25,8 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
2625
let commit: GitBlameCommit | undefined;
2726
let heatmap: DecorationOptions | undefined;
2827

28+
const computedHeatmap = this.getComputedHeatmap(blame);
29+
2930
for (const l of blame.lines) {
3031
const line = l.line;
3132

@@ -44,7 +45,7 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
4445
commit = blame.commits.get(l.sha);
4546
if (commit === undefined) continue;
4647

47-
heatmap = Annotations.heatmap(commit, now, renderOptions);
48+
heatmap = Annotations.heatmap(commit, computedHeatmap, renderOptions);
4849
heatmap.range = new Range(line, 0, line, 0);
4950

5051
this.decorations.push(heatmap);
@@ -62,4 +63,4 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
6263
this.selection(shaOrLine, blame);
6364
return true;
6465
}
65-
}
66+
}

src/ui/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ export interface IConfig {
299299
gitExplorer: IGitExplorerConfig;
300300

301301
heatmap: {
302+
ageThreshold: number;
303+
coldColor: string;
304+
hotColor: string;
302305
toggleMode: AnnotationsToggleMode;
303306
};
304307

0 commit comments

Comments
 (0)