diff --git a/README.md b/README.md
index cc6df90..4ebe328 100644
--- a/README.md
+++ b/README.md
@@ -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')
-
+
-- 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')
-
+
-- 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 `left` - adds a heatmap indicator on the left edge of the gutter blame annotations `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 `gutter` - adds a gutter glyph `line` - adds a full-line highlight background color `overview` - adds a decoration to the overview ruler (scroll bar) |
+| `gitlens.blame.highlight.locations` | Specifies where the associated line highlights will be shown `gutter` - adds a gutter indicator `line` - adds a full-line highlight background color `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 `file` - toggles each file individually `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 `gutter` - adds a gutter indicator `overview` - adds a decoration to the overview ruler (scroll bar) |
+| `gitlens.changes.toggleMode` | Specifies how the gutter changes annotations will be toggled `file` - toggles each file individually `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 `file` - toggles each file individually `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 `gutter` - adds a gutter glyph `line` - adds a full-line highlight background color `overview` - adds a decoration to the overview ruler (scroll bar) |
-| `gitlens.recentChanges.toggleMode` | Specifies how the recently changed lines annotations will be toggled `file` - toggles each file individually `window` - toggles the window, i.e. all files at once |
-
### Git Commands Menu Settings [#](#git-commands-menu-settings- 'Git Commands Menu Settings')
| Name | Description |
diff --git a/package.json b/package.json
index e154cf3..745ad5b 100644
--- a/package.json
+++ b/package.json
@@ -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/"
},
{
diff --git a/src/annotations/annotationProvider.ts b/src/annotations/annotationProvider.ts
index cf9db29..5db824b 100644
--- a/src/annotations/annotationProvider.ts
+++ b/src/annotations/annotationProvider.ts
@@ -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,
- protected decoration: TextEditorDecorationType | undefined,
- protected highlightDecoration: TextEditorDecorationType | undefined,
- ) {
+ constructor(public editor: TextEditor, protected readonly trackedDocument: TrackedDocument) {
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 {
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;
+ protected abstract onProvideAnnotation(shaOrLine?: string | number): Promise;
+
abstract selection(shaOrLine?: string | number): Promise;
+
+ 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;
}
diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts
index 82ad855..931b3e9 100644
--- a/src/annotations/annotations.ts
+++ b/src/annotations/annotations.ts
@@ -109,7 +109,7 @@ export class Annotations {
date: Date,
heatmap: ComputedHeatmap,
range: Range,
- map: Map,
+ map: Map,
) {
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(
` `,
@@ -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(
diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts
index 9462be8..ebd6fcd 100644
--- a/src/annotations/blameAnnotationProvider.ts
+++ b/src/annotations/blameAnnotationProvider.ts
@@ -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;
- protected _hoverProviderDisposable: Disposable | undefined;
- protected readonly _uri: GitUri;
-
- constructor(
- editor: TextEditor,
- trackedDocument: TrackedDocument,
- 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;
+ protected hoverProviderDisposable: Disposable | undefined;
+
+ constructor(editor: TextEditor, trackedDocument: TrackedDocument) {
+ 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 {
- const blame = await this._blame;
+ const blame = await this.blame;
return blame != null && blame.lines.length !== 0;
}
protected async getBlame(): Promise {
- 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 {
- 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 {
- 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 {
- 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[]) {
diff --git a/src/annotations/fileAnnotationController.ts b/src/annotations/fileAnnotationController.ts
index 4aec22d..24c676a 100644
--- a/src/annotations/fileAnnotationController.ts
+++ b/src/annotations/fileAnnotationController.ts
@@ -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(
` `,
@@ -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(
- ` `,
- )}`,
- )
- : undefined;
+ Decorations.changesLineAddedAnnotation = window.createTextEditorDecorationType({
+ gutterIconPath: locations.includes(ChangesLocations.Gutter)
+ ? Uri.parse(
+ `data:image/svg+xml,${encodeURIComponent(
+ " ",
+ )}`,
+ )
+ : 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(
+ " ",
+ )}`,
+ )
+ : 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(
+ " ",
+ )}`,
+ )
+ : 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 {
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;
}
}
diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts
index 0a629db..5836fb6 100644
--- a/src/annotations/gutterBlameAnnotationProvider.ts
+++ b/src/annotations/gutterBlameAnnotationProvider.ts
@@ -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 {
const cc = Logger.getCorrelationContext();
@@ -53,7 +64,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase {
options,
);
- this.decorations = [];
+ const decorationOptions = [];
const decorationsMap = new Map();
const avatarDecorationsMap = avatars ? new Map() : 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,
diff --git a/src/annotations/gutterChangesAnnotationProvider.ts b/src/annotations/gutterChangesAnnotationProvider.ts
new file mode 100644
index 0000000..aaa63cd
--- /dev/null
+++ b/src/annotations/gutterChangesAnnotationProvider.ts
@@ -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) {
+ super(editor, trackedDocument);
+ }
+
+ clear() {
+ this.state = undefined;
+ if (this.hoverProviderDisposable != null) {
+ this.hoverProviderDisposable.dispose();
+ this.hoverProviderDisposable = undefined;
+ }
+ super.clear();
+ }
+
+ selection(_shaOrLine?: string | number): Promise {
+ return Promise.resolve();
+ }
+
+ validate(): Promise {
+ return Promise.resolve(true);
+ }
+
+ @log()
+ async onProvideAnnotation(shaOrLine?: string | number): Promise {
+ 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;
+ }
+}
diff --git a/src/annotations/heatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts
similarity index 74%
rename from src/annotations/heatmapBlameAnnotationProvider.ts
rename to src/annotations/gutterHeatmapBlameAnnotationProvider.ts
index 71f19cc..a6cb7c5 100644
--- a/src/annotations/heatmapBlameAnnotationProvider.ts
+++ b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts
@@ -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 {
const cc = Logger.getCorrelationContext();
@@ -20,7 +19,10 @@ export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase
let start = process.hrtime();
- const decorationsMap = new Map();
+ 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 {
+ return Promise.resolve();
+ }
}
diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts
deleted file mode 100644
index 0d3e9cc..0000000
--- a/src/annotations/recentChangesAnnotationProvider.ts
+++ /dev/null
@@ -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,
- decoration: TextEditorDecorationType,
- highlightDecoration: TextEditorDecorationType | undefined,
- ) {
- super(editor, trackedDocument, decoration, highlightDecoration);
-
- this._uri = trackedDocument.uri;
- }
-
- @log()
- async onProvideAnnotation(shaOrLine?: string | number): Promise {
- 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 {
- return Promise.resolve(undefined);
- }
-
- validate(): Promise {
- return Promise.resolve(true);
- }
-}
diff --git a/src/commands/common.ts b/src/commands/common.ts
index a304399..1affbd8 100644
--- a/src/commands/common.ts
+++ b/src/commands/common.ts
@@ -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',
diff --git a/src/commands/toggleFileAnnotations.ts b/src/commands/toggleFileAnnotations.ts
index 0459517..c9e2b3c 100644
--- a/src/commands/toggleFileAnnotations.ts
+++ b/src/commands/toggleFileAnnotations.ts
@@ -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 {
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 {
return toggleFileAnnotations(editor, uri, {
...args,
- type: FileAnnotationType.RecentChanges,
+ type: FileAnnotationType.Heatmap,
});
}
}
diff --git a/src/config.ts b/src/config.ts
index 9e8c591..0b01081 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -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;
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;
diff --git a/src/container.ts b/src/container.ts
index 3366b4b..59e6a44 100644
--- a/src/container.ts
+++ b/src/container.ts
@@ -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')}`,
diff --git a/src/extension.ts b/src/extension.ts
index e4a7c46..678e465 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -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;
});
diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts
index d2e0f78..c19f6b7 100644
--- a/src/git/formatters/commitFormatter.ts
+++ b/src/git/formatters/commitFormatter.ts
@@ -32,7 +32,6 @@ const emptyStr = '';
const hasTokenRegexMap = new Map();
export interface CommitFormatOptions extends FormatOptions {
- annotationType?: FileAnnotationType;
autolinkedIssuesOrPullRequests?: Map;
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}`;
}
diff --git a/src/git/git.ts b/src/git/git.ts
index 27f2355..6ab4b5c 100644
--- a/src/git/git.ts
+++ b/src/git/git.ts
@@ -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 {
+ 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)}`);
+ }
+
+ // // ^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(
+ {
+ 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)) {
diff --git a/src/git/gitService.ts b/src/git/gitService.ts
index b68f716..d3c3797 100644
--- a/src/git/gitService.ts
+++ b/src/git/gitService.ts
@@ -1192,6 +1192,17 @@ export class GitService implements Disposable {
}
@log()
+ async getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise {
+ 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 {
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,
+ errorMessage: msg,
+ };
+ document.state.set(key, value);
+
+ return emptyPromise as Promise;
+ }
+
+ return undefined;
+ }
+ }
+
+ @log({
+ args: {
+ 1: _contents => '',
+ },
+ })
+ async getDiffForFileContents(
+ uri: GitUri,
+ ref: string,
+ contents: string,
+ originalFileName?: string,
+ ): Promise {
+ 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(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,
+ };
+ doc.state.set(key, value);
+ }
+
+ return promise;
+ }
+
+ async getDiffForFileContentsCore(
+ repoPath: string | undefined,
+ fileName: string,
+ ref: string,
+ contents: string,
+ originalFileName: string | undefined,
+ options: { encoding?: string },
+ document: TrackedDocument,
+ key: string,
+ cc: LogCorrelationContext | undefined,
+ ): Promise {
+ 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;
diff --git a/src/git/models/diff.ts b/src/git/models/diff.ts
index 1930eff..e8cc438 100644
--- a/src/git/models/diff.ts
+++ b/src/git/models/diff.ts
@@ -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;
}
}
diff --git a/src/git/parsers/diffParser.ts b/src/git/parsers/diffParser.ts
index 7b4bfab..400c143 100644
--- a/src/git/parsers/diffParser.ts
+++ b/src/git/parsers/diffParser.ts
@@ -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 })
diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts
index 43c38d2..7036525 100644
--- a/src/git/parsers/logParser.ts
+++ b/src/git/parsers/logParser.ts
@@ -25,6 +25,7 @@ const fileStatusAndSummaryRegex = /^(\d+?|-)\s+?(\d+?|-)\s+?(.*)(?:\n\s(delete|r
const fileStatusAndSummaryRenamedFileRegex = /(.+)\s=>\s(.+)/;
const fileStatusAndSummaryRenamedFilePathRegex = /(.*?){(.+?)\s=>\s(.*?)}(.*)/;
+const logFileRefsRegex = /^ (.*)/gm;
const logFileSimpleRegex = /^ (.*)\s*(?:(?:diff --git a\/(.*) b\/(.*))|(?:(\S)\S*\t([^\t\n]+)(?:\t(.+))?))/gm;
const logFileSimpleRenamedRegex = /^ (\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,
diff --git a/src/hovers/hovers.ts b/src/hovers/hovers.ts
index 0bc1d73..3b1979f 100644
--- a/src/hovers/hovers.ts
+++ b/src/hovers/hovers.ts
@@ -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;
- export async function changesMessage(
- commit: GitLogCommit,
- uri: GitUri,
- editorLine: number,
- hunkLine: GitDiffHunkLine,
- ): Promise;
- export async function changesMessage(
commit: GitBlameCommit | GitLogCommit,
uri: GitUri,
editorLine: number,
- hunkLine?: GitDiffHunkLine,
): Promise {
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 += ` ${GlyphChars.Dash} ${previous} ${GlyphChars.ArrowLeftRightLong} ${current}\n${diff}`;
+ message = `${diff}\n---\n\nChanges ${previous} ${GlyphChars.ArrowLeftRightLong} ${current} | ${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 ${previous} ${
+ GlyphChars.ArrowLeftRightLong
+ } ${current}${message == null ? '' : ` | ${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 {
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;
}
}
diff --git a/src/hovers/lineHoverController.ts b/src/hovers/lineHoverController.ts
index 3c5505d..b298e64 100644
--- a/src/hovers/lineHoverController.ts
+++ b/src/hovers/lineHoverController.ts
@@ -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';
diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts
index 9d7c4b1..5a79e09 100644
--- a/src/views/nodes/viewNode.ts
+++ b/src/views/nodes/viewNode.ts
@@ -58,6 +58,10 @@ export interface ViewNode {
@logName((c, name) => `${name}${c.id != null ? `(${c.id})` : ''}`)
export abstract class ViewNode {
+ 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;
}
diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts
index 00e7514..a75b97f 100644
--- a/src/views/viewCommands.ts
+++ b/src/views/viewCommands.ts
@@ -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,
));
diff --git a/src/webviews/apps/settings/partials/blame.ejs b/src/webviews/apps/settings/partials/blame.ejs
index 950bff8..bc40050 100644
--- a/src/webviews/apps/settings/partials/blame.ejs
+++ b/src/webviews/apps/settings/partials/blame.ejs
@@ -136,7 +136,7 @@
data-setting-type="array"
disabled
/>
- Add gutter highlight
+ Add gutter indicator
@@ -166,7 +166,7 @@
data-setting-type="array"
disabled
/>
- Add scroll bar highlight
+ Add scroll bar indicator
diff --git a/src/webviews/apps/settings/partials/recent-changes.ejs b/src/webviews/apps/settings/partials/changes.ejs
similarity index 51%
rename from src/webviews/apps/settings/partials/recent-changes.ejs
rename to src/webviews/apps/settings/partials/changes.ejs
index 60c65f3..006cde5 100644
--- a/src/webviews/apps/settings/partials/recent-changes.ejs
+++ b/src/webviews/apps/settings/partials/changes.ejs
@@ -1,7 +1,7 @@
-