From 1519898dfacbc79e2da7a58a09d77b8f3864f731 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 3 Mar 2017 03:32:31 -0500 Subject: [PATCH] Changes behaviors when file has unsaved changes: - Status bar blame information will hide - CodeLens change to a `Cannot determine...` message and become unclickable - Many menu choices and commands will hide Fixes #36 - Blame information is invalid when a file has unsaved changes Fixed #38 - Toggle Blame Annotation button shows even when it isn't valid Preps v2.9.0 --- .vscode/tasks.json | 3 +- CHANGELOG.md | 8 +++ package.json | 61 ++++++++++++---------- src/blameActiveLineController.ts | 95 +++++++++++++++++----------------- src/blameAnnotationController.ts | 55 +++++++++++++------- src/blameAnnotationProvider.ts | 21 +++++--- src/blameabilityTracker.ts | 95 ++++++++++++++++++++++++++++++++++ src/commands/copyMessageToClipboard.ts | 4 +- src/commands/copyShaToClipboard.ts | 4 +- src/commands/diffLineWithPrevious.ts | 6 ++- src/commands/diffLineWithWorking.ts | 6 ++- src/extension.ts | 8 ++- src/gitCodeLensProvider.ts | 39 +++++++++++--- src/gitProvider.ts | 29 ++++++++--- 14 files changed, 309 insertions(+), 125 deletions(-) create mode 100644 src/blameabilityTracker.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8106d70..ac6c666 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,8 +8,7 @@ // A task runner that calls a custom npm script that compiles the extension. { - "version": "0.1.0", - "_runner": "terminal", + "version": "2.0.0", "command": "npm", "args": ["run"], "isShellCommand": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index b7dfa8a..e615737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Release Notes +### 2.9.0 +- To accomodate the realization that blame information is invalid when a file has unsaved changes, the following behavior changes have been made + - Status bar blame information will hide + - CodeLens change to a `Cannot determine...` message and become unclickable + - Many menu choices and commands will hide +- Fixes [#38](https://github.com/eamodio/vscode-gitlens/issues/38) - Toggle Blame Annotation button shows even when it isn't valid +- Fixes [#36](https://github.com/eamodio/vscode-gitlens/issues/36) - Blame information is invalid when a file has unsaved changes + ### 2.8.2 - Adds `gitlens.blame.annotation.dateFormat` to specify how absolute commit dates will be shown in the blame annotations - Adds `gitlens.statusBar.date` to specify whether and how the commit date will be shown in the blame status bar diff --git a/package.json b/package.json index 8cac4f4..a089e50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gitlens", - "version": "2.8.2", + "version": "2.9.0", "author": { "name": "Eric Amodio", "email": "eamodio@gmail.com" @@ -444,7 +444,7 @@ }, { "command": "gitlens.diffLineWithPrevious", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.diffWithWorking", @@ -452,15 +452,15 @@ }, { "command": "gitlens.diffLineWithWorking", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.showBlame", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.toggleBlame", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.toggleCodeLens", @@ -468,7 +468,7 @@ }, { "command": "gitlens.showBlameHistory", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.showFileHistory", @@ -476,7 +476,7 @@ }, { "command": "gitlens.showQuickCommitDetails", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.showQuickFileHistory", @@ -492,11 +492,11 @@ }, { "command": "gitlens.copyShaToClipboard", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" }, { "command": "gitlens.copyMessageToClipboard", - "when": "gitlens:enabled" + "when": "gitlens:enabled && gitlens:isBlameable" } ], "explorer/context": [ @@ -507,46 +507,51 @@ }, { "command": "gitlens.diffWithPrevious", - "when": "config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", "group": "gitlens_diff" }, { "command": "gitlens.diffWithWorking", - "when": "config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "gitlens:enabled && config.gitlens.menus.diff.enabled", "group": "gitlens_diff" } ], "editor/title": [ { "command": "gitlens.toggleBlame", - "when": "gitlens:enabled", + "when": "gitlens:enabled && gitlens:isBlameable", "group": "navigation@100" }, { "command": "gitlens.showQuickFileHistory", - "when": "gitlens:enabled", - "group": "1_gitlens@1" + "when": "editorFocus && gitlens:enabled", + "group": "gitlens" + }, + { + "command": "gitlens.showQuickRepoHistory", + "when": "!editorFocus && gitlens:enabled", + "group": "gitlens" }, { "command": "gitlens.showQuickRepoStatus", "when": "gitlens:enabled", - "group": "1_gitlens@2" + "group": "gitlens" }, { "command": "gitlens.diffWithPrevious", - "when": "config.gitlens.menus.diff.enabled && gitlens:enabled", - "group": "1_gitlens_diff" + "when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled", + "group": "gitlens_diff" }, { "command": "gitlens.diffWithWorking", - "when": "config.gitlens.menus.diff.enabled && gitlens:enabled", - "group": "1_gitlens_diff" + "when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled", + "group": "gitlens_diff" } ], "editor/title/context": [ { "command": "gitlens.toggleBlame", - "when": "gitlens:enabled", + "when": "gitlens:enabled && gitlens:isBlameable", "group": "gitlens@1" }, { @@ -558,32 +563,32 @@ "editor/context": [ { "command": "gitlens.diffLineWithPrevious", - "when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable && && config.gitlens.menus.diff.enabled", "group": "1_gitlens@1" }, { "command": "gitlens.diffLineWithWorking", - "when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable && config.gitlens.menus.diff.enabled", "group": "1_gitlens@2" }, { "command": "gitlens.showQuickCommitDetails", - "when": "editorTextFocus && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable", "group": "1_gitlens@3" }, { "command": "gitlens.diffWithPrevious", - "when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled", "group": "1_gitlens-file@1" }, { "command": "gitlens.diffWithWorking", - "when": "editorTextFocus && config.gitlens.menus.diff.enabled && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && config.gitlens.menus.diff.enabled", "group": "1_gitlens-file@2" }, { "command": "gitlens.toggleBlame", - "when": "editorTextFocus && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable", "group": "2_gitlens@1" }, { @@ -594,12 +599,12 @@ }, { "command": "gitlens.copyShaToClipboard", - "when": "editorTextFocus && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable", "group": "9_gitlens@1" }, { "command": "gitlens.copyMessageToClipboard", - "when": "editorTextFocus && gitlens:enabled", + "when": "editorTextFocus && gitlens:enabled && gitlens:isBlameable", "group": "9_gitlens@2" } ] diff --git a/src/blameActiveLineController.ts b/src/blameActiveLineController.ts index 5301d4b..adf681b 100644 --- a/src/blameActiveLineController.ts +++ b/src/blameActiveLineController.ts @@ -1,6 +1,7 @@ 'use strict'; import { Functions, Objects } from './system'; import { DecorationOptions, DecorationInstanceRenderOptions, DecorationRenderOptions, Disposable, ExtensionContext, Range, StatusBarAlignment, StatusBarItem, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; +import { BlameabilityChangeEvent, BlameabilityTracker } from './blameabilityTracker'; import { BlameAnnotationController } from './blameAnnotationController'; import { BlameAnnotationFormat, BlameAnnotationFormatter } from './blameAnnotationFormatter'; import { TextEditorComparer } from './comparers'; @@ -19,17 +20,17 @@ export class BlameActiveLineController extends Disposable { private _activeEditorLineDisposable: Disposable | undefined; private _blame: Promise | undefined; + private _blameable: boolean; private _config: IConfig; private _currentLine: number = -1; private _disposable: Disposable; private _editor: TextEditor | undefined; - private _editorIsDirty: boolean; private _statusBarItem: StatusBarItem | undefined; private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise; private _uri: GitUri; private _useCaching: boolean; - constructor(context: ExtensionContext, private git: GitProvider, private annotationController: BlameAnnotationController) { + constructor(context: ExtensionContext, private git: GitProvider, private blameabilityTracker: BlameabilityTracker, private annotationController: BlameAnnotationController) { super(() => this.dispose()); this._updateBlameDebounced = Functions.debounce(this._updateBlame, 50); @@ -93,8 +94,8 @@ export class BlameActiveLineController extends Disposable { const subscriptions: Disposable[] = []; subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); - subscriptions.push(window.onDidChangeTextEditorSelection(this._onEditorSelectionChanged, this)); - subscriptions.push(workspace.onDidChangeTextDocument(this._onDocumentChanged, this)); + subscriptions.push(window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this)); + subscriptions.push(this.blameabilityTracker.onDidChange(this._onBlameabilityChanged, this)); this._activeEditorLineDisposable = Disposable.from(...subscriptions); } @@ -106,35 +107,27 @@ export class BlameActiveLineController extends Disposable { this._onActiveTextEditorChanged(window.activeTextEditor); } - private _onBlameAnnotationToggled() { - this._onActiveTextEditorChanged(window.activeTextEditor); - } - - private _onGitCacheChanged() { - this._blame = undefined; - this._onActiveTextEditorChanged(window.activeTextEditor); - } - - private _onActiveTextEditorChanged(e: TextEditor) { + private _onActiveTextEditorChanged(editor: TextEditor) { this._currentLine = -1; const previousEditor = this._editor; previousEditor && previousEditor.setDecorations(activeLineDecoration, []); - if (!e || !e.document || (e.document.isUntitled && e.document.uri.scheme !== DocumentSchemes.Git) || - (e.document.uri.scheme !== DocumentSchemes.File && e.document.uri.scheme !== DocumentSchemes.Git) || - (e.viewColumn === undefined && !this.git.hasGitUriForFile(e))) { - this.clear(e); + if (!editor || !editor.document || (editor.document.isUntitled && editor.document.uri.scheme !== DocumentSchemes.Git) || + (editor.document.uri.scheme !== DocumentSchemes.File && editor.document.uri.scheme !== DocumentSchemes.Git) || + (editor.viewColumn === undefined && !this.git.hasGitUriForFile(editor))) { + this.clear(editor); this._editor = undefined; return; } - this._editor = e; - this._uri = GitUri.fromUri(e.document.uri, this.git); + this._blameable = editor && editor.document && !editor.document.isDirty; + this._editor = editor; + this._uri = GitUri.fromUri(editor.document.uri, this.git); const maxLines = this._config.advanced.caching.statusBar.maxLines; - this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || e.document.lineCount <= maxLines); + this._useCaching = this._config.advanced.caching.enabled && (maxLines <= 0 || editor.document.lineCount <= maxLines); if (this._useCaching) { this._blame = this.git.getBlameForFile(this._uri.fsPath, this._uri.sha, this._uri.repoPath); } @@ -142,38 +135,53 @@ export class BlameActiveLineController extends Disposable { this._blame = undefined; } - this._updateBlame(e.selection.active.line, e); + this._updateBlame(editor.selection.active.line, editor); } - private _onEditorSelectionChanged(e: TextEditorSelectionChangeEvent): void { + private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + this._blameable = e.blameable; + if (!e.blameable || !this._editor) { + this.clear(e.editor); + return; + } + // Make sure this is for the editor we are tracking - if (!TextEditorComparer.equals(e.textEditor, this._editor)) return; + if (!TextEditorComparer.equals(this._editor, e.editor)) return; - const line = e.selections[0].active.line; + const line = this._editor.selection.active.line; if (line === this._currentLine) return; this._currentLine = line; - this._updateBlameDebounced(line, e.textEditor); + this._updateBlame(this._editor.selection.active.line, this._editor); + } + + private _onBlameAnnotationToggled() { + this._onActiveTextEditorChanged(window.activeTextEditor); } - private _onDocumentChanged(e: TextDocumentChangeEvent) { + private _onGitCacheChanged() { + this._blame = undefined; + this._onActiveTextEditorChanged(window.activeTextEditor); + } + + private _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): void { // Make sure this is for the editor we are tracking - if (!this._editor || !TextDocumentComparer.equals(e.document, this._editor.document)) return; + if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; - const line = this._editor.selections[0].active.line; - if (line === this._currentLine && this._editorIsDirty === this._editor.document.isDirty) return; + const line = e.selections[0].active.line; + if (line === this._currentLine) return; this._currentLine = line; - this._editorIsDirty = this._editor.document.isDirty; - this._updateBlame(this._editor.selections[0].active.line, this._editor); + this._updateBlameDebounced(line, e.textEditor); } private async _updateBlame(line: number, editor: TextEditor) { line = line - this._uri.offset; - let commitLine: IGitCommitLine; let commit: GitCommit; - if (line >= 0) { + let commitLine: IGitCommitLine; + // Since blame information isn't valid when there are unsaved changes -- don't show any status + if (this._blameable && line >= 0) { if (this._useCaching) { const blame = this._blame && await this._blame; if (!blame || !blame.lines.length) { @@ -202,6 +210,10 @@ export class BlameActiveLineController extends Disposable { clear(editor: TextEditor, previousEditor?: TextEditor) { editor && editor.setDecorations(activeLineDecoration, []); + // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay + if (editor) { + setTimeout(() => editor.setDecorations(activeLineDecoration, []), 1); + } this._statusBarItem && this._statusBarItem.hide(); } @@ -258,20 +270,7 @@ export class BlameActiveLineController extends Disposable { } if (this._config.blame.annotation.activeLine !== 'off') { - let activeLine = this._config.blame.annotation.activeLine; - - // Because the inline annotations can be noisy -- only show them if the document isn't dirty - if (editor && editor.document && editor.document.isDirty) { - editor.setDecorations(activeLineDecoration, []); - switch (activeLine) { - case 'both': - activeLine = 'hover'; - break; - case 'inline': - return; - } - } - + const activeLine = this._config.blame.annotation.activeLine; const offset = this._uri.offset; const config = { diff --git a/src/blameAnnotationController.ts b/src/blameAnnotationController.ts index 3726638..6e4d5ba 100644 --- a/src/blameAnnotationController.ts +++ b/src/blameAnnotationController.ts @@ -1,6 +1,7 @@ 'use strict'; import { Functions } from './system'; import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; +import { BlameabilityChangeEvent, BlameabilityTracker } from './blameabilityTracker'; import { BlameAnnotationProvider } from './blameAnnotationProvider'; import { TextDocumentComparer, TextEditorComparer } from './comparers'; import { IBlameConfig } from './configuration'; @@ -8,16 +9,17 @@ import { GitProvider } from './gitProvider'; import { Logger } from './logger'; import { WhitespaceController } from './whitespaceController'; -export const blameDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ - before: { - margin: '0 1.75em 0 0' - }, - after: { - margin: '0 0 0 4em' - } -} as DecorationRenderOptions); - -export let highlightDecoration: TextEditorDecorationType; +export const BlameDecorations = { + annotation: window.createTextEditorDecorationType({ + before: { + margin: '0 1.75em 0 0' + }, + after: { + margin: '0 0 0 4em' + } + } as DecorationRenderOptions), + highlight: undefined as TextEditorDecorationType +}; export class BlameAnnotationController extends Disposable { @@ -32,7 +34,7 @@ export class BlameAnnotationController extends Disposable { private _disposable: Disposable; private _whitespaceController: WhitespaceController | undefined; - constructor(private context: ExtensionContext, private git: GitProvider) { + constructor(private context: ExtensionContext, private git: GitProvider, private blameabilityTracker: BlameabilityTracker) { super(() => this.dispose()); this._onConfigurationChanged(); @@ -47,6 +49,9 @@ export class BlameAnnotationController extends Disposable { dispose() { this._annotationProviders.forEach(async (p, i) => await this.clear(i)); + BlameDecorations.annotation && BlameDecorations.annotation.dispose(); + BlameDecorations.highlight && BlameDecorations.highlight.dispose(); + this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose(); this._whitespaceController && this._whitespaceController.dispose(); this._disposable && this._disposable.dispose(); @@ -71,15 +76,11 @@ export class BlameAnnotationController extends Disposable { const config = workspace.getConfiguration('gitlens').get('blame'); if (config.annotation.highlight !== (this._config && this._config.annotation.highlight)) { - highlightDecoration && highlightDecoration.dispose(); + BlameDecorations.highlight && BlameDecorations.highlight.dispose(); switch (config.annotation.highlight) { - case 'none': - highlightDecoration = undefined; - break; - case 'gutter': - highlightDecoration = window.createTextEditorDecorationType({ + BlameDecorations.highlight = window.createTextEditorDecorationType({ dark: { gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), overviewRulerColor: 'rgba(255, 255, 255, 0.75)' @@ -94,7 +95,7 @@ export class BlameAnnotationController extends Disposable { break; case 'line': - highlightDecoration = window.createTextEditorDecorationType({ + BlameDecorations.highlight = window.createTextEditorDecorationType({ dark: { backgroundColor: 'rgba(255, 255, 255, 0.15)', overviewRulerColor: 'rgba(255, 255, 255, 0.75)' @@ -109,7 +110,7 @@ export class BlameAnnotationController extends Disposable { break; case 'both': - highlightDecoration = window.createTextEditorDecorationType({ + BlameDecorations.highlight = window.createTextEditorDecorationType({ dark: { backgroundColor: 'rgba(255, 255, 255, 0.15)', gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'), @@ -125,6 +126,10 @@ export class BlameAnnotationController extends Disposable { isWholeLine: true }); break; + + default: + BlameDecorations.highlight = undefined; + break; } } @@ -172,6 +177,7 @@ export class BlameAnnotationController extends Disposable { subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this)); subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this)); subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this)); + subscriptions.push(this.blameabilityTracker.onDidChange(this._onBlameabilityChanged, this)); this._blameAnnotationsDisposable = Disposable.from(...subscriptions); } @@ -202,6 +208,17 @@ export class BlameAnnotationController extends Disposable { return false; } + private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + if (e.blameable || !e.editor) return; + + for (const [key, p] of this._annotationProviders) { + if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue; + + Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`); + this.clear(key); + } + } + private _onTextDocumentClosed(e: TextDocument) { for (const [key, p] of this._annotationProviders) { if (!TextDocumentComparer.equals(p.document, e)) continue; diff --git a/src/blameAnnotationProvider.ts b/src/blameAnnotationProvider.ts index 471cd06..46efa45 100644 --- a/src/blameAnnotationProvider.ts +++ b/src/blameAnnotationProvider.ts @@ -2,7 +2,7 @@ import { Iterables } from './system'; import { DecorationInstanceRenderOptions, DecorationOptions, Disposable, ExtensionContext, Range, TextDocument, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; import { BlameAnnotationFormat, BlameAnnotationFormatter, cssIndent, defaultShaLength, defaultAuthorLength } from './blameAnnotationFormatter'; -import { blameDecoration, highlightDecoration } from './blameAnnotationController'; +import { BlameDecorations } from './blameAnnotationController'; import { TextDocumentComparer } from './comparers'; import { BlameAnnotationStyle, IBlameConfig } from './configuration'; import { GitProvider, GitUri, IGitBlame } from './gitProvider'; @@ -36,8 +36,15 @@ export class BlameAnnotationProvider extends Disposable { async dispose() { if (this.editor) { - this.editor.setDecorations(blameDecoration, []); - highlightDecoration && this.editor.setDecorations(highlightDecoration, []); + try { + this.editor.setDecorations(BlameDecorations.annotation, []); + BlameDecorations.highlight && this.editor.setDecorations(BlameDecorations.highlight, []); + // I have no idea why the decorators sometimes don't get removed, but if they don't try again with a tiny delay + if (BlameDecorations.highlight) { + setTimeout(() => this.editor.setDecorations(BlameDecorations.highlight, []), 1); + } + } + catch (ex) { } } // HACK: Until https://github.com/Microsoft/vscode/issues/11485 is fixed -- restore whitespace @@ -80,7 +87,7 @@ export class BlameAnnotationProvider extends Disposable { } if (blameDecorationOptions) { - this.editor.setDecorations(blameDecoration, blameDecorationOptions); + this.editor.setDecorations(BlameDecorations.annotation, blameDecorationOptions); } this._setSelection(blame, shaOrLine); @@ -95,7 +102,7 @@ export class BlameAnnotationProvider extends Disposable { } private _setSelection(blame: IGitBlame, shaOrLine?: string | number) { - if (!highlightDecoration) return; + if (!BlameDecorations.highlight) return; const offset = this._uri.offset; @@ -115,7 +122,7 @@ export class BlameAnnotationProvider extends Disposable { } if (!sha) { - this.editor.setDecorations(highlightDecoration, []); + this.editor.setDecorations(BlameDecorations.highlight, []); return; } @@ -123,7 +130,7 @@ export class BlameAnnotationProvider extends Disposable { .filter(l => l.sha === sha) .map(l => this.editor.document.validateRange(new Range(l.line + offset, 0, l.line + offset, 1000000))); - this.editor.setDecorations(highlightDecoration, highlightDecorationRanges); + this.editor.setDecorations(BlameDecorations.highlight, highlightDecorationRanges); } private _getCompactGutterDecorations(blame: IGitBlame): DecorationOptions[] { diff --git a/src/blameabilityTracker.ts b/src/blameabilityTracker.ts new file mode 100644 index 0000000..1ea89f6 --- /dev/null +++ b/src/blameabilityTracker.ts @@ -0,0 +1,95 @@ +'use strict'; +import { commands, Disposable, Event, EventEmitter, TextDocument, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; +import { TextDocumentComparer } from './comparers'; +import { BuiltInCommands } from './constants'; +import { GitProvider } from './gitProvider'; + +export interface BlameabilityChangeEvent { + blameable: boolean; + editor: TextEditor; +} + +export class BlameabilityTracker extends Disposable { + + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + private _disposable: Disposable; + private _documentChangeDisposable: Disposable; + private _editor: TextEditor; + private _isBlameable: boolean; + + constructor(private git: GitProvider) { + super(() => this.dispose()); + + const subscriptions: Disposable[] = []; + + subscriptions.push(window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this)); + subscriptions.push(workspace.onDidSaveTextDocument(this._onTextDocumentSaved, this)); + subscriptions.push(this.git.onDidBlameFail(this._onBlameFailed, this)); + + this._disposable = Disposable.from(...subscriptions); + + this._onActiveTextEditorChanged(window.activeTextEditor); + } + + dispose() { + this._disposable && this._disposable.dispose(); + this._documentChangeDisposable && this._documentChangeDisposable.dispose(); + } + + private _onActiveTextEditorChanged(editor: TextEditor) { + this._editor = editor; + let blameable = editor && editor.document && !editor.document.isDirty; + + if (blameable) { + blameable = this.git.getBlameability(editor.document.fileName); + } + + this._subscribeToDocumentChanges(); + this.updateBlameability(blameable, true); + } + + private _onBlameFailed(key: string) { + const fileName = this._editor && this._editor.document && this._editor.document.fileName; + if (!fileName || key !== this.git.getCacheEntryKey(fileName)) return; + + this.updateBlameability(false); + } + + private _onTextDocumentChanged(e: TextDocumentChangeEvent) { + if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return; + + this._unsubscribeToDocumentChanges(); + this.updateBlameability(false); + } + + private _onTextDocumentSaved(e: TextDocument) { + if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return; + + this._subscribeToDocumentChanges(); + this.updateBlameability(true); + } + + private _subscribeToDocumentChanges() { + this._unsubscribeToDocumentChanges(); + this._documentChangeDisposable = workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this); + } + + private _unsubscribeToDocumentChanges() { + this._documentChangeDisposable && this._documentChangeDisposable.dispose(); + this._documentChangeDisposable = undefined; + } + + private updateBlameability(blameable: boolean, force: boolean = false) { + if (!force && this._isBlameable === blameable) return; + + commands.executeCommand(BuiltInCommands.SetContext, 'gitlens:isBlameable', blameable); + this._onDidChange.fire({ + blameable: blameable, + editor: this._editor + }); + } +} \ No newline at end of file diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index 11def2d..72922ff 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -32,7 +32,9 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand { if (!message) { if (!sha) { - const line = editor.selection.active.line; + if (editor && editor.document && editor.document.isDirty) return undefined; + + const line = (editor && editor.selection.active.line) || gitUri.offset; const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index 176990d..84adbbb 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -31,7 +31,9 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { const gitUri = GitUri.fromUri(uri, this.git); if (!sha) { - const line = editor.selection.active.line; + if (editor && editor.document && editor.document.isDirty) return undefined; + + const line = (editor && editor.selection.active.line) || gitUri.offset; const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index 6976ba4..af98889 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -20,10 +20,12 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand { uri = editor.document.uri; } - line = line ||(editor && editor.selection.active.line) || 0; - let gitUri = GitUri.fromUri(uri, this.git); + const gitUri = GitUri.fromUri(uri, this.git); + line = line || (editor && editor.selection.active.line) || gitUri.offset; if (!commit || GitProvider.isUncommitted(commit.sha)) { + if (editor && editor.document && editor.document.isDirty) return undefined; + const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index f4d080a..bbae66d 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -18,10 +18,12 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { uri = editor.document.uri; } - line = line || (editor && editor.selection.active.line) || 0; + const gitUri = GitUri.fromUri(uri, this.git); + line = line || (editor && editor.selection.active.line) || gitUri.offset; if (!commit || GitProvider.isUncommitted(commit.sha)) { - const gitUri = GitUri.fromUri(uri, this.git); + if (editor && editor.document && editor.document.isDirty) return undefined; + const blameline = line - gitUri.offset; if (blameline < 0) return undefined; diff --git a/src/extension.ts b/src/extension.ts index 49b928f..062901b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,6 @@ 'use strict'; import { commands, ExtensionContext, languages, window, workspace } from 'vscode'; +import { BlameabilityTracker } from './blameabilityTracker'; import { BlameActiveLineController } from './blameActiveLineController'; import { BlameAnnotationController } from './blameAnnotationController'; import { configureCssCharacters } from './blameAnnotationFormatter'; @@ -63,14 +64,17 @@ export async function activate(context: ExtensionContext) { const git = new GitProvider(context); context.subscriptions.push(git); + const blameabilityTracker = new BlameabilityTracker(git); + context.subscriptions.push(blameabilityTracker); + context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); context.subscriptions.push(languages.registerCodeLensProvider(GitRevisionCodeLensProvider.selector, new GitRevisionCodeLensProvider(context, git))); - const annotationController = new BlameAnnotationController(context, git); + const annotationController = new BlameAnnotationController(context, git, blameabilityTracker); context.subscriptions.push(annotationController); - const activeLineController = new BlameActiveLineController(context, git, annotationController); + const activeLineController = new BlameActiveLineController(context, git, blameabilityTracker, annotationController); context.subscriptions.push(activeLineController); context.subscriptions.push(new Keyboard(context)); diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 271be4a..8205d8d 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -40,6 +40,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.File }; private _config: IConfig; + private _documentIsDirty: boolean; constructor(context: ExtensionContext, private git: GitProvider) { this._config = workspace.getConfiguration('').get('gitlens'); @@ -53,6 +54,8 @@ export default class GitCodeLensProvider implements CodeLensProvider { } async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + this._documentIsDirty = document.isDirty; + let languageLocations = this._config.codeLens.languageLocations.find(_ => _.language.toLowerCase() === document.languageId); if (languageLocations == null) { languageLocations = { @@ -93,7 +96,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); let blameForRangeFn: () => IGitBlameLines; - if (this._config.codeLens.recentChange.enabled) { + if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, blameRange, gitUri.sha, gitUri.repoPath)); lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character))); } @@ -101,7 +104,9 @@ export default class GitCodeLensProvider implements CodeLensProvider { if (!blameForRangeFn) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, blameRange, gitUri.sha, gitUri.repoPath)); } - lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); + if (!this._documentIsDirty) { + lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); + } } } } @@ -158,7 +163,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { } let blameForRangeFn: () => IGitBlameLines; - if (this._config.codeLens.recentChange.enabled) { + if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, symbol.location.range, gitUri.sha, gitUri.repoPath)); lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar)))); startChar++; @@ -188,7 +193,9 @@ export default class GitCodeLensProvider implements CodeLensProvider { if (!blameForRangeFn) { blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri.fsPath, symbol.location.range, gitUri.sha, gitUri.repoPath)); } - lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar)))); + if (!this._documentIsDirty) { + lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, symbol.kind, symbol.location.range, false, line.range.with(new Position(line.range.start.line, startChar)))); + } } } } @@ -200,10 +207,30 @@ export default class GitCodeLensProvider implements CodeLensProvider { } _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens { + // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately + let title: string; + if (this._documentIsDirty) { + if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) { + title = 'Cannot determine recent change or authors (unsaved changes)'; + } + else if (this._config.codeLens.recentChange.enabled) { + title = 'Cannot determine recent change (unsaved changes)'; + } + else { + title = 'Cannot determine authors (unsaved changes)'; + } + + lens.command = { + title: title, + command: undefined + }; + return lens; + } + const blame = lens.getBlame(); const recentCommit = Iterables.first(blame.commits.values()); - let title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; + title = `${recentCommit.author}, ${moment(recentCommit.date).fromNow()}`; if (this._config.advanced.debug && this._config.advanced.output.level === OutputLevel.Verbose) { title += ` [${recentCommit.sha}, Symbol(${SymbolKind[lens.symbolKind]}), Lines(${lens.blameRange.start.line + 1}-${lens.blameRange.end.line + 1})]`; } @@ -223,7 +250,7 @@ export default class GitCodeLensProvider implements CodeLensProvider { _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens { const blame = lens.getBlame(); const count = blame.authors.size; - const title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`; + let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${count > 1 ? ' and others' : ''})`; switch (this._config.codeLens.authors.command) { case CodeLensCommand.BlameAnnotate: return this._applyBlameAnnotateCommand(title, lens, blame); diff --git a/src/gitProvider.ts b/src/gitProvider.ts index 3de4bff..56406ff 100644 --- a/src/gitProvider.ts +++ b/src/gitProvider.ts @@ -52,6 +52,11 @@ export class GitProvider extends Disposable { return this._onDidChangeGitCacheEmitter.event; } + private _onDidBlameFailEmitter = new EventEmitter(); + get onDidBlameFail(): Event { + return this._onDidBlameFailEmitter.event; + } + private _gitCache: Map | undefined; private _cacheDisposable: Disposable | undefined; private _repoPath: string; @@ -100,6 +105,12 @@ export class GitProvider extends Disposable { this._uriCache = undefined; } + public getBlameability(fileName: string): boolean { + const cacheKey = this.getCacheEntryKey(Git.normalizePath(fileName)); + const entry = this._gitCache.get(cacheKey); + return !(entry && entry.hasErrors); + } + public get UseUriCaching() { return !!this._uriCache; } @@ -189,7 +200,7 @@ export class GitProvider extends Disposable { this.config = config; } - private _getCacheEntryKey(fileName: string) { + getCacheEntryKey(fileName: string) { return fileName.toLowerCase(); } @@ -206,7 +217,7 @@ export class GitProvider extends Disposable { const fileName = Git.normalizePath(document.fileName); - const cacheKey = this._getCacheEntryKey(fileName); + const cacheKey = this.getCacheEntryKey(fileName); if (reason === RemoveCacheReason.DocumentSaved) { // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again) @@ -240,7 +251,7 @@ export class GitProvider extends Disposable { fileName = fileNameOrEditor.document.uri.fsPath; } - const cacheKey = this._getCacheEntryKey(fileName); + const cacheKey = this.getCacheEntryKey(fileName); return this._uriCache.has(cacheKey); } @@ -271,7 +282,7 @@ export class GitProvider extends Disposable { getGitUriForFile(fileName: string) { if (!this.UseUriCaching) return undefined; - const cacheKey = this._getCacheEntryKey(fileName); + const cacheKey = this.getCacheEntryKey(fileName); const entry = this._uriCache.get(cacheKey); return entry && entry.uri; } @@ -294,7 +305,7 @@ export class GitProvider extends Disposable { let cacheKey: string | undefined; let entry: GitCacheEntry | undefined; if (useCaching) { - cacheKey = this._getCacheEntryKey(fileName); + cacheKey = this.getCacheEntryKey(fileName); entry = this._gitCache.get(cacheKey); if (entry !== undefined && entry.blame !== undefined) return entry.blame.item; @@ -306,6 +317,9 @@ export class GitProvider extends Disposable { const promise = this._gitignore.then(ignore => { if (ignore && !ignore.filter([fileName]).length) { Logger.log(`Skipping blame; '${fileName}' is gitignored`); + if (cacheKey) { + this._onDidBlameFailEmitter.fire(cacheKey); + } return GitProvider.EmptyPromise as Promise; } @@ -323,6 +337,7 @@ export class GitProvider extends Disposable { errorMessage: msg } as ICachedBlame; + this._onDidBlameFailEmitter.fire(cacheKey); this._gitCache.set(cacheKey, entry); return GitProvider.EmptyPromise as Promise; } @@ -485,7 +500,7 @@ export class GitProvider extends Disposable { let cacheKey: string; let entry: GitCacheEntry; if (useCaching) { - cacheKey = this._getCacheEntryKey(fileName); + cacheKey = this.getCacheEntryKey(fileName); entry = this._gitCache.get(cacheKey); if (entry !== undefined && entry.log !== undefined) return entry.log.item; @@ -583,7 +598,7 @@ export class GitProvider extends Disposable { const file = await Git.getVersionedFile(fileName, repoPath, sha); if (this.UseUriCaching) { - const cacheKey = this._getCacheEntryKey(file); + const cacheKey = this.getCacheEntryKey(file); const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath, fileName })); this._uriCache.set(cacheKey, entry); }