瀏覽代碼

Revamps heatmap - median relative age & customizable cold/hot colors

Closes #419 - Make blame heatmap color configurable
main
Eric Amodio 6 年之前
父節點
當前提交
44566b4ec9
共有 12 個檔案被更改,包括 686 行新增223 行删除
  1. +6
    -0
      CHANGELOG.md
  2. +8
    -4
      README.md
  3. 二進制
      images/ss-heatmap.png
  4. +18
    -0
      package.json
  5. +44
    -18
      src/annotations/annotations.ts
  6. +64
    -1
      src/annotations/blameAnnotationProvider.ts
  7. +2
    -1
      src/annotations/fileAnnotationController.ts
  8. +7
    -3
      src/annotations/gutterBlameAnnotationProvider.ts
  9. +4
    -3
      src/annotations/heatmapBlameAnnotationProvider.ts
  10. +3
    -0
      src/ui/config.ts
  11. 二進制
      src/ui/images/settings/heatmap.png
  12. +530
    -193
      src/ui/settings/index.html

+ 6
- 0
CHANGELOG.md 查看文件

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]
### Added
- Adds completely revamped **heatmap** annotations
- 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)
- 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
- 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`)
- 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
- 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
- Adds new branch history node under the **Repository Status** node in the *GitLens* explorer
- 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))

+ 8
- 4
README.md 查看文件

@ -342,8 +342,8 @@ An on-demand, [customizable](#gitlens-results-explorer-settings "Jump to the Git
- 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
- Contains the commit message and date, by [default](#gutter-blame-settings "Jump to the Gutter Blame settings")
- 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)
- Indicator ranges from bright yellow (newer) to dark brown (older)
- 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)
- See the [gutter heatmap](#gutter-Heatmap "Jump to the Gutter Heatmap") section below for more details
- Adds a *Toggle File Blame Annotations* command (`gitlens.toggleFileBlame`) with a shortcut of `alt+b` to toggle the blame annotations on and off
- Press `Escape` to turn off the annotations
@ -353,8 +353,9 @@ An on-demand, [customizable](#gitlens-results-explorer-settings "Jump to the Git
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/ss-heatmap.png" alt="Gutter Heatmap" />
</p>
- Adds an on-demand **heatmap** to the edge of the gutter to show the relative age of a line
- Indicator ranges from bright yellow (newer) to dark brown (older)
- Adds an on-demand **heatmap** to the edge of the gutter to show how recently lines were changed
- 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"))
- 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
- Adds *Toggle File Heatmap Annotations* command (`gitlens.toggleFileHeatmap`) to toggle the heatmap on and off
- Press `Escape` to turn off the annotations
@ -717,6 +718,9 @@ See also [Explorer Settings](#explorer-settings "Jump to the Explorer settings")
|Name | Description
|-----|------------
|`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`)
|`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
|`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
|`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
### Hover Settings

二進制
images/ss-heatmap.png 查看文件

Before After
Width: 610  |  Height: 216  |  Size: 14 KiB Width: 610  |  Height: 216  |  Size: 14 KiB

+ 18
- 0
package.json 查看文件

