From 48a1ca704d94cb01fc8e47d2c65d806f10925247 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 10 Jun 2017 04:19:07 -0400 Subject: [PATCH] Adds new recent changes annotations --- README.md | 16 +++ package.json | 39 +++++++ src/annotations/annotationController.ts | 101 ++++++++++++++---- src/annotations/annotationProvider.ts | 19 ++-- src/annotations/blameAnnotationProvider.ts | 3 +- src/annotations/diffAnnotationProvider.ts | 69 ------------ .../gutterBlameAnnotationProvider.ts | 4 +- .../hoverBlameAnnotationProvider.ts | 4 +- .../recentChangesAnnotationProvider.ts | 61 +++++++++++ src/commands.ts | 1 + src/commands/common.ts | 2 + src/commands/showFileBlame.ts | 4 +- src/commands/showLineBlame.ts | 4 +- src/commands/toggleFileBlame.ts | 4 +- src/commands/toggleFileRecentChanges.ts | 24 +++++ src/commands/toggleLineBlame.ts | 4 +- src/configuration.ts | 41 +++---- src/currentLineController.ts | 24 ++++- src/extension.ts | 11 +- 19 files changed, 299 insertions(+), 136 deletions(-) delete mode 100644 src/annotations/diffAnnotationProvider.ts create mode 100644 src/annotations/recentChangesAnnotationProvider.ts create mode 100644 src/commands/toggleFileRecentChanges.ts diff --git a/README.md b/README.md index 55e48654a30f6..c5d144a082263 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, - Adds a `Toggle Line Blame Annotations` command (`gitlens.toggleLineBlame`) to toggle the current line blame annotations on and off - Also adds a `Show Line Blame Annotations` command (`gitlens.showLineBlame`) +### Git Recent Changes Annotations + +- Adds on-demand, [customizable](#file-recent-changes-annotation-settings) and [themeable](#theme-settings), **recent changes annotations** of the whole file + - Highlights all of lines changed in the most recent commit + - Also adds a `changes` (diff) hover annotation to the current line annotation which provides **instant** access to the line's previous version ([optional](#file-recent-changes-annotation-settings), on by default) + +- Adds `Toggle Recent File Changes Annotations` command (`gitlens.toggleFileRecentChanges`) to toggle the recent changes annotations on and off + ### Git Code Lens - Adds **code lens** to the top of the file and on code blocks ([optional](#code-lens-settings), on by default) @@ -244,6 +252,14 @@ GitLens is highly customizable and provides many configuration settings to allow |`gitlens.annotations.line.hover.details`|Specifies whether or not to provide a commit details hover annotation for the current line |`gitlens.annotations.line.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotation for the current line +### File Recent Changes Annotation Settings + +|Name | Description +|-----|------------ +|`gitlens.recentChanges.file.lineHighlight.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
`overviewRuler` - adds a decoration to the overviewRuler (scroll bar) +|`gitlens.annotations.file.recentChanges.hover.changes`|Specifies whether or not to provide a changes (diff) hover annotations +|`gitlens.annotations.file.recentChanges.hover.wholeLine`|Specifies whether or not to trigger hover annotations over the whole line + ### Code Lens Settings |Name | Description diff --git a/package.json b/package.json index 7f2272961eb8e..9de26a7a3538d 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,16 @@ "default": true, "description": "Specifies whether or not to trigger hover annotations over the whole line" }, + "gitlens.annotations.file.recentChanges.hover.changes": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to provide a changes (diff) hover annotations" + }, + "gitlens.annotations.file.recentChanges.hover.wholeLine": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to trigger hover annotations over the whole line" + }, "gitlens.annotations.line.hover.details": { "type": "boolean", "default": true, @@ -205,6 +215,26 @@ ], "description": "Specifies the type of blame annotations that will be shown for the current line\n `trailing` - adds an annotation to the end of the current line\n `hover` - shows annotations when hovering over the current line" }, + "gitlens.recentChanges.file.lineHighlight.locations": { + "type": "array", + "default": [ + "gutter", + "line", + "overviewRuler" + ], + "items": { + "type": "string", + "enum": [ + "gutter", + "line", + "overviewRuler" + ] + }, + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "description": "Specifies where the highlights of the recently changed lines will be shown\n `gutter` - adds a gutter glyph\n `line` - adds a full-line highlight background color\n `overviewRuler` - adds a decoration to the overviewRuler (scroll bar)" + }, "gitlens.codeLens.enabled": { "type": "boolean", "default": true, @@ -757,6 +787,11 @@ "dark": "images/git-icon-dark.svg", "light": "images/git-icon-light.svg" } + }, + { + "command": "gitlens.toggleFileRecentChanges", + "title": "Toggle Recent File Changes Annotations", + "category": "GitLens" }, { "command": "gitlens.toggleLineBlame", @@ -920,6 +955,10 @@ { "command": "gitlens.toggleFileBlame", "when": "gitlens:isBlameable" + }, + { + "command": "gitlens.toggleFileRecentChanges", + "when": "gitlens:isTracked" }, { "command": "gitlens.toggleLineBlame", diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts index 2e15e7f9f08ea..d218d03c9a3fd 100644 --- a/src/annotations/annotationController.ts +++ b/src/annotations/annotationController.ts @@ -3,19 +3,29 @@ import { Functions, Objects } from '../system'; import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; import { AnnotationProviderBase } from './annotationProvider'; import { TextDocumentComparer, TextEditorComparer } from '../comparers'; -import { BlameLineHighlightLocations, ExtensionKey, FileAnnotationType, IConfig, themeDefaults } from '../configuration'; +import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration'; import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; import { Logger } from '../logger'; +import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider'; import { WhitespaceController } from './whitespaceController'; +export type FileAnnotationType = 'gutter' | 'hover' | 'recentChanges'; +export const FileAnnotationType = { + Gutter: 'gutter' as FileAnnotationType, + Hover: 'hover' as FileAnnotationType, + RecentChanges: 'recentChanges' as FileAnnotationType +}; + export const Decorations = { - annotation: window.createTextEditorDecorationType({ + blameAnnotation: window.createTextEditorDecorationType({ isWholeLine: true, textDecoration: 'none' } as DecorationRenderOptions), - highlight: undefined as TextEditorDecorationType | undefined + blameHighlight: undefined as TextEditorDecorationType | undefined, + recentChangesAnnotation: undefined as TextEditorDecorationType | undefined, + recentChangesHighlight: undefined as TextEditorDecorationType | undefined }; export class AnnotationController extends Disposable { @@ -46,8 +56,8 @@ export class AnnotationController extends Disposable { dispose() { this._annotationProviders.forEach(async (p, i) => await this.clear(i)); - Decorations.annotation && Decorations.annotation.dispose(); - Decorations.highlight && Decorations.highlight.dispose(); + Decorations.blameAnnotation && Decorations.blameAnnotation.dispose(); + Decorations.blameHighlight && Decorations.blameHighlight.dispose(); this._annotationsDisposable && this._annotationsDisposable.dispose(); this._whitespaceController && this._whitespaceController.dispose(); @@ -82,50 +92,83 @@ export class AnnotationController extends Disposable { } const cfg = workspace.getConfiguration().get(ExtensionKey)!; - const cfgHighlight = cfg.blame.file.lineHighlight; + const cfgBlameHighlight = cfg.blame.file.lineHighlight; + const cfgChangesHighlight = cfg.recentChanges.file.lineHighlight; const cfgTheme = cfg.theme.lineHighlight; - if (!Objects.areEquivalent(cfgHighlight, this._config && this._config.blame.file.lineHighlight) || + if (!Objects.areEquivalent(cfgBlameHighlight, this._config && this._config.blame.file.lineHighlight) || + !Objects.areEquivalent(cfgChangesHighlight, this._config && this._config.recentChanges.file.lineHighlight) || !Objects.areEquivalent(cfgTheme, this._config && this._config.theme.lineHighlight)) { changed = true; - Decorations.highlight && Decorations.highlight.dispose(); + Decorations.blameHighlight && Decorations.blameHighlight.dispose(); - if (cfgHighlight.enabled) { - Decorations.highlight = window.createTextEditorDecorationType({ + if (cfgBlameHighlight.enabled) { + Decorations.blameHighlight = window.createTextEditorDecorationType({ gutterIconSize: 'contain', isWholeLine: true, overviewRulerLane: OverviewRulerLane.Right, dark: { - backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line) + backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line) ? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor : undefined, - gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter) + gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/blame-dark.svg') : undefined, - overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler) + overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler) ? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor : undefined }, light: { - backgroundColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.Line) + backgroundColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.Line) ? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor : undefined, - gutterIconPath: cfgHighlight.locations.includes(BlameLineHighlightLocations.Gutter) + gutterIconPath: cfgBlameHighlight.locations.includes(LineHighlightLocations.Gutter) ? this.context.asAbsolutePath('images/blame-light.svg') : undefined, - overviewRulerColor: cfgHighlight.locations.includes(BlameLineHighlightLocations.OverviewRuler) + overviewRulerColor: cfgBlameHighlight.locations.includes(LineHighlightLocations.OverviewRuler) ? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor : undefined } }); } else { - Decorations.highlight = undefined; + Decorations.blameHighlight = undefined; } + + Decorations.recentChangesHighlight && Decorations.recentChangesHighlight.dispose(); + + Decorations.recentChangesHighlight = window.createTextEditorDecorationType({ + gutterIconSize: 'contain', + isWholeLine: true, + overviewRulerLane: OverviewRulerLane.Right, + dark: { + backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.dark.backgroundColor || themeDefaults.lineHighlight.dark.backgroundColor + : undefined, + gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter) + ? this.context.asAbsolutePath('images/blame-dark.svg') + : undefined, + overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.dark.overviewRulerColor || themeDefaults.lineHighlight.dark.overviewRulerColor + : undefined + }, + light: { + backgroundColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.Line) + ? cfgTheme.light.backgroundColor || themeDefaults.lineHighlight.light.backgroundColor + : undefined, + gutterIconPath: cfgChangesHighlight.locations.includes(LineHighlightLocations.Gutter) + ? this.context.asAbsolutePath('images/blame-light.svg') + : undefined, + overviewRulerColor: cfgChangesHighlight.locations.includes(LineHighlightLocations.OverviewRuler) + ? cfgTheme.light.overviewRulerColor || themeDefaults.lineHighlight.light.overviewRulerColor + : undefined + } + }); } if (!Objects.areEquivalent(cfg.blame.file, this._config && this._config.blame.file) || + !Objects.areEquivalent(cfg.recentChanges.file, this._config && this._config.recentChanges.file) || !Objects.areEquivalent(cfg.annotations, this._config && this._config.annotations) || !Objects.areEquivalent(cfg.theme.annotations, this._config && this._config.theme.annotations)) { changed = true; @@ -138,7 +181,12 @@ export class AnnotationController extends Disposable { for (const provider of this._annotationProviders.values()) { if (provider === undefined) continue; - provider.reset(this._whitespaceController); + if (provider.annotationType === FileAnnotationType.RecentChanges) { + provider.reset(Decorations.recentChangesAnnotation, Decorations.recentChangesHighlight); + } + else { + provider.reset(Decorations.blameAnnotation, Decorations.blameHighlight, this._whitespaceController); + } } } } @@ -184,10 +232,15 @@ export class AnnotationController extends Disposable { let provider: AnnotationProviderBase | undefined = undefined; switch (type) { case FileAnnotationType.Gutter: - provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri); + provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this._whitespaceController, this.git, gitUri); break; + case FileAnnotationType.Hover: - provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.annotation, Decorations.highlight, this._whitespaceController, this.git, gitUri); + provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this._whitespaceController, this.git, gitUri); + break; + + case FileAnnotationType.RecentChanges: + provider = new RecentChangesAnnotationProvider(this.context, editor, undefined, Decorations.recentChangesHighlight!, this.git, gitUri); break; } if (provider === undefined || !(await provider.validate())) return false; @@ -219,13 +272,17 @@ export class AnnotationController extends Disposable { } async toggleAnnotations(editor: TextEditor, type: FileAnnotationType, shaOrLine?: string | number): Promise { - if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false; + if (!editor || !editor.document || type === FileAnnotationType.RecentChanges ? !this.git.isTrackable(editor.document.uri) : !this.git.isEditorBlameable(editor)) return false; const provider = this._annotationProviders.get(editor.viewColumn || -1); if (provider === undefined) return this.showAnnotations(editor, type, shaOrLine); + const reopen = provider.annotationType !== type; await this.clear(provider.editor.viewColumn || -1); - return false; + + if (!reopen) return false; + + return this.showAnnotations(editor, type, shaOrLine); } private _onBlameabilityChanged(e: BlameabilityChangeEvent) { diff --git a/src/annotations/annotationProvider.ts b/src/annotations/annotationProvider.ts index a4e013e316254..0a8be81a534d8 100644 --- a/src/annotations/annotationProvider.ts +++ b/src/annotations/annotationProvider.ts @@ -1,8 +1,9 @@ 'use strict'; import { Functions } from '../system'; import { Disposable, ExtensionContext, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { FileAnnotationType } from '../annotations/annotationController'; import { TextDocumentComparer } from '../comparers'; -import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { ExtensionKey, IConfig } from '../configuration'; import { WhitespaceController } from './whitespaceController'; export abstract class AnnotationProviderBase extends Disposable { @@ -13,7 +14,7 @@ import { WhitespaceController } from './whitespaceController'; protected _config: IConfig; protected _disposable: Disposable; - constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) { + constructor(context: ExtensionContext, public editor: TextEditor, protected decoration: TextEditorDecorationType | undefined, protected highlightDecoration: TextEditorDecorationType | undefined, protected whitespaceController: WhitespaceController | undefined) { super(() => this.dispose()); this.document = this.editor.document; @@ -42,10 +43,14 @@ import { WhitespaceController } from './whitespaceController'; async clear() { if (this.editor !== undefined) { try { - this.editor.setDecorations(this.decoration, []); - this.highlightDecoration && this.editor.setDecorations(this.highlightDecoration, []); - // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay + if (this.decoration !== undefined) { + this.editor.setDecorations(this.decoration, []); + } + if (this.highlightDecoration !== undefined) { + this.editor.setDecorations(this.highlightDecoration, []); + + // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay await Functions.wait(1); if (this.highlightDecoration === undefined) return; @@ -60,10 +65,12 @@ import { WhitespaceController } from './whitespaceController'; this.whitespaceController && await this.whitespaceController.restore(); } - async reset(whitespaceController: WhitespaceController | undefined) { + async reset(decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController?: WhitespaceController) { await this.clear(); this._config = workspace.getConfiguration().get(ExtensionKey)!; + this.decoration = decoration; + this.highlightDecoration = highlightDecoration; this.whitespaceController = whitespaceController; await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line); diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index ef7736400faeb..d69c906524236 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -9,7 +9,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase protected _blame: Promise; - constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) { + constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, protected git: GitService, protected uri: GitUri) { super(context, editor, decoration, highlightDecoration, whitespaceController); this._blame = this.git.getBlameForFile(this.uri); @@ -56,7 +56,6 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase const blame = await this._blame; return blame !== undefined && blame.lines.length !== 0; } - protected async getBlame(requiresWhitespaceHack: boolean): Promise { let whitespacePromise: Promise | undefined; // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- override whitespace (turn off) diff --git a/src/annotations/diffAnnotationProvider.ts b/src/annotations/diffAnnotationProvider.ts deleted file mode 100644 index 4baf345c6ed39..0000000000000 --- a/src/annotations/diffAnnotationProvider.ts +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; -import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; -import { AnnotationProviderBase } from './annotationProvider'; -import { GitService, GitUri } from '../gitService'; -import { WhitespaceController } from './whitespaceController'; - -export class DiffAnnotationProvider extends AnnotationProviderBase { - - constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType, highlightDecoration: TextEditorDecorationType | undefined, whitespaceController: WhitespaceController | undefined, private git: GitService, private uri: GitUri) { - super(context, editor, decoration, highlightDecoration, whitespaceController); - } - - async provideAnnotation(shaOrLine?: string | number): Promise { - // let sha1: string | undefined = undefined; - // let sha2: string | undefined = undefined; - // if (shaOrLine === undefined) { - // const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); - // if (commit === undefined) return false; - - // sha1 = commit.previousSha; - // } - // else if (typeof shaOrLine === 'string') { - // sha1 = shaOrLine; - // } - // else { - // const blame = await this.git.getBlameForLine(this.uri, shaOrLine); - // if (blame === undefined) return false; - - // sha1 = blame.commit.previousSha; - // sha2 = blame.commit.sha; - // } - - // if (sha1 === undefined) return false; - - const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); - if (commit === undefined) return false; - - const diff = await this.git.getDiffForFile(this.uri, commit.previousSha); - if (diff === undefined) return false; - - const decorators: DecorationOptions[] = []; - - for (const chunk of diff.chunks) { - let count = chunk.currentPosition.start - 2; - for (const change of chunk.current) { - if (change === undefined) continue; - - count++; - - if (change.state === 'unchanged') continue; - - decorators.push({ - range: new Range(new Position(count, 0), new Position(count, 0)) - } as DecorationOptions); - } - } - - this.editor.setDecorations(this.decoration, decorators); - - return true; - } - - async selection(shaOrLine?: string | number): Promise { - } - - async validate(): Promise { - return true; - } -} \ No newline at end of file diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index a125438edcfaa..cda574411b090 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -1,9 +1,9 @@ 'use strict'; import { Strings } from '../system'; import { DecorationOptions, Range } from 'vscode'; +import { FileAnnotationType } from './annotationController'; import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; import { Annotations, endOfLineIndex } from './annotations'; -import { FileAnnotationType } from '../configuration'; import { ICommitFormatOptions } from '../gitService'; import * as moment from 'moment'; @@ -67,7 +67,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { } if (decorations.length) { - this.editor.setDecorations(this.decoration, decorations); + this.editor.setDecorations(this.decoration!, decorations); } this.selection(shaOrLine, blame); diff --git a/src/annotations/hoverBlameAnnotationProvider.ts b/src/annotations/hoverBlameAnnotationProvider.ts index 64694f0b98d09..53166b0e59cf5 100644 --- a/src/annotations/hoverBlameAnnotationProvider.ts +++ b/src/annotations/hoverBlameAnnotationProvider.ts @@ -1,8 +1,8 @@ 'use strict'; import { DecorationOptions, Range } from 'vscode'; +import { FileAnnotationType } from './annotationController'; import { BlameAnnotationProviderBase } from './blameAnnotationProvider'; import { Annotations, endOfLineIndex } from './annotations'; -import { FileAnnotationType } from '../configuration'; import * as moment from 'moment'; export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { @@ -40,7 +40,7 @@ export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { } if (decorations.length) { - this.editor.setDecorations(this.decoration, decorations); + this.editor.setDecorations(this.decoration!, decorations); } this.selection(shaOrLine, blame); diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts new file mode 100644 index 0000000000000..5b596fe05109a --- /dev/null +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -0,0 +1,61 @@ +'use strict'; +import { DecorationOptions, ExtensionContext, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; +import { endOfLineIndex } from './annotations'; +import { FileAnnotationType } from './annotationController'; +import { AnnotationProviderBase } from './annotationProvider'; +import { CommitFormatter, GitService, GitUri } from '../gitService'; + +export class RecentChangesAnnotationProvider extends AnnotationProviderBase { + + constructor(context: ExtensionContext, editor: TextEditor, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, private git: GitService, private uri: GitUri) { + super(context, editor, decoration, highlightDecoration, undefined); + } + + async provideAnnotation(shaOrLine?: string | number): Promise { + this.annotationType = FileAnnotationType.RecentChanges; + + const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); + if (commit === undefined) return false; + + const diff = await this.git.getDiffForFile(this.uri, commit.previousSha); + if (diff === undefined) return false; + + const cfg = this._config.annotations.file.recentChanges; + + const decorators: DecorationOptions[] = []; + + for (const chunk of diff.chunks) { + let count = chunk.currentPosition.start - 2; + for (const change of chunk.current) { + if (change === undefined) continue; + + count++; + + if (change.state === 'unchanged') continue; + + let endingIndex = 0; + let message: string | undefined = undefined; + if (cfg.hover.changes) { + message = CommitFormatter.toHoverDiff(commit, chunk.previous[count], change); + endingIndex = cfg.hover.wholeLine ? endOfLineIndex : this.editor.document.lineAt(count).firstNonWhitespaceCharacterIndex; + } + + decorators.push({ + hoverMessage: message, + range: this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endingIndex))) + } as DecorationOptions); + } + } + + this.editor.setDecorations(this.highlightDecoration!, decorators); + + return true; + } + + async selection(shaOrLine?: string | number): Promise { + } + + async validate(): Promise { + return true; + } +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 88aa41d65e3ce..7fce9d4aecd08 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -38,4 +38,5 @@ export * from './commands/stashDelete'; export * from './commands/stashSave'; export * from './commands/toggleCodeLens'; export * from './commands/toggleFileBlame'; +export * from './commands/toggleFileRecentChanges'; export * from './commands/toggleLineBlame'; \ No newline at end of file diff --git a/src/commands/common.ts b/src/commands/common.ts index 7d326bf4201d2..85a65f17a6da7 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -39,6 +39,7 @@ export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.stashSave' | 'gitlens.toggleCodeLens' | 'gitlens.toggleFileBlame' | + 'gitlens.toggleFileRecentChanges' | 'gitlens.toggleLineBlame'; export const Commands = { CloseUnchangedFiles: 'gitlens.closeUnchangedFiles' as Commands, @@ -76,6 +77,7 @@ export const Commands = { StashSave: 'gitlens.stashSave' as Commands, ToggleCodeLens: 'gitlens.toggleCodeLens' as Commands, ToggleFileBlame: 'gitlens.toggleFileBlame' as Commands, + ToggleFileRecentChanges: 'gitlens.toggleFileRecentChanges' as Commands, ToggleLineBlame: 'gitlens.toggleLineBlame' as Commands }; diff --git a/src/commands/showFileBlame.ts b/src/commands/showFileBlame.ts index d18d24cff217f..1da2dbe36ec8c 100644 --- a/src/commands/showFileBlame.ts +++ b/src/commands/showFileBlame.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; -import { AnnotationController } from '../annotations/annotationController'; +import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { Commands, EditorCommand } from './common'; -import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { ExtensionKey, IConfig } from '../configuration'; import { Logger } from '../logger'; export interface ShowFileBlameCommandArgs { diff --git a/src/commands/showLineBlame.ts b/src/commands/showLineBlame.ts index 340a5594d98e7..911c148e441c0 100644 --- a/src/commands/showLineBlame.ts +++ b/src/commands/showLineBlame.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; -import { CurrentLineController } from '../currentLineController'; +import { CurrentLineController, LineAnnotationType } from '../currentLineController'; import { Commands, EditorCommand } from './common'; -import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration'; +import { ExtensionKey, IConfig } from '../configuration'; import { Logger } from '../logger'; export interface ShowLineBlameCommandArgs { diff --git a/src/commands/toggleFileBlame.ts b/src/commands/toggleFileBlame.ts index 901d1d2c4a013..e0e1eb8e0107c 100644 --- a/src/commands/toggleFileBlame.ts +++ b/src/commands/toggleFileBlame.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; -import { AnnotationController } from '../annotations/annotationController'; +import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { Commands, EditorCommand } from './common'; -import { ExtensionKey, FileAnnotationType, IConfig } from '../configuration'; +import { ExtensionKey, IConfig } from '../configuration'; import { Logger } from '../logger'; export interface ToggleFileBlameCommandArgs { diff --git a/src/commands/toggleFileRecentChanges.ts b/src/commands/toggleFileRecentChanges.ts new file mode 100644 index 0000000000000..d744f5555a216 --- /dev/null +++ b/src/commands/toggleFileRecentChanges.ts @@ -0,0 +1,24 @@ +'use strict'; +import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; +import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; +import { Commands, EditorCommand } from './common'; +import { Logger } from '../logger'; + +export class ToggleFileRecentChangesCommand extends EditorCommand { + + constructor(private annotationController: AnnotationController) { + super(Commands.ToggleFileRecentChanges); + } + + async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { + if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; + + try { + return this.annotationController.toggleAnnotations(editor, FileAnnotationType.RecentChanges); + } + catch (ex) { + Logger.error(ex, 'ToggleFileRecentChangesCommand'); + return window.showErrorMessage(`Unable to toggle recent file changes annotations. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/commands/toggleLineBlame.ts b/src/commands/toggleLineBlame.ts index 3742ddd01f69e..45456b0b90207 100644 --- a/src/commands/toggleLineBlame.ts +++ b/src/commands/toggleLineBlame.ts @@ -1,8 +1,8 @@ 'use strict'; import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; -import { CurrentLineController } from '../currentLineController'; +import { CurrentLineController, LineAnnotationType } from '../currentLineController'; import { Commands, EditorCommand } from './common'; -import { ExtensionKey, IConfig, LineAnnotationType } from '../configuration'; +import { ExtensionKey, IConfig } from '../configuration'; import { Logger } from '../logger'; export interface ToggleLineBlameCommandArgs { diff --git a/src/configuration.ts b/src/configuration.ts index f9b137e27b216..ba3bcdf6541e7 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,16 +1,11 @@ 'use strict'; +import { FileAnnotationType } from './annotations/annotationController'; import { Commands } from './commands'; +import { LineAnnotationType } from './currentLineController'; import { OutputLevel } from './logger'; export { ExtensionKey } from './constants'; -export type BlameLineHighlightLocations = 'gutter' | 'line' | 'overviewRuler'; -export const BlameLineHighlightLocations = { - Gutter: 'gutter' as BlameLineHighlightLocations, - Line: 'line' as BlameLineHighlightLocations, - OverviewRuler: 'overviewRuler' as BlameLineHighlightLocations -}; - export type CodeLensCommand = 'gitlens.toggleFileBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | @@ -38,16 +33,11 @@ export const CodeLensLocations = { Custom: 'custom' as CodeLensLocations }; -export type FileAnnotationType = 'gutter' | 'hover'; -export const FileAnnotationType = { - Gutter: 'gutter' as FileAnnotationType, - Hover: 'hover' as FileAnnotationType -}; - -export type LineAnnotationType = 'trailing' | 'hover'; -export const LineAnnotationType = { - Trailing: 'trailing' as LineAnnotationType, - Hover: 'hover' as LineAnnotationType +export type LineHighlightLocations = 'gutter' | 'line' | 'overviewRuler'; +export const LineHighlightLocations = { + Gutter: 'gutter' as LineHighlightLocations, + Line: 'line' as LineHighlightLocations, + OverviewRuler: 'overviewRuler' as LineHighlightLocations }; export type StatusBarCommand = 'gitlens.toggleFileBlame' | @@ -244,6 +234,13 @@ export interface IConfig { }; wholeLine: boolean; }; + + recentChanges: { + hover: { + changes: boolean; + wholeLine: boolean; + }; + }; }; line: { @@ -269,7 +266,7 @@ export interface IConfig { annotationType: FileAnnotationType; lineHighlight: { enabled: boolean; - locations: BlameLineHighlightLocations[]; + locations: LineHighlightLocations[]; }; }; @@ -279,6 +276,14 @@ export interface IConfig { }; }; + recentChanges: { + file: { + lineHighlight: { + locations: LineHighlightLocations[]; + }; + } + }; + codeLens: { enabled: boolean; recentChange: { diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 9b4b2a17f0382..d9256a612cf30 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -1,11 +1,11 @@ 'use strict'; import { Functions, Objects } from './system'; import { DecorationOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; -import { AnnotationController } from './annotations/annotationController'; +import { AnnotationController, FileAnnotationType } from './annotations/annotationController'; import { Annotations, endOfLineIndex } from './annotations/annotations'; import { Commands } from './commands'; import { TextEditorComparer } from './comparers'; -import { FileAnnotationType, IConfig, LineAnnotationType, StatusBarCommand } from './configuration'; +import { IConfig, StatusBarCommand } from './configuration'; import { DocumentSchemes, ExtensionKey } from './constants'; import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri } from './gitService'; @@ -16,6 +16,12 @@ const annotationDecoration: TextEditorDecorationType = window.createTextEditorDe } } as DecorationRenderOptions); +export type LineAnnotationType = 'trailing' | 'hover'; +export const LineAnnotationType = { + Trailing: 'trailing' as LineAnnotationType, + Hover: 'hover' as LineAnnotationType +}; + export class CurrentLineController extends Disposable { private _activeEditorLineDisposable: Disposable | undefined; @@ -351,6 +357,20 @@ export class CurrentLineController extends Disposable { } } + break; + } + case FileAnnotationType.RecentChanges: { + const cfgChanges = this._config.annotations.file.recentChanges.hover; + if (cfgChanges.changes) { + if (cfgChanges.wholeLine) { + // Avoid double annotations if we are showing the whole-file hover blame annotations + showChanges = false; + } + else { + showChangesInStartingWhitespace = false; + } + } + break; } } diff --git a/src/extension.ts b/src/extension.ts index ddb778002bdbc..9b2bf0652f9b3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,7 +8,7 @@ import { OpenBranchInRemoteCommand, OpenCommitInRemoteCommand, OpenFileInRemoteC import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './commands'; import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; import { ResetSuppressedWarningsCommand } from './commands'; -import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleLineBlameCommand } from './commands'; +import { ShowFileBlameCommand, ShowLineBlameCommand, ToggleFileBlameCommand, ToggleFileRecentChangesCommand, ToggleLineBlameCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; import { ShowLastQuickPickCommand } from './commands'; import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands'; @@ -17,9 +17,9 @@ import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './command import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands'; import { ToggleCodeLensCommand } from './commands'; import { Keyboard } from './commands'; -import { BlameLineHighlightLocations, CodeLensLocations, IConfig, LineAnnotationType } from './configuration'; +import { CodeLensLocations, IConfig, LineHighlightLocations } from './configuration'; import { ApplicationInsightsKey, ExtensionKey, QualifiedExtensionId, WorkspaceState } from './constants'; -import { CurrentLineController } from './currentLineController'; +import { CurrentLineController, LineAnnotationType } from './currentLineController'; import { GitContentProvider } from './gitContentProvider'; import { GitContextTracker, GitService } from './gitService'; import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider'; @@ -107,6 +107,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new ShowFileBlameCommand(annotationController)); context.subscriptions.push(new ShowLineBlameCommand(currentLineController)); context.subscriptions.push(new ToggleFileBlameCommand(annotationController)); + context.subscriptions.push(new ToggleFileRecentChangesCommand(annotationController)); context.subscriptions.push(new ToggleLineBlameCommand(currentLineController)); context.subscriptions.push(new ResetSuppressedWarningsCommand(context)); context.subscriptions.push(new ShowBlameHistoryCommand(git)); @@ -166,10 +167,10 @@ async function migrateSettings(context: ExtensionContext) { await cfg.update('blame.file.lineHighlight.enabled', false); break; case 'gutter': - await cfg.update('blame.file.lineHighlight.locations', [BlameLineHighlightLocations.Gutter, BlameLineHighlightLocations.OverviewRuler], true); + await cfg.update('blame.file.lineHighlight.locations', [LineHighlightLocations.Gutter, LineHighlightLocations.OverviewRuler], true); break; case 'line': - await cfg.update('blame.file.lineHighlight.locations', [BlameLineHighlightLocations.Line, BlameLineHighlightLocations.OverviewRuler], true); + await cfg.update('blame.file.lineHighlight.locations', [LineHighlightLocations.Line, LineHighlightLocations.OverviewRuler], true); break; case 'both': }