Browse Source

Overhauls recent change annotations

Renamed to Gutter Changes
Provides local changes if any, or recent commit changes
Distinguishes between added, changed, and removed lines
Shows a hover for the full diff hunk
Shows diff even with unsaved changes
main
Eric Amodio 4 years ago
parent
commit
a953e113f1
30 changed files with 1199 additions and 837 deletions
  1. +19
    -19
      README.md
  2. +45
    -48
      package.json
  3. +37
    -69
      src/annotations/annotationProvider.ts
  4. +5
    -5
      src/annotations/annotations.ts
  5. +49
    -131
      src/annotations/blameAnnotationProvider.ts
  6. +98
    -93
      src/annotations/fileAnnotationController.ts
  7. +60
    -11
      src/annotations/gutterBlameAnnotationProvider.ts
  8. +283
    -0
      src/annotations/gutterChangesAnnotationProvider.ts
  9. +13
    -11
      src/annotations/gutterHeatmapBlameAnnotationProvider.ts
  10. +0
    -142
      src/annotations/recentChangesAnnotationProvider.ts
  11. +1
    -1
      src/commands/common.ts
  12. +6
    -6
      src/commands/toggleFileAnnotations.ts
  13. +19
    -16
      src/config.ts
  14. +5
    -5
      src/container.ts
  15. +5
    -0
      src/extension.ts
  16. +1
    -7
      src/git/formatters/commitFormatter.ts
  17. +76
    -10
      src/git/git.ts
  18. +125
    -17
      src/git/gitService.ts
  19. +21
    -5
      src/git/models/diff.ts
  20. +22
    -6
      src/git/parsers/diffParser.ts
  21. +43
    -0
      src/git/parsers/logParser.ts
  22. +96
    -52
      src/hovers/hovers.ts
  23. +6
    -5
      src/hovers/lineHoverController.ts
  24. +4
    -0
      src/views/nodes/viewNode.ts
  25. +2
    -2
      src/views/viewCommands.ts
  26. +2
    -2
      src/webviews/apps/settings/partials/blame.ejs
  27. +19
    -37
      src/webviews/apps/settings/partials/changes.ejs
  28. +8
    -8
      src/webviews/apps/settings/settings.ejs

+ 19
- 19
README.md View File