@ -500,6 +500,24 @@
"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",
"scope": "window"
},
"gitlens.heatmap.ageThreshold": {
"type": "string",
"default": "90",
"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`)",
"scope": "window"
},
"gitlens.heatmap.coldColor": {
"type": "string",
"default": "#0a60f6",
"description": "Specifies the base color of the gutter heatmap annotations when the most recent change is older (cold) than the `gitlens.heatmap.ageThreshold` setting",
"scope": "window"
},
"gitlens.heatmap.hotColor": {
"type": "string",
"default": "#f66a0a",
"description": "Specifies the base color of the gutter heatmap annotations when the most recent change is newer (hot) than the `gitlens.heatmap.ageThreshold` setting",
"scope": "window"
},
"gitlens.heatmap.toggleMode": {
"type": "string",
"default": "file",

+ 44
- 18
src/annotations/annotations.ts 查看文件

@ -1,10 +1,20 @@
import { Dates, Objects, Strings } from '../system';
import { Objects, Strings } from '../system';
import { DecorationInstanceRenderOptions, DecorationOptions, MarkdownString, ThemableDecorationRenderOptions, ThemeColor } from 'vscode';
import { DiffWithCommand, OpenCommitInRemoteCommand, OpenFileRevisionCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand } from '../commands';
import { FileAnnotationType } from './../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatter, GitCommit, GitDiffChunkLine, GitRemote, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import { toRgba } from '../ui/shared/colors';
export interface ComputedHeatmap {
cold: boolean;
colors: { hot: string, cold: string };
median: number;
newest: number;
oldest: number;
computeAge(date: Date): number;
}
interface IHeatmapConfig {
enabled: boolean;
@ -16,29 +26,45 @@ interface IRenderOptions extends DecorationInstanceRenderOptions, ThemableDecora
uncommittedColor?: string | ThemeColor;
}
const defaultHeatmapHotColor = '#f66a0a';
const defaultHeatmapColdColor = '#0a60f6';
const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g;
// 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___';
let computedHeatmapColor: {
color: string,
rgb: string
};
export class Annotations {
static applyHeatmap(decoration: DecorationOptions, date: Date, now: number) {
const color = this.getHeatmapColor(now, date);
static applyHeatmap(decoration: DecorationOptions, date: Date, heatmap: ComputedHeatmap) {
const color = this.getHeatmapColor(date, heatmap);
(decoration.renderOptions!.before! as any).borderColor = color;
}
private static getHeatmapColor(now: number, date: Date) {
const days = Dates.dateDaysFromNow(date, now);
if (days <= 2) return '#ffeca7';
if (days <= 7) return '#ffdd8c';
if (days <= 14) return '#ffdd7c';
if (days <= 30) return '#fba447';
if (days <= 60) return '#f68736';
if (days <= 90) return '#f37636';
if (days <= 180) return '#ca6632';
if (days <= 365) return '#c0513f';
if (days <= 730) return '#a2503a';
return '#793738';
private static getHeatmapColor(date: Date, heatmap: ComputedHeatmap) {
const baseColor = heatmap.cold
? heatmap.colors.cold
: heatmap.colors.hot;
const age = heatmap.computeAge(date);
if (age === 0) return baseColor;
if (computedHeatmapColor === undefined || computedHeatmapColor.color !== baseColor) {
let rgba = toRgba(baseColor);
if (rgba == null) {
rgba = toRgba(heatmap.cold ? defaultHeatmapColdColor : defaultHeatmapHotColor)!;
}
const [r, g, b] = rgba;
computedHeatmapColor = {
color: baseColor,
rgb: `${r}, ${g}, ${b}`
};
}
return `rgba(${computedHeatmapColor.rgb}, ${(1 - (age / 10)).toFixed(2)})`;
}
private static getHoverCommandBar(commit: GitCommit, hasRemote: boolean, annotationType?: FileAnnotationType, line: number = 0) {
@ -213,14 +239,14 @@ export class Annotations {
} as IRenderOptions;
}
static heatmap(commit: GitCommit, now: number, renderOptions: IRenderOptions): DecorationOptions {
static heatmap(commit: GitCommit, heatmap: ComputedHeatmap, renderOptions: IRenderOptions): DecorationOptions {
const decoration = {
renderOptions: {
before: { ...renderOptions }
} as DecorationInstanceRenderOptions
} as DecorationOptions;
Annotations.applyHeatmap(decoration, commit.date, now);
Annotations.applyHeatmap(decoration, commit.date, heatmap);
return decoration;
}

+ 64
- 1
src/annotations/blameAnnotationProvider.ts 查看文件

@ -2,7 +2,7 @@
import { Arrays, Iterables } from '../system';
import { CancellationToken, Disposable, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { Annotations } from './annotations';
import { Annotations, ComputedHeatmap } from './annotations';
import { Container } from '../container';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
import { GitBlame, GitCommit, GitUri } from '../gitService';
@ -91,6 +91,69 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
return blame;
}
protected getComputedHeatmap(blame: GitBlame): ComputedHeatmap {
const dates = [];
let commit;
let previousSha;
for (const l of blame.lines) {
if (previousSha === l.sha) continue;
previousSha = l.sha;
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
dates.push(commit.date);
}
dates.sort((a, b) => a.getTime() - b.getTime());
const half = Math.floor(dates.length / 2);
const median = dates.length % 2
? dates[half].getTime()
: (dates[half - 1].getTime() + dates[half].getTime()) / 2.0;
const lookup: number[] = [];
const newest = dates[dates.length - 1].getTime();
let step = (newest - median) / 5;
for (let i = 5; i > 0; i--) {
lookup.push(median + (step * i));
}
lookup.push(median);
const oldest = dates[0].getTime();
step = (median - oldest) / 4;
for (let i = 1; i <= 4; i++) {
lookup.push(median - (step * i));
}
const d = new Date();
d.setDate(d.getDate() - (Container.config.heatmap.ageThreshold || 90));
return {
cold: newest < d.getTime(),
colors: {
cold: Container.config.heatmap.coldColor,
hot: Container.config.heatmap.hotColor
},
median: median,
newest: newest,
oldest: oldest,
computeAge: (date: Date) => {
const time = date.getTime();
let index = 0;
for (let i = 0; i < lookup.length; i++) {
index = i;
if (time >= lookup[i]) break;
}
return index;
}
};
}
registerHoverProviders(providers: { details: boolean, changes: boolean }) {
if (!Container.config.hovers.enabled || !Container.config.hovers.annotations.enabled || (!providers.details && !providers.changes)) return;

+ 2
- 1
src/annotations/fileAnnotationController.ts 查看文件

@ -161,6 +161,7 @@ export class FileAnnotationController extends Disposable {
if (configuration.changed(e, configuration.name('blame').value) ||
configuration.changed(e, configuration.name('recentChanges').value) ||
configuration.changed(e, configuration.name('heatmap').value) ||
configuration.changed(e, configuration.name('hovers').value)) {
// Since the configuration has changed -- reset any visible annotations
for (const provider of this._annotationProviders.values()) {
@ -416,7 +417,7 @@ export class FileAnnotationController extends Disposable {
this._keyboardScope = undefined;
}
private async showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string}>): Promise<AnnotationProviderBase | undefined> {
private async showAnnotationsCore(currentProvider: AnnotationProviderBase | undefined, editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number, progress?: Progress<{ message: string }>): Promise<AnnotationProviderBase | undefined> {
if (progress !== undefined) {
let annotationsLabel = 'annotations';
switch (type) {

+ 7
- 3
src/annotations/gutterBlameAnnotationProvider.ts 查看文件

@ -33,7 +33,6 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
tokenOptions: tokenOptions
};
const now = Date.now();
const avatars = cfg.avatars;
const gravatarDefault = Container.config.defaultGravatarsStyle;
const separateLines = cfg.separateLines;
@ -48,6 +47,11 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
let gutter: DecorationOptions | undefined;
let previousSha: string | undefined;
let computedHeatmap;
if (cfg.heatmap.enabled) {
computedHeatmap = this.getComputedHeatmap(blame);
}
for (const l of blame.lines) {
const line = l.line;
@ -106,8 +110,8 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
gutter = Annotations.gutter(commit, cfg.format, options, renderOptions);
if (cfg.heatmap.enabled) {
Annotations.applyHeatmap(gutter, commit.date, now);
if (computedHeatmap !== undefined) {
Annotations.applyHeatmap(gutter, commit.date, computedHeatmap);
}
gutter.range = new Range(line, 0, line, 0);

+ 4
- 3
src/annotations/heatmapBlameAnnotationProvider.ts 查看文件

@ -17,7 +17,6 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
const start = process.hrtime();
const now = Date.now();
const renderOptions = Annotations.heatmapRenderOptions();
this.decorations = [];
@ -26,6 +25,8 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
let commit: GitBlameCommit | undefined;
let heatmap: DecorationOptions | undefined;
const computedHeatmap = this.getComputedHeatmap(blame);
for (const l of blame.lines) {
const line = l.line;
@ -44,7 +45,7 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
commit = blame.commits.get(l.sha);
if (commit === undefined) continue;
heatmap = Annotations.heatmap(commit, now, renderOptions);
heatmap = Annotations.heatmap(commit, computedHeatmap, renderOptions);
heatmap.range = new Range(line, 0, line, 0);
this.decorations.push(heatmap);
@ -62,4 +63,4 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
this.selection(shaOrLine, blame);
return true;
}
}
}

+ 3
- 0
src/ui/config.ts 查看文件

@ -299,6 +299,9 @@ export interface IConfig {
gitExplorer: IGitExplorerConfig;
heatmap: {
ageThreshold: number;
coldColor: string;
hotColor: string;
toggleMode: AnnotationsToggleMode;
};

二進制
src/ui/images/settings/heatmap.png 查看文件

Before After
Width: 600  |  Height: 206  |  Size: 10 KiB Width: 600  |  Height: 206  |  Size: 10 KiB

+ 530
- 193
src/ui/settings/index.html
文件差異過大導致無法顯示
查看文件


Loading…
取消
儲存