@ -55,8 +55,8 @@ Here are just some of the **features** that GitLens provides,
- a [**_Search Commits_ view**](#search-commits-view- 'Jump to the Search Commits view') to search and explore commit histories by message, author, files, id, etc
- a [**_Compare_ view**](#compare-view- 'Jump to the Compare view') to visualize comparisons between branches, tags, commits, and more
- on-demand [**gutter blame**](#gutter-blame- 'Jump to the Gutter Blame') annotations, including a heatmap, for the whole file
- on-demand [**gutter changes**](#gutter-changes- 'Jump to the Gutter Changes') annotations to highlight any local changes or lines changed by the most recent commit
- on-demand [**gutter heatmap**](#gutter-heatmap- 'Jump to the Gutter Heatmap') annotations to show how recently lines were changed, relative to all the other changes in the file and to now (hot vs. cold)
- on-demand [**recent changes**](#recent-changes- 'Jump to the Recent Changes') annotations to highlight lines changed by the most recent commit
- many [**powerful commands**](#navigate-and-explore- 'Jump to the Navigate and Explorer') for exploring commits and histories, comparing and navigating revisions, stash access, repository status, etc
- user-defined [**modes**](#modes- 'Jump to the Modes') for quickly toggling between sets of settings
- and so much [**more**](#and-more- 'Jump to More')
@ -458,28 +458,28 @@ The compare view provides the following features,
---
### Gutter Heatmap [#](#gutter-heatmap- 'Gutter Heatmap')
### Gutter Changes [#](#changes- 'Gutter Changes')
<p align="center">
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/docs/heatmap.png" alt="Gutter Heatmap" />
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/docs/changes.png" alt="Gutter Changes" />
</p>
- 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
- Adds an on-demand, [customizable](#gutter-changes-settings- 'Jump to the Gutter Changes settings') and [themable](#themable-colors- 'Jump to the Themable Colors'), **gutter changes annotation** to highlight any local changes or lines changed by the most recent commit
- Adds _Toggle File Changes Annotations_ command (`gitlens.toggleFileChanges`) to toggle the changes annotations on and off
- Press `Escape` to turn off the annotations
---
### Recent Changes [#](#recent-changes- 'Recent Changes')
### Gutter Heatmap [#](#gutter-heatmap- 'Gutter Heatmap')
<p align="center">
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/docs/recent-changes.png" alt="Recent Changes" />
<img src="https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/docs/heatmap.png" alt="Gutter Heatmap" />
</p>
- Adds an on-demand, [customizable](#recent-changes-settings- 'Jump to the Recent Changes settings') and [themable](#themable-colors- 'Jump to the Themable Colors'), **recent changes annotation** to highlight lines changed by the most recent commit
- Adds _Toggle Recent File Changes Annotations_ command (`gitlens.toggleFileRecentChanges`) to toggle the recent changes annotations on and off
- 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
---
@ -821,11 +821,18 @@ See also [View Settings](#view-settings- 'Jump to the View settings')
| `gitlens.blame.heatmap.enabled` | Specifies whether to provide a heatmap indicator in the gutter blame annotations |
| `gitlens.blame.heatmap.location` | Specifies where the heatmap indicators will be shown in the gutter blame annotations<br /><br />`left` - adds a heatmap indicator on the left edge of the gutter blame annotations<br />`right` - adds a heatmap indicator on the right edge of the gutter blame annotations |
| `gitlens.blame.highlight.enabled` | Specifies whether to highlight lines associated with the current line |
| `gitlens.blame.highlight.locations` | Specifies where the associated line highlights will be shown<br /><br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overview` - adds a decoration to the overview ruler (scroll bar) |
| `gitlens.blame.highlight.locations` | Specifies where the associated line highlights will be shown<br /><br />`gutter` - adds a gutter indicator<br />`line` - adds a full-line highlight background color<br />`overview` - adds a decoration to the overview ruler (scroll bar) |
| `gitlens.blame.ignoreWhitespace` | Specifies whether to ignore whitespace when comparing revisions during blame operations |
| `gitlens.blame.separateLines` | Specifies whether gutter blame annotations will have line separators |
| `gitlens.blame.toggleMode` | Specifies how the gutter blame annotations will be toggled<br /><br />`file` - toggles each file individually<br />`window` - toggles the window, i.e. all files at once |
### Gutter Changes Settings [#](#gutter-changes-settings- 'Gutter Changes Settings')
| Name | Description |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.changes.locations` | Specifies where the indicators of the gutter changes annotations will be shown<br /><br />`gutter` - adds a gutter indicator<br />`overview` - adds a decoration to the overview ruler (scroll bar) |
| `gitlens.changes.toggleMode` | Specifies how the gutter changes annotations will be toggled<br /><br />`file` - toggles each file individually<br />`window` - toggles the window, i.e. all files at once |
### Gutter Heatmap Settings [#](#gutter-heatmap-settings- 'Gutter Heatmap Settings')
| Name | Description |
@ -835,13 +842,6 @@ See also [View Settings](#view-settings- 'Jump to the View settings')
| `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` value |
| `gitlens.heatmap.toggleMode` | Specifies how the gutter heatmap annotations will be toggled<br /><br />`file` - toggles each file individually<br />`window` - toggles the window, i.e. all files at once |
### Recent Changes Settings [#](#recent-changes-settings- 'Recent Changes Settings')
| Name | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gitlens.recentChanges.highlight.locations` | Specifies where the highlights of the recently changed lines will be shown<br /><br />`gutter` - adds a gutter glyph<br />`line` - adds a full-line highlight background color<br />`overview` - adds a decoration to the overview ruler (scroll bar) |
| `gitlens.recentChanges.toggleMode` | Specifies how the recently changed lines annotations will be toggled<br /><br />`file` - toggles each file individually<br />`window` - toggles the window, i.e. all files at once |
### Git Commands Menu Settings [#](#git-commands-menu-settings- 'Git Commands Menu Settings')
| Name | Description |

+ 45
- 48
package.json View File

@ -166,7 +166,7 @@
"overview"
],
"enumDescriptions": [
"Adds a gutter glyph",
"Adds a gutter indicator",
"Adds a full-line highlight background color",
"Adds a decoration to the overview ruler (scroll bar)"
]
@ -203,6 +203,43 @@
"markdownDescription": "Specifies how the gutter blame annotations will be toggled",
"scope": "window"
},
"gitlens.changes.locations": {
"type": "array",
"default": [
"gutter",
"overview"
],
"items": {
"type": "string",
"enum": [
"gutter",
"overview"
],
"enumDescriptions": [
"Adds a gutter indicator",
"Adds a decoration to the overview ruler (scroll bar)"
]
},
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"markdownDescription": "Specifies where the indicators of the gutter changes annotations will be shown",
"scope": "window"
},
"gitlens.changes.toggleMode": {
"type": "string",
"default": "file",
"enum": [
"file",
"window"
],
"enumDescriptions": [
"Toggles each file individually",
"Toggles the window, i.e. all files at once"
],
"markdownDescription": "Specifies how the gutter changes annotations will be toggled",
"scope": "window"
},
"gitlens.codeLens.authors.command": {
"anyOf": [
{
@ -1140,13 +1177,13 @@
"type": "string",
"enum": [
"blame",
"heatmap",
"recentChanges"
"changes",
"heatmap"
],
"enumDescriptions": [
"Shows the gutter blame annotations",
"Shows the gutter heatmap annotations",
"Shows the recently changed lines annotations"
"Shows the gutter changes annotations",
"Shows the gutter heatmap annotations"
],
"description": "Specifies which (if any) file annotations will be shown when this user-defined mode is active"
},
@ -1212,46 +1249,6 @@
"markdownDescription": "Specifies how much (if any) output will be sent to the GitLens output channel",
"scope": "window"
},
"gitlens.recentChanges.highlight.locations": {
"type": "array",
"default": [
"gutter",
"line",
"overview"
],
"items": {
"type": "string",
"enum": [
"gutter",
"line",
"overview"
],
"enumDescriptions": [
"Adds a gutter glyph",
"Adds a full-line highlight background color",
"Adds a decoration to the overview ruler (scroll bar)"
]
},
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"markdownDescription": "Specifies where the highlights of the recently changed lines will be shown",
"scope": "window"
},
"gitlens.recentChanges.toggleMode": {
"type": "string",
"default": "file",
"enum": [
"file",
"window"
],
"enumDescriptions": [
"Toggles each file individually",
"Toggles the window, i.e. all files at once"
],
"markdownDescription": "Specifies how the recently changed lines annotations will be toggled",
"scope": "window"
},
"gitlens.remotes": {
"type": [
"array",
@ -2328,8 +2325,8 @@
}
},
{
"command": "gitlens.toggleFileRecentChanges",
"title": "Toggle Recent File Changes Annotations",
"command": "gitlens.toggleFileChanges",
"title": "Toggle File Changes Annotations",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-git.svg",
@ -3566,7 +3563,7 @@
"when": "gitlens:activeFileStatus =~ /blameable/"
},
{
"command": "gitlens.toggleFileRecentChanges",
"command": "gitlens.toggleFileChanges",
"when": "gitlens:activeFileStatus =~ /blameable/"
},
{

+ 37
- 69
src/annotations/annotationProvider.ts View File

@ -12,7 +12,7 @@ import {
} from 'vscode';
import { FileAnnotationType } from '../configuration';
import { CommandContext, setCommandContext } from '../constants';
import { Functions } from '../system';
import { Logger } from '../logger';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
export enum AnnotationStatus {
@ -32,15 +32,12 @@ export abstract class AnnotationProviderBase implements Disposable {
document: TextDocument;
status: AnnotationStatus | undefined;
protected decorations: DecorationOptions[] | undefined;
private decorations:
| { decorationType: TextEditorDecorationType; rangesOrOptions: Range[] | DecorationOptions[] }[]
| undefined;
protected disposable: Disposable;
constructor(
public editor: TextEditor,
protected readonly trackedDocument: TrackedDocument<GitDocumentState>,
protected decoration: TextEditorDecorationType | undefined,
protected highlightDecoration: TextEditorDecorationType | undefined,
) {
constructor(public editor: TextEditor, protected readonly trackedDocument: TrackedDocument<GitDocumentState>) {
this.correlationKey = AnnotationProviderBase.getCorrelationKey(this.editor);
this.document = this.editor.document;
@ -71,65 +68,19 @@ export abstract class AnnotationProviderBase implements Disposable {
return this.editor.document.uri;
}
protected additionalDecorations: { decoration: TextEditorDecorationType; ranges: Range[] }[] | undefined;
clear() {
this.status = undefined;
if (this.editor == null) return;
if (this.decoration != null) {
try {
this.editor.setDecorations(this.decoration, []);
} catch {}
}
if (this.additionalDecorations?.length) {
for (const d of this.additionalDecorations) {
if (this.decorations?.length) {
for (const d of this.decorations) {
try {
this.editor.setDecorations(d.decoration, []);
this.editor.setDecorations(d.decorationType, []);
} catch {}
}
this.additionalDecorations = undefined;
}
if (this.highlightDecoration != null) {
try {
this.editor.setDecorations(this.highlightDecoration, []);
} catch {}
}
}
private _resetDebounced:
| ((changes?: {
decoration: TextEditorDecorationType;
highlightDecoration: TextEditorDecorationType | undefined;
}) => void)
| undefined;
reset(changes?: {
decoration: TextEditorDecorationType;
highlightDecoration: TextEditorDecorationType | undefined;
}) {
if (this._resetDebounced == null) {
this._resetDebounced = Functions.debounce(this.onReset.bind(this), 250);
}
this._resetDebounced(changes);
}
async onReset(changes?: {
decoration: TextEditorDecorationType;
highlightDecoration: TextEditorDecorationType | undefined;
}) {
if (changes != null) {
this.clear();
this.decoration = changes.decoration;
this.highlightDecoration = changes.highlightDecoration;
this.decorations = undefined;
}
await this.provideAnnotation(this.editor == null ? undefined : this.editor.selection.active.line);
}
async restore(editor: TextEditor) {
@ -146,13 +97,9 @@ export abstract class AnnotationProviderBase implements Disposable {
this.correlationKey = AnnotationProviderBase.getCorrelationKey(editor);
this.document = editor.document;
if (this.decoration != null && this.decorations?.length) {
this.editor.setDecorations(this.decoration, this.decorations);
}
if (this.additionalDecorations?.length) {
for (const d of this.additionalDecorations) {
this.editor.setDecorations(d.decoration, d.ranges);
if (this.decorations?.length) {
for (const d of this.decorations) {
this.editor.setDecorations(d.decorationType, d.rangesOrOptions);
}
}
@ -164,16 +111,37 @@ export abstract class AnnotationProviderBase implements Disposable {
async provideAnnotation(shaOrLine?: string | number): Promise<boolean> {
this.status = AnnotationStatus.Computing;
if (await this.onProvideAnnotation(shaOrLine)) {
this.status = AnnotationStatus.Computed;
return true;
try {
if (await this.onProvideAnnotation(shaOrLine)) {
this.status = AnnotationStatus.Computed;
return true;
}
} catch (ex) {
Logger.error(ex);
}
this.status = undefined;
return false;
}
abstract onProvideAnnotation(shaOrLine?: string | number): Promise<boolean>;
protected abstract onProvideAnnotation(shaOrLine?: string | number): Promise<boolean>;
abstract selection(shaOrLine?: string | number): Promise<void>;
protected setDecorations(
decorations: { decorationType: TextEditorDecorationType; rangesOrOptions: Range[] | DecorationOptions[] }[],
) {
if (this.decorations?.length) {
this.clear();
}
this.decorations = decorations;
if (this.decorations?.length) {
for (const d of this.decorations) {
this.editor.setDecorations(d.decorationType, d.rangesOrOptions);
}
}
}
abstract validate(): Promise<boolean>;
}

+ 5
- 5
src/annotations/annotations.ts View File

@ -109,7 +109,7 @@ export class Annotations {
date: Date,
heatmap: ComputedHeatmap,
range: Range,
map: Map<string, { decoration: TextEditorDecorationType; ranges: Range[] }>,
map: Map<string, { decorationType: TextEditorDecorationType; rangesOrOptions: Range[] }>,
) {
const [r, g, b, a] = this.getHeatmapColor(date, heatmap);
@ -117,7 +117,7 @@ export class Annotations {
let colorDecoration = map.get(key);
if (colorDecoration == null) {
colorDecoration = {
decoration: window.createTextEditorDecorationType({
decorationType: window.createTextEditorDecorationType({
gutterIconPath: Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><rect fill='rgb(${r},${g},${b})' fill-opacity='${a}' x='7' y='0' width='2' height='18'/></svg>`,
@ -125,14 +125,14 @@ export class Annotations {
),
gutterIconSize: 'contain',
}),
ranges: [range],
rangesOrOptions: [range],
};
map.set(key, colorDecoration);
} else {
colorDecoration.ranges.push(range);
colorDecoration.rangesOrOptions.push(range);
}
return colorDecoration.decoration;
return colorDecoration.decorationType;
}
static gutter(

+ 49
- 131
src/annotations/blameAnnotationProvider.ts View File

@ -4,11 +4,11 @@ import {
Disposable,
Hover,
languages,
MarkdownString,
Position,
Range,
TextDocument,
TextEditor,
TextEditorDecorationType,
} from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { ComputedHeatmap, getHeatmapColors } from './annotations';
@ -16,26 +16,19 @@ import { Container } from '../container';
import { GitBlame, GitBlameCommit, GitCommit } from '../git/git';
import { GitUri } from '../git/gitUri';
import { Hovers } from '../hovers/hovers';
import { Arrays, Iterables, log } from '../system';
import { log } from '../system';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase {
protected _blame: Promise<GitBlame | undefined>;
protected _hoverProviderDisposable: Disposable | undefined;
protected readonly _uri: GitUri;
constructor(
editor: TextEditor,
trackedDocument: TrackedDocument<GitDocumentState>,
decoration: TextEditorDecorationType | undefined,
highlightDecoration: TextEditorDecorationType | undefined,
) {
super(editor, trackedDocument, decoration, highlightDecoration);
this._uri = trackedDocument.uri;
this._blame = editor.document.isDirty
? Container.git.getBlameForFileContents(this._uri, editor.document.getText())
: Container.git.getBlameForFile(this._uri);
protected blame: Promise<GitBlame | undefined>;
protected hoverProviderDisposable: Disposable | undefined;
constructor(editor: TextEditor, trackedDocument: TrackedDocument<GitDocumentState>) {
super(editor, trackedDocument);
this.blame = editor.document.isDirty
? Container.git.getBlameForFileContents(this.trackedDocument.uri, editor.document.getText())
: Container.git.getBlameForFile(this.trackedDocument.uri);
if (editor.document.isDirty) {
trackedDocument.setForceDirtyStateChangeOnNextDocumentChange();
@ -43,69 +36,20 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
}
clear() {
if (this._hoverProviderDisposable != null) {
this._hoverProviderDisposable.dispose();
this._hoverProviderDisposable = undefined;
if (this.hoverProviderDisposable != null) {
this.hoverProviderDisposable.dispose();
this.hoverProviderDisposable = undefined;
}
super.clear();
}
onReset(changes?: {
decoration: TextEditorDecorationType;
highlightDecoration: TextEditorDecorationType | undefined;
}) {
if (this.editor != null) {
this._blame = this.editor.document.isDirty
? Container.git.getBlameForFileContents(this._uri, this.editor.document.getText())
: Container.git.getBlameForFile(this._uri);
}
return super.onReset(changes);
}
@log({ args: false })
async selection(shaOrLine?: string | number, blame?: GitBlame) {
if (!this.highlightDecoration) return;
if (blame == null) {
blame = await this._blame;
if (!blame || !blame.lines.length) return;
}
let sha: string | undefined = undefined;
if (typeof shaOrLine === 'string') {
sha = shaOrLine;
} else if (typeof shaOrLine === 'number') {
if (shaOrLine >= 0) {
const commitLine = blame.lines[shaOrLine];
sha = commitLine?.sha;
}
} else {
sha = Iterables.first(blame.commits.values()).sha;
}
if (!sha) {
this.editor.setDecorations(this.highlightDecoration, []);
return;
}
const highlightDecorationRanges = Arrays.filterMap(blame.lines, l =>
l.sha === sha
? // editor lines are 0-based
this.editor.document.validateRange(new Range(l.line - 1, 0, l.line - 1, Number.MAX_SAFE_INTEGER))
: undefined,
);
this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges);
}
async validate(): Promise<boolean> {
const blame = await this._blame;
const blame = await this.blame;
return blame != null && blame.lines.length !== 0;
}
protected async getBlame(): Promise<GitBlame | undefined> {
const blame = await this._blame;
const blame = await this.blame;
if (blame == null || blame.lines.length === 0) return undefined;
return blame;
@ -190,39 +134,46 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
return;
}
const subscriptions: Disposable[] = [];
if (providers.changes) {
subscriptions.push(
languages.registerHoverProvider(
{ pattern: this.document.uri.fsPath },
{
provideHover: this.provideChangesHover.bind(this),
},
),
);
}
if (providers.details) {
subscriptions.push(
languages.registerHoverProvider(
{ pattern: this.document.uri.fsPath },
{
provideHover: this.provideDetailsHover.bind(this),
},
),
);
}
this._hoverProviderDisposable = Disposable.from(...subscriptions);
this.hoverProviderDisposable = languages.registerHoverProvider(
{ pattern: this.document.uri.fsPath },
{
provideHover: (document, position, token) => this.provideHover(providers, document, position, token),
},
);
}
async provideDetailsHover(
async provideHover(
providers: { details: boolean; changes: boolean },
document: TextDocument,
position: Position,
_token: CancellationToken,
): Promise<Hover | undefined> {
const commit = await this.getCommitForHover(position);
if (Container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined;
const blame = await this.getBlame();
if (blame == null) return undefined;
const line = blame.lines[position.line];
const commit = blame.commits.get(line.sha);
if (commit == null) return undefined;
const messages = (
await Promise.all([
providers.details ? this.getDetailsHoverMessage(commit, document) : undefined,
providers.changes
? Hovers.changesMessage(commit, await GitUri.fromUri(document.uri), position.line)
: undefined,
])
).filter(Boolean) as MarkdownString[];
return new Hover(
messages,
document.validateRange(new Range(position.line, 0, position.line, Number.MAX_SAFE_INTEGER)),
);
}
private async getDetailsHoverMessage(commit: GitBlameCommit, document: TextDocument) {
// Get the full commit message -- since blame only returns the summary
let logCommit: GitCommit | undefined = undefined;
if (!commit.isUncommitted) {
@ -241,46 +192,13 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
editorLine = commitLine.originalLine - 1;
const message = await Hovers.detailsMessage(
return Hovers.detailsMessage(
logCommit ?? commit,
await GitUri.fromUri(document.uri),
editorLine,
Container.config.defaultDateFormat,
this.annotationType,
);
return new Hover(
message,
document.validateRange(new Range(position.line, 0, position.line, Number.MAX_SAFE_INTEGER)),
);
}
async provideChangesHover(
document: TextDocument,
position: Position,
_token: CancellationToken,
): Promise<Hover | undefined> {
const commit = await this.getCommitForHover(position);
if (commit == null) return undefined;
const message = await Hovers.changesMessage(commit, await GitUri.fromUri(document.uri), position.line);
if (message == null) return undefined;
return new Hover(
message,
document.validateRange(new Range(position.line, 0, position.line, Number.MAX_SAFE_INTEGER)),
);
}
private async getCommitForHover(position: Position): Promise<GitBlameCommit | undefined> {
if (Container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined;
const blame = await this.getBlame();
if (blame == null) return undefined;
const line = blame.lines[position.line];
return blame.commits.get(line.sha);
}
}
function getRelativeAgeLookupTable(dates: Date[]) {

+ 98
- 93
src/annotations/fileAnnotationController.ts View File

@ -18,9 +18,19 @@ import {
window,
workspace,
} from 'vscode';
import { AnnotationsToggleMode, configuration, FileAnnotationType, HighlightLocations } from '../configuration';
import { AnnotationProviderBase, AnnotationStatus, TextEditorCorrelationKey } from './annotationProvider';
import {
AnnotationsToggleMode,
BlameHighlightLocations,
ChangesLocations,
configuration,
FileAnnotationType,
} from '../configuration';
import { CommandContext, isTextEditor, setCommandContext } from '../constants';
import { Container } from '../container';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { GutterChangesAnnotationProvider } from './gutterChangesAnnotationProvider';
import { GutterHeatmapBlameAnnotationProvider } from './gutterHeatmapBlameAnnotationProvider';
import { KeyboardScope } from '../keyboard';
import { Logger } from '../logger';
import { Functions, Iterables } from '../system';
@ -29,10 +39,6 @@ import {
DocumentDirtyStateChangeEvent,
GitDocumentState,
} from '../trackers/gitDocumentTracker';
import { AnnotationProviderBase, AnnotationStatus, TextEditorCorrelationKey } from './annotationProvider';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { HeatmapBlameAnnotationProvider } from './heatmapBlameAnnotationProvider';
import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider';
export enum AnnotationClearReason {
User = 'User',
@ -44,15 +50,14 @@ export enum AnnotationClearReason {
}
export const Decorations = {
blameAnnotation: window.createTextEditorDecorationType({
gutterBlameAnnotation: window.createTextEditorDecorationType({
rangeBehavior: DecorationRangeBehavior.ClosedOpen,
textDecoration: 'none',
}),
blameHighlight: undefined as TextEditorDecorationType | undefined,
heatmapAnnotation: undefined as TextEditorDecorationType | undefined,
heatmapHighlight: undefined as TextEditorDecorationType | undefined,
recentChangesAnnotation: undefined as TextEditorDecorationType | undefined,
recentChangesHighlight: undefined as TextEditorDecorationType | undefined,
gutterBlameHighlight: undefined as TextEditorDecorationType | undefined,
changesLineChangedAnnotation: undefined as TextEditorDecorationType | undefined,
changesLineAddedAnnotation: undefined as TextEditorDecorationType | undefined,
changesLineDeletedAnnotation: undefined as TextEditorDecorationType | undefined,
};
export class FileAnnotationController implements Disposable {
@ -79,8 +84,11 @@ export class FileAnnotationController implements Disposable {
dispose() {
void this.clearAll();
Decorations.blameAnnotation?.dispose();
Decorations.blameHighlight?.dispose();
Decorations.gutterBlameAnnotation?.dispose();
Decorations.gutterBlameHighlight?.dispose();
Decorations.changesLineChangedAnnotation?.dispose();
Decorations.changesLineAddedAnnotation?.dispose();
Decorations.changesLineDeletedAnnotation?.dispose();
this._annotationsDisposable?.dispose();
this._disposable?.dispose();
@ -90,15 +98,17 @@ export class FileAnnotationController implements Disposable {
const cfg = Container.config;
if (configuration.changed(e, 'blame', 'highlight')) {
Decorations.blameHighlight?.dispose();
Decorations.blameHighlight = undefined;
Decorations.gutterBlameHighlight?.dispose();
Decorations.gutterBlameHighlight = undefined;
const highlight = cfg.blame.highlight;
const cfgHighlight = cfg.blame.highlight;
if (highlight.enabled) {
const { locations } = highlight;
if (cfgHighlight.enabled) {
// TODO@eamodio: Read from the theme color when the API exists
const gutterHighlightColor = '#00bcf2'; // new ThemeColor('gitlens.lineHighlightOverviewRulerColor')
const gutterHighlightUri = cfgHighlight.locations.includes(HighlightLocations.Gutter)
const gutterHighlightUri = locations.includes(BlameHighlightLocations.Gutter)
? Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><rect fill='${gutterHighlightColor}' fill-opacity='0.6' x='7' y='0' width='3' height='18'/></svg>`,
@ -106,46 +116,70 @@ export class FileAnnotationController implements Disposable {
)
: undefined;
Decorations.blameHighlight = window.createTextEditorDecorationType({
Decorations.gutterBlameHighlight = window.createTextEditorDecorationType({
gutterIconPath: gutterHighlightUri,
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
backgroundColor: cfgHighlight.locations.includes(HighlightLocations.Line)
backgroundColor: locations.includes(BlameHighlightLocations.Line)
? new ThemeColor('gitlens.lineHighlightBackgroundColor')
: undefined,
overviewRulerColor: cfgHighlight.locations.includes(HighlightLocations.Overview)
overviewRulerColor: locations.includes(BlameHighlightLocations.Overview)
? new ThemeColor('gitlens.lineHighlightOverviewRulerColor')
: undefined,
});
}
}
if (configuration.changed(e, 'recentChanges', 'highlight')) {
Decorations.recentChangesAnnotation?.dispose();
if (configuration.changed(e, 'changes', 'locations')) {
Decorations.changesLineAddedAnnotation?.dispose();
Decorations.changesLineChangedAnnotation?.dispose();
Decorations.changesLineDeletedAnnotation?.dispose();
const cfgHighlight = cfg.recentChanges.highlight;
const { locations } = cfg.changes;
// TODO@eamodio: Read from the theme color when the API exists
const gutterHighlightColor = '#00bcf2'; // new ThemeColor('gitlens.lineHighlightOverviewRulerColor')
const gutterHighlightUri = cfgHighlight.locations.includes(HighlightLocations.Gutter)
? Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><rect fill='${gutterHighlightColor}' fill-opacity='0.6' x='7' y='0' width='3' height='18'/></svg>`,
)}`,
)
: undefined;
Decorations.changesLineAddedAnnotation = window.createTextEditorDecorationType({
gutterIconPath: locations.includes(ChangesLocations.Gutter)
? Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><rect fill='#587c0c' x='13' y='0' width='3' height='18'/></svg>",
)}`,
)
: undefined,
gutterIconSize: 'contain',
overviewRulerLane: OverviewRulerLane.Left,
overviewRulerColor: locations.includes(ChangesLocations.Overview)
? new ThemeColor('editorOverviewRuler.addedForeground')
: undefined,
});
Decorations.recentChangesAnnotation = window.createTextEditorDecorationType({
gutterIconPath: gutterHighlightUri,
Decorations.changesLineChangedAnnotation = window.createTextEditorDecorationType({
gutterIconPath: locations.includes(ChangesLocations.Gutter)
? Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><rect fill='#0c7d9d' x='13' y='0' width='3' height='18'/></svg>",
)}`,
)
: undefined,
gutterIconSize: 'contain',
isWholeLine: true,
overviewRulerLane: OverviewRulerLane.Right,
backgroundColor: cfgHighlight.locations.includes(HighlightLocations.Line)
? new ThemeColor('gitlens.lineHighlightBackgroundColor')
overviewRulerLane: OverviewRulerLane.Left,
overviewRulerColor: locations.includes(ChangesLocations.Overview)
? new ThemeColor('editorOverviewRuler.modifiedForeground')
: undefined,
overviewRulerColor: cfgHighlight.locations.includes(HighlightLocations.Overview)
? new ThemeColor('gitlens.lineHighlightOverviewRulerColor')
});
Decorations.changesLineDeletedAnnotation = window.createTextEditorDecorationType({
gutterIconPath: locations.includes(ChangesLocations.Gutter)
? Uri.parse(
`data:image/svg+xml,${encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><polygon fill='#94151b' points='13,10 13,18 17,14'/></svg>",
)}`,
)
: undefined,
gutterIconSize: 'contain',
overviewRulerLane: OverviewRulerLane.Left,
overviewRulerColor: locations.includes(ChangesLocations.Overview)
? new ThemeColor('editorOverviewRuler.deletedForeground')
: undefined,
});
}
@ -159,16 +193,16 @@ export class FileAnnotationController implements Disposable {
}
}
if (configuration.changed(e, 'heatmap', 'toggleMode')) {
this._toggleModes.set(FileAnnotationType.Heatmap, cfg.heatmap.toggleMode);
if (!initializing && cfg.heatmap.toggleMode === AnnotationsToggleMode.File) {
if (configuration.changed(e, 'changes', 'toggleMode')) {
this._toggleModes.set(FileAnnotationType.Changes, cfg.changes.toggleMode);
if (!initializing && cfg.changes.toggleMode === AnnotationsToggleMode.File) {
void this.clearAll();
}
}
if (configuration.changed(e, 'recentChanges', 'toggleMode')) {
this._toggleModes.set(FileAnnotationType.RecentChanges, cfg.recentChanges.toggleMode);
if (!initializing && cfg.recentChanges.toggleMode === AnnotationsToggleMode.File) {
if (configuration.changed(e, 'heatmap', 'toggleMode')) {
this._toggleModes.set(FileAnnotationType.Heatmap, cfg.heatmap.toggleMode);
if (!initializing && cfg.heatmap.toggleMode === AnnotationsToggleMode.File) {
void this.clearAll();
}
}
@ -177,7 +211,7 @@ export class FileAnnotationController implements Disposable {
if (
configuration.changed(e, 'blame') ||
configuration.changed(e, 'recentChanges') ||
configuration.changed(e, 'changes') ||
configuration.changed(e, 'heatmap') ||
configuration.changed(e, 'hovers')
) {
@ -185,19 +219,7 @@ export class FileAnnotationController implements Disposable {
for (const provider of this._annotationProviders.values()) {
if (provider == null) continue;
if (provider.annotationType === FileAnnotationType.RecentChanges) {
provider.reset({
decoration: Decorations.recentChangesAnnotation!,
highlightDecoration: Decorations.recentChangesHighlight,
});
} else if (provider.annotationType === FileAnnotationType.Blame) {
provider.reset({
decoration: Decorations.blameAnnotation,
highlightDecoration: Decorations.blameHighlight,
});
} else {
void this.show(provider.editor, FileAnnotationType.Heatmap);
}
void this.show(provider.editor, provider.annotationType ?? FileAnnotationType.Blame);
}
}
}
@ -330,7 +352,7 @@ export class FileAnnotationController implements Disposable {
let first = this._annotationType == null;
const reset =
(!first && this._annotationType !== type) ||
(this._annotationType === FileAnnotationType.RecentChanges && typeof shaOrLine === 'string');
(this._annotationType === FileAnnotationType.Changes && typeof shaOrLine === 'string');
this._annotationType = type;
@ -393,10 +415,7 @@ export class FileAnnotationController implements Disposable {
): Promise<boolean> {
if (editor != null) {
const trackedDocument = await Container.tracker.getOrAdd(editor.document);
if (
(type === FileAnnotationType.RecentChanges && !trackedDocument.isTracked) ||
!trackedDocument.isBlameable
) {
if ((type === FileAnnotationType.Changes && !trackedDocument.isTracked) || !trackedDocument.isBlameable) {
return false;
}
}
@ -405,8 +424,7 @@ export class FileAnnotationController implements Disposable {
if (provider == null) return this.show(editor, type, shaOrLine);
const reopen =
provider.annotationType !== type ||
(type === FileAnnotationType.RecentChanges && typeof shaOrLine === 'string');
provider.annotationType !== type || (type === FileAnnotationType.Changes && typeof shaOrLine === 'string');
if (on === true && !reopen) return true;
if (this.isInWindowToggle()) {
@ -482,12 +500,12 @@ export class FileAnnotationController implements Disposable {
annotationsLabel = 'blame annotations';
break;
case FileAnnotationType.Heatmap:
annotationsLabel = 'heatmap annotations';
case FileAnnotationType.Changes:
annotationsLabel = 'changes annotations';
break;
case FileAnnotationType.RecentChanges:
annotationsLabel = 'recent changes annotations';
case FileAnnotationType.Heatmap:
annotationsLabel = 'heatmap annotations';
break;
}
@ -504,30 +522,15 @@ export class FileAnnotationController implements Disposable {
let provider: AnnotationProviderBase | undefined = undefined;
switch (type) {
case FileAnnotationType.Blame:
provider = new GutterBlameAnnotationProvider(
editor,
trackedDocument,
Decorations.blameAnnotation,
Decorations.blameHighlight,
);
provider = new GutterBlameAnnotationProvider(editor, trackedDocument);
break;
case FileAnnotationType.Heatmap:
provider = new HeatmapBlameAnnotationProvider(
editor,
trackedDocument,
Decorations.heatmapAnnotation,
Decorations.heatmapHighlight,
);
case FileAnnotationType.Changes:
provider = new GutterChangesAnnotationProvider(editor, trackedDocument);
break;
case FileAnnotationType.RecentChanges:
provider = new RecentChangesAnnotationProvider(
editor,
trackedDocument,
Decorations.recentChangesAnnotation!,
Decorations.recentChangesHighlight,
);
case FileAnnotationType.Heatmap:
provider = new GutterHeatmapBlameAnnotationProvider(editor, trackedDocument);
break;
}
if (provider == null || !(await provider.validate())) return undefined;
@ -536,7 +539,7 @@ export class FileAnnotationController implements Disposable {
await this.clearCore(currentProvider.correlationKey, AnnotationClearReason.User);
}
if (!this._annotationsDisposable && this._annotationProviders.size === 0) {
if (this._annotationsDisposable == null && this._annotationProviders.size === 0) {
Logger.log('Add listener registrations for annotations');
this._annotationsDisposable = Disposable.from(
@ -555,6 +558,8 @@ export class FileAnnotationController implements Disposable {
return provider;
}
await this.clearCore(provider.correlationKey, AnnotationClearReason.Disposing);
return undefined;
}
}

+ 60
- 11
src/annotations/gutterBlameAnnotationProvider.ts View File

@ -1,15 +1,26 @@
'use strict';
import { DecorationOptions, Range, ThemableDecorationAttachmentRenderOptions } from 'vscode';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { FileAnnotationType, GravatarDefaultStyle } from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { CommitFormatOptions, CommitFormatter, GitBlameCommit } from '../git/git';
import { Decorations } from './fileAnnotationController';
import { CommitFormatOptions, CommitFormatter, GitBlame, GitBlameCommit } from '../git/git';
import { Logger } from '../logger';
import { log, Strings } from '../system';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { Arrays, Iterables, log, Strings } from '../system';
export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
clear() {
super.clear();
if (Decorations.gutterBlameHighlight != null) {
try {
this.editor.setDecorations(Decorations.gutterBlameHighlight, []);
} catch {}
}
}
@log()
async onProvideAnnotation(_shaOrLine?: string | number, _type?: FileAnnotationType): Promise<boolean> {
const cc = Logger.getCorrelationContext();
@ -53,7 +64,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
options,
);
this.decorations = [];
const decorationOptions = [];
const decorationsMap = new Map<string, DecorationOptions | undefined>();
const avatarDecorationsMap = avatars ? new Map<string, ThemableDecorationAttachmentRenderOptions>() : undefined;
@ -99,7 +110,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
gutter.range = new Range(editorLine, 0, editorLine, 0);
this.decorations.push(gutter);
decorationOptions.push(gutter);
continue;
}
@ -117,7 +128,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
range: new Range(editorLine, 0, editorLine, 0),
};
this.decorations.push(gutter);
decorationOptions.push(gutter);
continue;
}
@ -130,7 +141,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
gutter.range = new Range(editorLine, 0, editorLine, 0);
this.decorations.push(gutter);
decorationOptions.push(gutter);
if (avatars && commit.email != null) {
this.applyAvatarDecoration(commit, gutter, gravatarDefault, avatarDecorationsMap!);
@ -141,10 +152,12 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to compute gutter blame annotations`);
if (this.decoration != null && this.decorations.length) {
if (decorationOptions.length) {
start = process.hrtime();
this.editor.setDecorations(this.decoration, this.decorations);
this.setDecorations([
{ decorationType: Decorations.gutterBlameAnnotation, rangesOrOptions: decorationOptions },
]);
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to apply all gutter blame annotations`);
}
@ -153,7 +166,43 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
return true;
}
applyAvatarDecoration(
@log({ args: false })
async selection(shaOrLine?: string | number, blame?: GitBlame) {
if (Decorations.gutterBlameHighlight == null) return;
if (blame == null) {
blame = await this.blame;
if (!blame?.lines.length) return;
}
let sha: string | undefined = undefined;
if (typeof shaOrLine === 'string') {
sha = shaOrLine;
} else if (typeof shaOrLine === 'number') {
if (shaOrLine >= 0) {
const commitLine = blame.lines[shaOrLine];
sha = commitLine?.sha;
}
} else {
sha = Iterables.first(blame.commits.values()).sha;
}
if (!sha) {
this.editor.setDecorations(Decorations.gutterBlameHighlight, []);
return;
}
const highlightDecorationRanges = Arrays.filterMap(blame.lines, l =>
l.sha === sha
? // editor lines are 0-based
this.editor.document.validateRange(new Range(l.line - 1, 0, l.line - 1, Number.MAX_SAFE_INTEGER))
: undefined,
);
this.editor.setDecorations(Decorations.gutterBlameHighlight, highlightDecorationRanges);
}
private applyAvatarDecoration(
commit: GitBlameCommit,
gutter: DecorationOptions,
gravatarDefault: GravatarDefaultStyle,

+ 283
- 0
src/annotations/gutterChangesAnnotationProvider.ts View File

@ -0,0 +1,283 @@
'use strict';
import {
CancellationToken,
DecorationOptions,
Disposable,
Hover,
languages,
Position,
Range,
Selection,
TextDocument,
TextEditor,
TextEditorDecorationType,
TextEditorRevealType,
} from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { Decorations } from './fileAnnotationController';
import { GitDiff, GitLogCommit } from '../git/git';
import { Hovers } from '../hovers/hovers';
import { Logger } from '../logger';
import { log, Strings } from '../system';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
export class GutterChangesAnnotationProvider extends AnnotationProviderBase {
private state: { commit: GitLogCommit | undefined; diffs: GitDiff[] } | undefined;
private hoverProviderDisposable: Disposable | undefined;
constructor(editor: TextEditor, trackedDocument: TrackedDocument<GitDocumentState>) {
super(editor, trackedDocument);
}
clear() {
this.state = undefined;
if (this.hoverProviderDisposable != null) {
this.hoverProviderDisposable.dispose();
this.hoverProviderDisposable = undefined;
}
super.clear();
}
selection(_shaOrLine?: string | number): Promise<void> {
return Promise.resolve();
}
validate(): Promise<boolean> {
return Promise.resolve(true);
}
@log()
async onProvideAnnotation(shaOrLine?: string | number): Promise<boolean> {
const cc = Logger.getCorrelationContext();
this.annotationType = FileAnnotationType.Changes;
let ref1 = this.trackedDocument.uri.sha;
let ref2;
if (typeof shaOrLine === 'string') {
if (shaOrLine !== this.trackedDocument.uri.sha) {
ref2 = `${shaOrLine}^`;
}
}
let commit: GitLogCommit | undefined;
let localChanges = ref1 == null && ref2 == null;
if (localChanges) {
let ref = await Container.git.getOldestUnpushedRefForFile(
this.trackedDocument.uri.repoPath!,
this.trackedDocument.uri.fsPath,
);
if (ref != null) {
ref = `${ref}^`;
commit = await Container.git.getCommitForFile(
this.trackedDocument.uri.repoPath,
this.trackedDocument.uri.fsPath,
{ ref: ref },
);
if (commit != null) {
if (ref2 != null) {
ref2 = ref;
} else {
ref1 = ref;
ref2 = '';
}
} else {
localChanges = false;
}
} else {
const status = await Container.git.getStatusForFile(
this.trackedDocument.uri.repoPath!,
this.trackedDocument.uri.fsPath,
);
const commits = await status?.toPsuedoCommits();
if (commits?.length) {
commit = await Container.git.getCommitForFile(
this.trackedDocument.uri.repoPath,
this.trackedDocument.uri.fsPath,
);
ref1 = 'HEAD';
} else if (this.trackedDocument.dirty) {
ref1 = 'HEAD';
} else {
localChanges = false;
}
}
}
if (!localChanges) {
commit = await Container.git.getCommitForFile(
this.trackedDocument.uri.repoPath,
this.trackedDocument.uri.fsPath,
{
ref: ref2 ?? ref1,
},
);
if (commit == null) return false;
if (ref2 != null) {
ref2 = commit.ref;
} else {
ref1 = commit.ref;
ref2 = `${commit.ref}^`;
}
}
const diffs = (
await Promise.all(
ref2 == null && this.editor.document.isDirty
? [
Container.git.getDiffForFileContents(
this.trackedDocument.uri,
ref1!,
this.editor.document.getText(),
),
Container.git.getDiffForFile(this.trackedDocument.uri, ref1, ref2),
]
: [Container.git.getDiffForFile(this.trackedDocument.uri, ref1, ref2)],
)
).filter(Boolean) as GitDiff[];
if (!diffs?.length) return false;
let start = process.hrtime();
const decorationsMap = new Map<
string,
{ decorationType: TextEditorDecorationType; rangesOrOptions: DecorationOptions[] }
>();
let selection: Selection | undefined;
for (const diff of diffs) {
for (const hunk of diff.hunks) {
// Subtract 2 because editor lines are 0-based and we will be adding 1 in the first iteration of the loop
let count = Math.max(hunk.current.position.start - 2, -1);
let index = -1;
for (const hunkLine of hunk.lines) {
index++;
count++;
if (hunkLine.current?.state === 'unchanged') continue;
const range = this.editor.document.validateRange(
new Range(new Position(count, 0), new Position(count, Number.MAX_SAFE_INTEGER)),
);
if (selection == null) {
selection = new Selection(range.start, range.end);
}
let state;
if (hunkLine.current == null) {
const previous = hunk.lines[index - 1];
if (hunkLine.previous != null && (previous == null || previous.current != null)) {
// Check if there are more deleted lines than added lines show a deleted indicator
if (hunk.previous.count > hunk.current.count) {
state = 'removed';
} else {
continue;
}
} else {
continue;
}
} else if (hunkLine.current?.state === 'added') {
if (hunkLine.previous?.state === 'removed') {
state = 'changed';
} else {
state = 'added';
}
} else if (hunkLine?.current.state === 'removed') {
// Check if there are more deleted lines than added lines show a deleted indicator
if (hunk.previous.count > hunk.current.count) {
state = 'removed';
} else {
continue;
}
} else {
state = 'changed';
}
let decoration = decorationsMap.get(state);
if (decoration == null) {
decoration = {
decorationType: (state === 'added'
? Decorations.changesLineAddedAnnotation
: state === 'removed'
? Decorations.changesLineDeletedAnnotation
: Decorations.changesLineChangedAnnotation)!,
rangesOrOptions: [{ range: range }],
};
decorationsMap.set(state, decoration);
} else {
decoration.rangesOrOptions.push({ range: range });
}
}
}
}
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to compute recent changes annotations`);
if (decorationsMap.size) {
start = process.hrtime();
this.setDecorations([...decorationsMap.values()]);
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to apply recent changes annotations`);
if (selection != null) {
this.editor.selection = selection;
this.editor.revealRange(selection, TextEditorRevealType.InCenterIfOutsideViewport);
}
}
this.state = { commit: commit, diffs: diffs };
this.registerHoverProvider();
return true;
}
registerHoverProvider() {
if (!Container.config.hovers.enabled || !Container.config.hovers.annotations.enabled) {
return;
}
this.hoverProviderDisposable = languages.registerHoverProvider(
{ pattern: this.document.uri.fsPath },
{
provideHover: (document, position, token) => this.provideHover(document, position, token),
},
);
}
provideHover(document: TextDocument, position: Position, _token: CancellationToken): Hover | undefined {
if (this.state == null) return undefined;
if (Container.config.hovers.annotations.over !== 'line' && position.character !== 0) return undefined;
const { commit, diffs } = this.state;
for (const diff of diffs) {
for (const hunk of diff.hunks) {
// If we have a "mixed" diff hunk, check if we have more deleted lines than added, to include a trailing line for the deleted indicator
const hasMoreDeletedLines = hunk.state === 'changed' && hunk.previous.count > hunk.current.count;
if (
position.line >= hunk.current.position.start - 1 &&
position.line <= hunk.current.position.end - (hasMoreDeletedLines ? 0 : 1)
) {
return new Hover(
Hovers.localChangesMessage(commit, this.trackedDocument.uri, position.line, hunk),
document.validateRange(
new Range(
hunk.current.position.start - 1,
0,
hunk.current.position.end - (hasMoreDeletedLines ? 0 : 1),
Number.MAX_SAFE_INTEGER,
),
),
);
}
}
}
return undefined;
}
}

src/annotations/heatmapBlameAnnotationProvider.ts → src/annotations/gutterHeatmapBlameAnnotationProvider.ts View File

@ -1,14 +1,13 @@
'use strict';
import { Range, TextEditorDecorationType } from 'vscode';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
import { FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { GitBlameCommit } from '../git/git';
import { Logger } from '../logger';
import { log, Strings } from '../system';
import { Annotations } from './annotations';
import { BlameAnnotationProviderBase } from './blameAnnotationProvider';
export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase {
export class GutterHeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase {
@log()
async onProvideAnnotation(_shaOrLine?: string | number, _type?: FileAnnotationType): Promise<boolean> {
const cc = Logger.getCorrelationContext();
@ -20,7 +19,10 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
let start = process.hrtime();
const decorationsMap = new Map<string, { decoration: TextEditorDecorationType; ranges: Range[] }>();
const decorationsMap = new Map<
string,
{ decorationType: TextEditorDecorationType; rangesOrOptions: Range[] }
>();
const computedHeatmap = await this.getComputedHeatmap(blame);
let commit: GitBlameCommit | undefined;
@ -44,16 +46,16 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
if (decorationsMap.size) {
start = process.hrtime();
this.additionalDecorations = [];
for (const d of decorationsMap.values()) {
this.additionalDecorations.push(d);
this.editor.setDecorations(d.decoration, d.ranges);
}
this.setDecorations([...decorationsMap.values()]);
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to apply recent changes annotations`);
}
this.registerHoverProviders(Container.config.hovers.annotations);
// this.registerHoverProviders(Container.config.hovers.annotations);
return true;
}
selection(_shaOrLine?: string | number): Promise<void> {
return Promise.resolve();
}
}

+ 0
- 142
src/annotations/recentChangesAnnotationProvider.ts View File

@ -1,142 +0,0 @@
'use strict';
import {
MarkdownString,
Position,
Range,
Selection,
TextEditor,
TextEditorDecorationType,
TextEditorRevealType,
} from 'vscode';
import { AnnotationProviderBase } from './annotationProvider';
import { FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { GitUri } from '../git/gitUri';
import { Hovers } from '../hovers/hovers';
import { Logger } from '../logger';
import { log, Strings } from '../system';
import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker';
export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
private readonly _uri: GitUri;
constructor(
editor: TextEditor,
trackedDocument: TrackedDocument<GitDocumentState>,
decoration: TextEditorDecorationType,
highlightDecoration: TextEditorDecorationType | undefined,
) {
super(editor, trackedDocument, decoration, highlightDecoration);
this._uri = trackedDocument.uri;
}
@log()
async onProvideAnnotation(shaOrLine?: string | number): Promise<boolean> {
const cc = Logger.getCorrelationContext();
this.annotationType = FileAnnotationType.RecentChanges;
let ref1 = this._uri.sha;
let ref2;
if (typeof shaOrLine === 'string') {
if (shaOrLine !== this._uri.sha) {
ref2 = `${shaOrLine}^`;
}
}
const commit = await Container.git.getCommitForFile(this._uri.repoPath, this._uri.fsPath, {
ref: ref2 ?? ref1,
});
if (commit === undefined) return false;
if (ref2 !== undefined) {
ref2 = commit.ref;
} else {
ref1 = commit.ref;
}
const diff = await Container.git.getDiffForFile(this._uri, ref1, ref2);
if (diff === undefined) return false;
let start = process.hrtime();
const cfg = Container.config;
const dateFormat = cfg.defaultDateFormat;
this.decorations = [];
let selection: Selection | undefined;
for (const hunk of diff.hunks) {
// Subtract 2 because editor lines are 0-based and we will be adding 1 in the first iteration of the loop
let count = hunk.currentPosition.start - 2;
for (const hunkLine of hunk.lines) {
if (hunkLine.current === undefined) continue;
count++;
if (hunkLine.current.state === 'unchanged') continue;
const range = this.editor.document.validateRange(
new Range(new Position(count, 0), new Position(count, Number.MAX_SAFE_INTEGER)),
);
if (selection === undefined) {
selection = new Selection(range.start, range.end);
}
let message: MarkdownString | undefined = undefined;
if (cfg.hovers.enabled && cfg.hovers.annotations.enabled) {
if (cfg.hovers.annotations.details) {
this.decorations.push({
hoverMessage: await Hovers.detailsMessage(
commit,
await GitUri.fromUri(this.editor.document.uri),
count,
dateFormat,
this.annotationType,
),
range: range,
});
}
if (cfg.hovers.annotations.changes) {
message = await Hovers.changesMessage(commit, this._uri, count, hunkLine);
if (message === undefined) continue;
}
}
this.decorations.push({
hoverMessage: message,
range: range,
});
}
}
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to compute recent changes annotations`);
if (this.decoration != null && this.decorations.length) {
start = process.hrtime();
this.editor.setDecorations(this.decoration, this.decorations);
Logger.log(cc, `${Strings.getDurationMilliseconds(start)} ms to apply recent changes annotations`);
if (selection !== undefined) {
this.editor.selection = selection;
this.editor.revealRange(selection, TextEditorRevealType.InCenterIfOutsideViewport);
}
}
return true;
}
selection(_shaOrLine?: string | number): Promise<void> {
return Promise.resolve(undefined);
}
validate(): Promise<boolean> {
return Promise.resolve(true);
}
}

+ 1
- 1
src/commands/common.ts View File

@ -106,8 +106,8 @@ export enum Commands {
SwitchMode = 'gitlens.switchMode',
ToggleCodeLens = 'gitlens.toggleCodeLens',
ToggleFileBlame = 'gitlens.toggleFileBlame',
ToggleFileChanges = 'gitlens.toggleFileChanges',
ToggleFileHeatmap = 'gitlens.toggleFileHeatmap',
ToggleFileRecentChanges = 'gitlens.toggleFileRecentChanges',
ToggleLineBlame = 'gitlens.toggleLineBlame',
ToggleReviewMode = 'gitlens.toggleReviewMode',
ToggleZenMode = 'gitlens.toggleZenMode',

+ 6
- 6
src/commands/toggleFileAnnotations.ts View File

@ -55,29 +55,29 @@ export class ToggleFileBlameCommand extends ActiveEditorCommand {
}
@command()
export class ToggleFileHeatmapCommand extends ActiveEditorCommand {
export class ToggleFileChangesCommand extends ActiveEditorCommand {
constructor() {
super(Commands.ToggleFileHeatmap);
super(Commands.ToggleFileChanges);
}
execute(editor: TextEditor, uri?: Uri, args?: ToggleFileAnnotationCommandArgs): Promise<void> {
return toggleFileAnnotations(editor, uri, {
...args,
type: FileAnnotationType.Heatmap,
type: FileAnnotationType.Changes,
});
}
}
@command()
export class ToggleFileRecentChangesCommand extends ActiveEditorCommand {
export class ToggleFileHeatmapCommand extends ActiveEditorCommand {
constructor() {
super(Commands.ToggleFileRecentChanges);
super(Commands.ToggleFileHeatmap);
}
execute(editor: TextEditor, uri?: Uri, args?: ToggleFileAnnotationCommandArgs): Promise<void> {
return toggleFileAnnotations(editor, uri, {
...args,
type: FileAnnotationType.RecentChanges,
type: FileAnnotationType.Heatmap,
});
}
}

+ 19
- 16
src/config.ts View File

@ -14,12 +14,17 @@ export interface Config {
};
highlight: {
enabled: boolean;
locations: HighlightLocations[];
locations: BlameHighlightLocations[];
};
ignoreWhitespace: boolean;
separateLines: boolean;
toggleMode: AnnotationsToggleMode;
};
changes: {
locations: ChangesLocations[];
toggleMode: AnnotationsToggleMode;
};
codeLens: CodeLensConfig;
currentLine: {
dateFormat: string | null;
enabled: boolean;
@ -29,7 +34,6 @@ export interface Config {
};
scrollable: boolean;
};
codeLens: CodeLensConfig;
debug: boolean;
defaultDateFormat: string | null;
defaultDateShortFormat: string | null;
@ -93,12 +97,6 @@ export interface Config {
};
modes: Record<string, ModeConfig>;
outputLevel: TraceLevel;
recentChanges: {
highlight: {
locations: HighlightLocations[];
};
toggleMode: AnnotationsToggleMode;
};
remotes: RemotesConfig[] | null;
showWhatsNewAfterUpgrades: boolean;
sortBranchesBy: BranchSorting;
@ -137,6 +135,12 @@ export interface AutolinkReference {
ignoreCase?: boolean;
}
export enum BlameHighlightLocations {
Gutter = 'gutter',
Line = 'line',
Overview = 'overview',
}
export enum BranchSorting {
NameDesc = 'name:desc',
NameAsc = 'name:asc',
@ -144,6 +148,11 @@ export enum BranchSorting {
DateAsc = 'date:asc',
}
export enum ChangesLocations {
Gutter = 'gutter',
Overview = 'overview',
}
export enum CodeLensCommand {
DiffWithPrevious = 'gitlens.diffWithPrevious',
RevealCommitInView = 'gitlens.revealCommitInView',
@ -181,8 +190,8 @@ export enum DateStyle {
export enum FileAnnotationType {
Blame = 'blame',
Changes = 'changes',
Heatmap = 'heatmap',
RecentChanges = 'recentChanges',
}
export enum GravatarDefaultStyle {
@ -194,12 +203,6 @@ export enum GravatarDefaultStyle {
Robot = 'robohash',
}
export enum HighlightLocations {
Gutter = 'gutter',
Line = 'line',
Overview = 'overview',
}
export enum KeyMap {
Alternate = 'alternate',
Chorded = 'chorded',
@ -386,7 +389,7 @@ export interface ModeConfig {
name: string;
statusBarItemName?: string;
description?: string;
annotations?: 'blame' | 'heatmap' | 'recentChanges';
annotations?: 'blame' | 'changes' | 'heatmap';
codeLens?: boolean;
currentLine?: boolean;
hovers?: boolean;

+ 5
- 5
src/container.ts View File

@ -315,14 +315,14 @@ export class Container {
config.blame.toggleMode = AnnotationsToggleMode.Window;
command = Commands.ToggleFileBlame;
break;
case 'changes':
config.changes.toggleMode = AnnotationsToggleMode.Window;
command = Commands.ToggleFileChanges;
break;
case 'heatmap':
config.heatmap.toggleMode = AnnotationsToggleMode.Window;
command = Commands.ToggleFileHeatmap;
break;
case 'recentChanges':
config.recentChanges.toggleMode = AnnotationsToggleMode.Window;
command = Commands.ToggleFileRecentChanges;
break;
}
if (command != null) {
@ -375,11 +375,11 @@ export class Container {
`gitlens.${configuration.name('mode')}`,
`gitlens.${configuration.name('modes')}`,
`gitlens.${configuration.name('blame', 'toggleMode')}`,
`gitlens.${configuration.name('changes', 'toggleMode')}`,
`gitlens.${configuration.name('codeLens')}`,
`gitlens.${configuration.name('currentLine')}`,
`gitlens.${configuration.name('heatmap', 'toggleMode')}`,
`gitlens.${configuration.name('hovers')}`,
`gitlens.${configuration.name('recentChanges', 'toggleMode')}`,
`gitlens.${configuration.name('statusBar')}`,
`gitlens.${configuration.name('views', 'compare')}`,
`gitlens.${configuration.name('views', 'fileHistory')}`,

+ 5
- 0
src/extension.ts View File

@ -11,6 +11,7 @@ import { GitUri } from './git/gitUri';
import { Logger } from './logger';
import { Messages } from './messages';
import { Strings, Versions } from './system';
import { ViewNode } from './views/nodes';
export async function activate(context: ExtensionContext) {
const start = process.hrtime();
@ -29,6 +30,10 @@ export async function activate(context: ExtensionContext) {
return `GitCommit(${o.sha ? ` sha=${o.sha}` : ''}${o.repoPath ? ` repoPath=${o.repoPath}` : ''})`;
}
if (ViewNode.is(o)) {
return o.toString();
}
return undefined;
});

+ 1
- 7
src/git/formatters/commitFormatter.ts View File

@ -32,7 +32,6 @@ const emptyStr = '';
const hasTokenRegexMap = new Map<string, RegExp>();
export interface CommitFormatOptions extends FormatOptions {
annotationType?: FileAnnotationType;
autolinkedIssuesOrPullRequests?: Map<string, IssueOrPullRequest | Promises.CancellationError | undefined>;
dateStyle?: DateStyle;
getBranchAndTagTips?: (sha: string) => string | undefined;
@ -270,11 +269,6 @@ export class CommitFormatter extends Formatter {
)} "Open Changes")${separator}`;
if (this._item.previousSha != null) {
let annotationType = this._options.annotationType;
if (annotationType === FileAnnotationType.RecentChanges) {
annotationType = FileAnnotationType.Blame;
}
const uri = GitUri.toRevisionUri(
this._item.previousSha,
this._item.previousUri.fsPath,
@ -282,7 +276,7 @@ export class CommitFormatter extends Formatter {
);
commands += `[$(history)](${OpenFileAtRevisionCommand.getMarkdownCommandArgs(
uri,
annotationType ?? FileAnnotationType.Blame,
FileAnnotationType.Blame,
this._options.line,
)} "Blame Previous Revision")${separator}`;
}

+ 76
- 10
src/git/git.ts View File

@ -614,6 +614,67 @@ export namespace Git {
}
}
export async function diff__contents(
repoPath: string,
fileName: string,
ref: string,
contents: string,
options: { encoding?: string; filters?: GitDiffFilter[]; similarityThreshold?: number | null } = {},
): Promise<string> {
const params = [
'diff',
`-M${options.similarityThreshold == null ? '' : `${options.similarityThreshold}%`}`,
'--no-ext-diff',
'-U0',
'--minimal',
];
if (options.filters != null && options.filters.length !== 0) {
params.push(`--diff-filter=${options.filters.join(emptyStr)}`);
}
// // <sha>^3 signals an untracked file in a stash and if we are trying to find its parent, use the root sha
// if (ref.endsWith('^3^')) {
// ref = rootSha;
// }
// params.push(GitRevision.isUncommittedStaged(ref) ? '--staged' : ref);
params.push('--no-index');
try {
return await git<string>(
{
cwd: repoPath,
configs: ['-c', 'color.diff=false'],
encoding: options.encoding === 'utf8' ? 'utf8' : 'binary',
stdin: contents,
},
...params,
'--',
fileName,
// Pipe the contents to stdin
'-',
);
} catch (ex) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (ex.stdout) {
return ex.stdout;
}
const match = GitErrors.badRevision.exec(ex.message);
if (match !== null) {
const [, matchedRef] = match;
// If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha
if (matchedRef === ref && matchedRef != null && matchedRef.endsWith('^')) {
return Git.diff__contents(repoPath, fileName, rootSha, contents, options);
}
}
throw ex;
}
}
export function diff__name_status(
repoPath: string,
ref1?: string,
@ -766,7 +827,7 @@ export namespace Git {
renames = true,
reverse = false,
skip,
simple = false,
format = 'default',
startLine,
endLine,
}: {
@ -777,14 +838,17 @@ export namespace Git {
renames?: boolean;
reverse?: boolean;
skip?: number;
simple?: boolean;
format?: 'refs' | 'simple' | 'default';
startLine?: number;
endLine?: number;
} = {},
) {
const [file, root] = Git.splitPath(fileName, repoPath);
const params = ['log', `--format=${simple ? GitLogParser.simpleFormat : GitLogParser.defaultFormat}`];
const params = [
'log',
`--format=${format === 'default' ? GitLogParser.defaultFormat : GitLogParser.simpleFormat}`,
];
if (limit && !reverse) {
params.push(`-n${limit}`);
@ -808,15 +872,17 @@ export namespace Git {
params.push('--first-parent');
}
if (startLine == null) {
if (simple) {
params.push('--name-status');
if (format !== 'refs') {
if (startLine == null) {
if (format === 'simple') {
params.push('--name-status');
} else {
params.push('--numstat', '--summary');
}
} else {
params.push('--numstat', '--summary');
// Don't include --name-status or -s because Git won't honor it
params.push(`-L ${startLine},${endLine == null ? startLine : endLine}:${file}`);
}
} else {
// Don't include --name-status or -s because Git won't honor it
params.push(`-L ${startLine},${endLine == null ? startLine : endLine}:${file}`);
}
if (ref && !GitRevision.isUncommittedStaged(ref)) {

+ 125
- 17
src/git/gitService.ts View File

@ -1192,6 +1192,17 @@ export class GitService implements Disposable {
}
@log()
async getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise<string | undefined> {
const data = await Git.log__file(repoPath, fileName, '@{push}..', {
format: 'refs',
renames: true,
});
if (data == null || data.length === 0) return undefined;
return GitLogParser.parseLastRefOnly(data);
}
@log()
getConfig(key: string, repoPath?: string): Promise<string | undefined> {
return Git.config__get(key, repoPath);
}
@ -1334,19 +1345,116 @@ export class GitService implements Disposable {
const [file, root] = Git.splitPath(fileName, repoPath, false);
try {
let data;
if (ref1 != null && ref2 == null && !GitRevision.isUncommittedStaged(ref1)) {
data = await Git.show__diff(root, file, ref1, originalFileName, {
similarityThreshold: Container.config.advanced.similarityThreshold,
});
} else {
data = await Git.diff(root, file, ref1, ref2, {
...options,
filters: ['M'],
similarityThreshold: Container.config.advanced.similarityThreshold,
});
// let data;
// if (ref2 == null && ref1 != null && !GitRevision.isUncommittedStaged(ref1)) {
// data = await Git.show__diff(root, file, ref1, originalFileName, {
// similarityThreshold: Container.config.advanced.similarityThreshold,
// });
// } else {
const data = await Git.diff(root, file, ref1, ref2, {
...options,
filters: ['M'],
similarityThreshold: Container.config.advanced.similarityThreshold,
});
// }
const diff = GitDiffParser.parse(data);
return diff;
} catch (ex) {
// Trap and cache expected diff errors
if (document.state != null) {
const msg = ex?.toString() ?? '';
Logger.debug(cc, `Cache replace (with empty promise): '${key}'`);
const value: CachedDiff = {
item: emptyPromise as Promise<GitDiff>,
errorMessage: msg,
};
document.state.set<CachedDiff>(key, value);
return emptyPromise as Promise<GitDiff>;
}
return undefined;
}
}
@log({
args: {
1: _contents => '<contents>',
},
})
async getDiffForFileContents(
uri: GitUri,
ref: string,
contents: string,
originalFileName?: string,
): Promise<GitDiff | undefined> {
const cc = Logger.getCorrelationContext();
const key = `diff:${Strings.sha1(contents)}`;
const doc = await Container.tracker.getOrAdd(uri);
if (this.useCaching) {
if (doc.state != null) {
const cachedDiff = doc.state.get<CachedDiff>(key);
if (cachedDiff != null) {
Logger.debug(cc, `Cache hit: ${key}`);
return cachedDiff.item;
}
}
Logger.debug(cc, `Cache miss: ${key}`);
if (doc.state == null) {
doc.state = new GitDocumentState(doc.key);
}
}
const promise = this.getDiffForFileContentsCore(
uri.repoPath,
uri.fsPath,
ref,
contents,
originalFileName,
{ encoding: GitService.getEncoding(uri) },
doc,
key,
cc,
);
if (doc.state != null) {
Logger.debug(cc, `Cache add: '${key}'`);
const value: CachedDiff = {
item: promise as Promise<GitDiff>,
};
doc.state.set<CachedDiff>(key, value);
}
return promise;
}
async getDiffForFileContentsCore(
repoPath: string | undefined,
fileName: string,
ref: string,
contents: string,
originalFileName: string | undefined,
options: { encoding?: string },
document: TrackedDocument<GitDocumentState>,
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitDiff | undefined> {
const [file, root] = Git.splitPath(fileName, repoPath, false);
try {
const data = await Git.diff__contents(root, file, ref, contents, {
...options,
filters: ['M'],
similarityThreshold: Container.config.advanced.similarityThreshold,
});
const diff = GitDiffParser.parse(data);
return diff;
} catch (ex) {
@ -1381,10 +1489,10 @@ export class GitService implements Disposable {
if (diff == null) return undefined;
const line = editorLine + 1;
const hunk = diff.hunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line);
const hunk = diff.hunks.find(c => c.current.position.start <= line && c.current.position.end >= line);
if (hunk == null) return undefined;
return hunk.lines[line - hunk.currentPosition.start];
return hunk.lines[line - hunk.current.position.start];
} catch (ex) {
return undefined;
}
@ -2071,7 +2179,7 @@ export class GitService implements Disposable {
limit: skip + 1,
// startLine: editorLine != null ? editorLine + 1 : undefined,
reverse: true,
simple: true,
format: 'simple',
});
if (data == null || data.length === 0) return undefined;
@ -2082,7 +2190,7 @@ export class GitService implements Disposable {
filters: ['R', 'C'],
limit: 1,
// startLine: editorLine != null ? editorLine + 1 : undefined
simple: true,
format: 'simple',
});
if (data == null || data.length === 0) {
return GitUri.fromFile(file ?? fileName, repoPath, nextRef);
@ -2319,7 +2427,7 @@ export class GitService implements Disposable {
data = await Git.log__file(repoPath, fileName, ref, {
limit: skip + 2,
firstParent: firstParent,
simple: true,
format: 'simple',
startLine: editorLine != null ? editorLine + 1 : undefined,
});
} catch (ex) {
@ -2882,7 +2990,7 @@ export class GitService implements Disposable {
data = await Git.log__file(repoPath, '.', ref, {
filters: ['R', 'C', 'D'],
limit: 1,
simple: true,
format: 'simple',
});
if (data == null || data.length === 0) break;

+ 21
- 5
src/git/models/diff.ts View File

@ -1,6 +1,5 @@
'use strict';
import { GitDiffParser } from '../parsers/diffParser';
import { memoize } from '../../system';
export interface GitDiffLine {
line: string;
@ -16,13 +15,30 @@ export interface GitDiffHunkLine {
export class GitDiffHunk {
constructor(
public readonly diff: string,
public currentPosition: { start: number; end: number },
public previousPosition: { start: number; end: number },
public current: {
count: number;
position: { start: number; end: number };
},
public previous: {
count: number;
position: { start: number; end: number };
},
) {}
@memoize()
get lines(): GitDiffHunkLine[] {
return GitDiffParser.parseHunk(this);
return this.parseHunk().lines;
}
get state(): 'added' | 'changed' | 'removed' {
return this.parseHunk().state;
}
private parsedHunk: { lines: GitDiffHunkLine[]; state: 'added' | 'changed' | 'removed' } | undefined;
private parseHunk() {
if (this.parsedHunk == null) {
this.parsedHunk = GitDiffParser.parseHunk(this);
}
return this.parsedHunk;
}
}

+ 22
- 6
src/git/parsers/diffParser.ts View File

@ -27,7 +27,9 @@ export class GitDiffParser {
[, previousStart, previousCount, currentStart, currentCount, hunk] = match;
previousCount = Number(previousCount) || 0;
previousStart = Number(previousStart) || 0;
currentCount = Number(currentCount) || 0;
currentStart = Number(currentStart) || 0;
hunks.push(
@ -35,12 +37,18 @@ export class GitDiffParser {
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
` ${hunk}`.substr(1),
{
start: currentStart,
end: currentStart + (Number(currentCount) || 0),
count: currentCount,
position: {
start: currentStart,
end: currentStart + (currentCount > 0 ? currentCount - 1 : 0),
},
},
{
start: previousStart,
end: previousStart + (Number(previousCount) || 0),
count: previousCount,
position: {
start: previousStart,
end: previousStart + (previousCount > 0 ? previousCount - 1 : 0),
},
},
),
);
@ -56,14 +64,18 @@ export class GitDiffParser {
}
@debug({ args: false, singleLine: true })
static parseHunk(hunk: GitDiffHunk): GitDiffHunkLine[] {
static parseHunk(hunk: GitDiffHunk): { lines: GitDiffHunkLine[]; state: 'added' | 'changed' | 'removed' } {
const currentLines: (GitDiffLine | undefined)[] = [];
const previousLines: (GitDiffLine | undefined)[] = [];
let hasAddedOrChanged;
let hasRemoved;
let removed = 0;
for (const l of Strings.lines(hunk.diff)) {
switch (l[0]) {
case '+':
hasAddedOrChanged = true;
currentLines.push({
line: ` ${l.substring(1)}`,
state: 'added',
@ -78,6 +90,7 @@ export class GitDiffParser {
break;
case '-':
hasRemoved = true;
removed++;
previousLines.push({
@ -115,7 +128,10 @@ export class GitDiffParser {
});
}
return hunkLines;
return {
lines: hunkLines,
state: hasAddedOrChanged && hasRemoved ? 'changed' : hasAddedOrChanged ? 'added' : 'removed',
};
}
@debug({ args: false, singleLine: true })

+ 43
- 0
src/git/parsers/logParser.ts View File

@ -25,6 +25,7 @@ const fileStatusAndSummaryRegex = /^(\d+?|-)\s+?(\d+?|-)\s+?(.*)(?:\n\s(delete|r
const fileStatusAndSummaryRenamedFileRegex = /(.+)\s=>\s(.+)/;
const fileStatusAndSummaryRenamedFilePathRegex = /(.*?){(.+?)\s=>\s(.*?)}(.*)/;
const logFileRefsRegex = /^<r> (.*)/gm;
const logFileSimpleRegex = /^<r> (.*)\s*(?:(?:diff --git a\/(.*) b\/(.*))|(?:(\S)\S*\t([^\t\n]+)(?:\t(.+))?))/gm;
const logFileSimpleRenamedRegex = /^<r> (\S+)\s*(.*)$/s;
const logFileSimpleRenamedFilesRegex = /^(\S)\S*\t([^\t\n]+)(?:\t(.+)?)?$/gm;
@ -75,6 +76,7 @@ export class GitLogParser {
`${lb}f${rb}`,
].join('%n');
static simpleRefs = `${lb}r${rb}${sp}%H`;
static simpleFormat = `${lb}r${rb}${sp}%H`;
@debug({ args: false })
@ -468,6 +470,47 @@ export class GitLogParser {
}
@debug({ args: false })
static parseLastRefOnly(data: string): string | undefined {
let ref;
let match;
do {
match = logFileRefsRegex.exec(data);
if (match == null) break;
[, ref] = match;
} while (true);
// Ensure the regex state is reset
logFileRefsRegex.lastIndex = 0;
return ref;
}
@debug({ args: false })
static parseRefsOnly(data: string): string[] {
const refs = [];
let ref;
let match;
do {
match = logFileRefsRegex.exec(data);
if (match == null) break;
[, ref] = match;
if (ref == null || ref.length === 0) {
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
refs.push(` ${ref}`.substr(1));
}
} while (true);
// Ensure the regex state is reset
logFileRefsRegex.lastIndex = 0;
return refs;
}
@debug({ args: false })
static parseSimple(
data: string,
skip: number,

+ 96
- 52
src/hovers/hovers.ts View File

@ -1,13 +1,13 @@
'use strict';
import { MarkdownString } from 'vscode';
import { DiffWithCommand, ShowQuickCommitCommand } from '../commands';
import { FileAnnotationType } from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import {
CommitFormatter,
GitBlameCommit,
GitCommit,
GitDiffHunk,
GitDiffHunkLine,
GitLogCommit,
GitRemote,
@ -19,23 +19,13 @@ import { Iterables, Promises, Strings } from '../system';
export namespace Hovers {
export async function changesMessage(
commit: GitBlameCommit,
uri: GitUri,
editorLine: number,
): Promise<MarkdownString | undefined>;
export async function changesMessage(
commit: GitLogCommit,
uri: GitUri,
editorLine: number,
hunkLine: GitDiffHunkLine,
): Promise<MarkdownString | undefined>;
export async function changesMessage(
commit: GitBlameCommit | GitLogCommit,
uri: GitUri,
editorLine: number,
hunkLine?: GitDiffHunkLine,
): Promise<MarkdownString | undefined> {
const documentRef = uri.sha;
let hunkLine;
if (GitBlameCommit.is(commit)) {
// TODO: Figure out how to optimize this
let ref;
@ -51,17 +41,18 @@ export namespace Hovers {
const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
let originalFileName = commit.originalFileName;
if (originalFileName === undefined) {
if (originalFileName == null) {
if (uri.fsPath !== commit.uri.fsPath) {
originalFileName = commit.fileName;
}
}
editorLine = commitLine.originalLine - 1;
// TODO: Doesn't work with dirty files -- pass in editor? or contents?
hunkLine = await Container.git.getDiffForLine(uri, editorLine, ref, undefined, originalFileName);
// If we didn't find a diff & ref is undefined (meaning uncommitted), check for a staged diff
if (hunkLine === undefined && ref === undefined) {
if (hunkLine == null && ref == null) {
hunkLine = await Container.git.getDiffForLine(
uri,
editorLine,
@ -72,7 +63,7 @@ export namespace Hovers {
}
}
if (hunkLine === undefined || commit.previousSha === undefined) return undefined;
if (hunkLine == null || commit.previousSha == null) return undefined;
const diff = getDiffFromHunkLine(hunkLine);
@ -81,11 +72,11 @@ export namespace Hovers {
let current;
if (commit.isUncommitted) {
const diffUris = await commit.getPreviousLineDiffUris(uri, editorLine, documentRef);
if (diffUris === undefined || diffUris.previous === undefined) {
if (diffUris == null || diffUris.previous == null) {
return undefined;
}
message = `[$(compare-changes) Changes](${DiffWithCommand.getMarkdownCommandArgs({
message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({
lhs: {
sha: diffUris.previous.sha ?? '',
uri: diffUris.previous.documentUri(),
@ -99,7 +90,7 @@ export namespace Hovers {
})} "Open Changes")`;
previous =
diffUris.previous.sha === undefined || diffUris.previous.isUncommitted
diffUris.previous.sha == null || diffUris.previous.isUncommitted
? `_${GitRevision.shorten(diffUris.previous.sha, {
strings: {
working: 'Working Tree',
@ -107,12 +98,10 @@ export namespace Hovers {
})}_`
: `[$(git-commit) ${GitRevision.shorten(
diffUris.previous.sha || '',
)}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
diffUris.previous.sha || '',
)} "Show Commit Details")`;
)}](${ShowQuickCommitCommand.getMarkdownCommandArgs(diffUris.previous.sha || '')} "Show Commit")`;
current =
diffUris.current.sha === undefined || diffUris.current.isUncommitted
diffUris.current.sha == null || diffUris.current.isUncommitted
? `_${GitRevision.shorten(diffUris.current.sha, {
strings: {
working: 'Working Tree',
@ -120,25 +109,68 @@ export namespace Hovers {
})}_`
: `[$(git-commit) ${GitRevision.shorten(
diffUris.current.sha || '',
)}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
diffUris.current.sha || '',
)} "Show Commit Details")`;
)}](${ShowQuickCommitCommand.getMarkdownCommandArgs(diffUris.current.sha || '')} "Show Commit")`;
} else {
message = `[$(compare-changes) Changes](${DiffWithCommand.getMarkdownCommandArgs(
message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs(
commit,
editorLine,
)} "Open Changes")`;
previous = `[$(git-commit) ${commit.previousShortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
commit.previousSha,
)} "Show Commit Details")`;
)} "Show Commit")`;
current = `[$(git-commit) ${commit.shortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
commit.sha,
)} "Show Commit Details")`;
)} "Show Commit")`;
}
message += ` &nbsp; ${GlyphChars.Dash} &nbsp; ${previous} &nbsp;${GlyphChars.ArrowLeftRightLong}&nbsp; ${current}\n${diff}`;
message = `${diff}\n---\n\nChanges &nbsp;${previous} &nbsp;${GlyphChars.ArrowLeftRightLong}&nbsp; ${current} &nbsp;&nbsp;|&nbsp;&nbsp; ${message}`;
const markdown = new MarkdownString(message, true);
markdown.isTrusted = true;
return markdown;
}
export function localChangesMessage(
fromCommit: GitLogCommit | undefined,
uri: GitUri,
editorLine: number,
hunk: GitDiffHunk,
): MarkdownString {
const diff = getDiffFromHunk(hunk);
let message;
let previous;
let current;
if (fromCommit == null) {
previous = '_Working Tree_';
current = '_Unsaved_';
} else {
const file = fromCommit.findFile(uri.fsPath)!;
message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({
lhs: {
sha: fromCommit.sha,
uri: GitUri.fromFile(file, uri.repoPath!, undefined, true).toFileUri(),
},
rhs: {
sha: '',
uri: uri.toFileUri(),
},
repoPath: uri.repoPath!,
line: editorLine,
})} "Open Changes")`;
previous = `[$(git-commit) ${fromCommit.shortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
fromCommit.sha,
)} "Show Commit")`;
current = '_Working Tree_';
}
message = `${diff}\n---\n\nLocal Changes &nbsp;${previous} &nbsp;${
GlyphChars.ArrowLeftRightLong
}&nbsp; ${current}${message == null ? '' : ` &nbsp;&nbsp;|&nbsp;&nbsp; ${message}`}`;
const markdown = new MarkdownString(message, true);
markdown.isTrusted = true;
@ -150,7 +182,6 @@ export namespace Hovers {
uri: GitUri,
editorLine: number,
dateFormat: string | null,
annotationType: FileAnnotationType | undefined,
): Promise<MarkdownString> {
if (dateFormat === null) {
dateFormat = 'MMMM Do, YYYY h:mma';
@ -166,7 +197,6 @@ export namespace Hovers {
]);
const details = CommitFormatter.fromTemplate(Container.config.hovers.detailsMarkdownFormat, commit, {
annotationType: annotationType,
autolinkedIssuesOrPullRequests: autolinkedIssuesOrPullRequests,
dateFormat: dateFormat,
line: editorLine,
@ -182,13 +212,17 @@ export namespace Hovers {
return markdown;
}
function getDiffFromHunkLine(hunkLine: GitDiffHunkLine): string {
if (Container.config.hovers.changesDiff === 'hunk') {
return `\`\`\`diff\n${hunkLine.hunk.diff}\n\`\`\``;
function getDiffFromHunk(hunk: GitDiffHunk): string {
return `\`\`\`diff\n${hunk.diff.trim()}\n\`\`\``;
}
function getDiffFromHunkLine(hunkLine: GitDiffHunkLine, diffStyle?: 'line' | 'hunk'): string {
if (diffStyle === 'hunk' || (diffStyle == null && Container.config.hovers.changesDiff === 'hunk')) {
return getDiffFromHunk(hunkLine.hunk);
}
return `\`\`\`diff${hunkLine.previous === undefined ? '' : `\n-${hunkLine.previous.line}`}${
hunkLine.current === undefined ? '' : `\n+${hunkLine.current.line}`
return `\`\`\`diff${hunkLine.previous == null ? '' : `\n-${hunkLine.previous.line.trim()}`}${
hunkLine.current == null ? '' : `\n+${hunkLine.current.line.trim()}`
}\n\`\`\``;
}
@ -209,7 +243,7 @@ export namespace Hovers {
}
const remote = remotes.find(r => r.default && r.provider != null);
if (remote === undefined) {
if (remote == null) {
Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
return undefined;
@ -223,24 +257,34 @@ export namespace Hovers {
timeout: timeout,
});
if (autolinks !== undefined && (Logger.level === TraceLevel.Debug || Logger.isDebugging)) {
const timeouts = [
...Iterables.filterMap(autolinks.values(), issue =>
issue instanceof Promises.CancellationError ? issue.promise : undefined,
),
];
// If there are any PRs that timed out, refresh the annotation(s) once they complete
if (timeouts.length !== 0) {
if (autolinks != null && (Logger.level === TraceLevel.Debug || Logger.isDebugging)) {
// If there are any issues/PRs that timed out, log it
const count = Iterables.count(autolinks.values(), pr => pr instanceof Promises.CancellationError);
if (count !== 0) {
Logger.debug(
cc,
`timed out ${GlyphChars.Dash} issue/pr queries (${
timeouts.length
}) took too long (over ${timeout} ms) ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(
start,
)} ms`,
`timed out ${
GlyphChars.Dash
} ${count} issue/pull request queries took too long (over ${timeout} ms) ${
GlyphChars.Dot
} ${Strings.getDurationMilliseconds(start)} ms`,
);
// const pending = [
// ...Iterables.map(autolinks.values(), issueOrPullRequest =>
// issueOrPullRequest instanceof Promises.CancellationError
// ? issueOrPullRequest.promise
// : undefined,
// ),
// ];
// void Promise.all(pending).then(() => {
// Logger.debug(
// cc,
// `${GlyphChars.Dot} ${count} issue/pull request queries completed; refreshing...`,
// );
// void commands.executeCommand('editor.action.showHover');
// });
return autolinks;
}
}

+ 6
- 5
src/hovers/lineHoverController.ts View File

@ -12,7 +12,7 @@ import {
Uri,
window,
} from 'vscode';
import { configuration } from '../configuration';
import { configuration, FileAnnotationType } from '../configuration';
import { Container } from '../container';
import { Hovers } from './hovers';
import { LinesChangeEvent } from '../trackers/gitLineTracker';
@ -98,8 +98,10 @@ export class LineHoverController implements Disposable {
if (commit === undefined) return undefined;
// Avoid double annotations if we are showing the whole-file hover blame annotations
const fileAnnotations = await Container.fileAnnotations.getAnnotationType(window.activeTextEditor);
if (fileAnnotations !== undefined && Container.config.hovers.annotations.details) return undefined;
if (Container.config.hovers.annotations.details) {
const fileAnnotations = await Container.fileAnnotations.getAnnotationType(window.activeTextEditor);
if (fileAnnotations === FileAnnotationType.Blame) return undefined;
}
const wholeLine = Container.config.hovers.currentLine.over === 'line';
// If we aren't showing the hover over the whole line, make sure the annotation is on
@ -140,7 +142,6 @@ export class LineHoverController implements Disposable {
trackedDocument.uri,
editorLine,
Container.config.defaultDateFormat,
fileAnnotations,
);
return new Hover(message, range);
}
@ -166,7 +167,7 @@ export class LineHoverController implements Disposable {
// Avoid double annotations if we are showing the whole-file hover blame annotations
if (Container.config.hovers.annotations.changes) {
const fileAnnotations = await Container.fileAnnotations.getAnnotationType(window.activeTextEditor);
if (fileAnnotations !== undefined) return undefined;
if (fileAnnotations === FileAnnotationType.Blame) return undefined;
}
const wholeLine = Container.config.hovers.currentLine.over === 'line';

+ 4
- 0
src/views/nodes/viewNode.ts View File

@ -58,6 +58,10 @@ export interface ViewNode {
@logName<ViewNode>((c, name) => `${name}${c.id != null ? `(${c.id})` : ''}`)
export abstract class ViewNode<TView extends View = View> {
static is(node: any): node is ViewNode {
return node instanceof ViewNode;
}
constructor(uri: GitUri, public readonly view: TView, protected readonly parent?: ViewNode) {
this._uri = uri;
}

+ 2
- 2
src/views/viewCommands.ts View File

@ -300,7 +300,7 @@ export class ViewCommands {
void (await this.openFile(node));
void (await Container.fileAnnotations.toggle(
window.activeTextEditor,
FileAnnotationType.RecentChanges,
FileAnnotationType.Changes,
node.ref,
true,
));
@ -319,7 +319,7 @@ export class ViewCommands {
void (await this.openRevision(node, { showOptions: { preserveFocus: true, preview: true } }));
void (await Container.fileAnnotations.toggle(
window.activeTextEditor,
FileAnnotationType.RecentChanges,
FileAnnotationType.Changes,
node.ref,
true,
));

+ 2
- 2
src/webviews/apps/settings/partials/blame.ejs View File

@ -136,7 +136,7 @@
data-setting-type="array"
disabled
/>
<label for="blame.highlight.locations">Add gutter highlight</label>
<label for="blame.highlight.locations">Add gutter indicator</label>
</div>
</div>
@ -166,7 +166,7 @@
data-setting-type="array"
disabled
/>
<label for="blame.highlight.locations-2">Add scroll bar highlight</label>
<label for="blame.highlight.locations-2">Add scroll bar indicator</label>
</div>
</div>
</div>

src/webviews/apps/settings/partials/recent-changes.ejs → src/webviews/apps/settings/partials/changes.ejs View File

@ -1,7 +1,7 @@
<section id="recent-changes" class="section--settings section--collapsible">
<section id="changes" class="section--settings section--collapsible">
<div class="section__header">
<h2>
Recent Changes
Gutter Changes
<a
class="link__learn-more"
title="Learn more"
@ -12,23 +12,24 @@
</h2>
<p class="section__header-hint">
Adds on-demand recent changes annotations to highlight lines changed by the most recent commit
Adds on-demand gutter changes annotations to highlight any local changes or lines changed by the most recent
commit
</p>
<div class="section__header-info">
<i class="icon icon--md icon__bulb"></i>
<div>
<p>
Use the
<span class="command hidden" data-visibility="recentChanges.toggleMode =file">
GitLens: Toggle Recent File Changes Annotations
<span class="command hidden" data-visibility="changes.toggleMode =file">
GitLens: Toggle File Changes Annotations
</span>
<a
class="command hidden"
title="Run command"
href="command:gitlens.toggleFileRecentChanges"
data-visibility="recentChanges.toggleMode =window"
href="command:gitlens.toggleFileChanges"
data-visibility="changes.toggleMode =window"
>
GitLens: Toggle Recent File Changes Annotations
GitLens: Toggle File Changes Annotations
</a>
command to turn the annotations on or off
</p>
@ -42,8 +43,8 @@
<div class="section__content">
<div class="setting">
<div class="setting__input">
<label for="recentChanges.toggleMode">Toggle annotations </label>
<select id="recentChanges.toggleMode" name="recentChanges.toggleMode" data-setting>
<label for="changes.toggleMode">Toggle annotations </label>
<select id="changes.toggleMode" name="changes.toggleMode" data-setting>
<option value="file">individually for each file</option>
<option value="window">for all files</option>
</select>
@ -54,42 +55,28 @@
<div class="setting">
<div class="setting__input">
<input
id="recentChanges.highlight.locations"
name="recentChanges.highlight.locations"
id="changes.locations"
name="changes.locations"
type="checkbox"
value="gutter"
data-setting
data-setting-type="array"
/>
<label for="recentChanges.highlight.locations">Add gutter highlight</label>
<label for="changes.locations">Add gutter indicator</label>
</div>
</div>
<div class="setting">
<div class="setting__input">
<input
id="recentChanges.highlight.locations-1"
name="recentChanges.highlight.locations"
type="checkbox"
value="line"
data-setting
data-setting-type="array"
/>
<label for="recentChanges.highlight.locations-1">Add line highlight</label>
</div>
</div>
<div class="setting">
<div class="setting__input">
<input
id="recentChanges.highlight.locations-2"
name="recentChanges.highlight.locations"
id="changes.locations-1"
name="changes.locations"
type="checkbox"
value="overview"
data-setting
data-setting-type="array"
/>
<label for="recentChanges.highlight.locations-2">Add scroll bar highlight</label>
<label for="changes.locations-1">Add scroll bar indicator</label>
</div>
</div>
</div>
@ -100,17 +87,12 @@
<img
class="image__preview--overlay hidden"
src="#{root}/images/settings/recent-changes-highlight-gutter.png"
data-visibility="recentChanges.highlight.locations +gutter"
/>
<img
class="image__preview--overlay hidden"
src="#{root}/images/settings/recent-changes-highlight-line.png"
data-visibility="recentChanges.highlight.locations +line"
data-visibility="changes.locations +gutter"
/>
<img
class="image__preview--overlay hidden"
src="#{root}/images/settings/recent-changes-highlight-scrollbar.png"
data-visibility="recentChanges.highlight.locations +overview"
data-visibility="changes.locations +overview"
/>
</div>
</div>

+ 8
- 8
src/webviews/apps/settings/settings.ejs View File

@ -188,9 +188,9 @@
<!-- prettier-ignore -->
<%- include partials/blame.ejs -%>
<!-- prettier-ignore -->
<%- include partials/heatmap.ejs -%>
<%- include partials/changes.ejs -%>
<!-- prettier-ignore -->
<%- include partials/recent-changes.ejs -%>
<%- include partials/heatmap.ejs -%>
<!-- prettier-ignore -->
<%- include partials/dates.ejs -%>
<!-- prettier-ignore -->
@ -310,18 +310,18 @@
<a
class="sidebar__jump-link"
data-action="jump"
href="#heatmap"
title="Jump to Gutter Heatmap settings"
>Gutter Heatmap</a
href="#changes"
title="Jump to Gutter Changes settings"
>Gutter Changes</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#recent-changes"
title="Jump to Recent Changes settings"
>Recent Changes</a
href="#heatmap"
title="Jump to Gutter Heatmap settings"
>Gutter Heatmap</a
>
</li>

Loading…
Cancel
Save