diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f92b11..20ec346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] ### Added +- Adds experimental support for providing blame annotations, code lens, etc on files with unsaved changes (enabled via `"gitlens.insiders": true`) -- closes [#112](https://github.com/eamodio/vscode-gitlens/issues/112) - Adds `gitlens.defaultDateStyle` setting to specify how dates will be displayed by default -- closes [#89](https://github.com/eamodio/vscode-gitlens/issues/89) ### Fixed diff --git a/package.json b/package.json index 25e6f4d..0e6da98 100644 --- a/package.json +++ b/package.json @@ -1697,7 +1697,7 @@ }, { "command": "gitlens.toggleFileRecentChanges", - "when": "gitlens:activeIsTracked" + "when": "gitlens:activeIsBlameable" }, { "command": "gitlens.toggleLineBlame", diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts index baf1b46..eaeb64d 100644 --- a/src/annotations/annotationController.ts +++ b/src/annotations/annotationController.ts @@ -1,11 +1,11 @@ 'use strict'; import { Functions, Iterables } from '../system'; -import { ConfigurationChangeEvent, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, ThemeColor, window, workspace } from 'vscode'; +import { ConfigurationChangeEvent, DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, ThemeColor, window, workspace } from 'vscode'; import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider'; import { TextDocumentComparer } from '../comparers'; import { configuration, IConfig, LineHighlightLocations } from '../configuration'; import { CommandContext, isTextEditor, setCommandContext } from '../constants'; -import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; +import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri, LineDirtyStateChangeEvent } from '../gitService'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; import { HeatmapBlameAnnotationProvider } from './heatmapBlameAnnotationProvider'; import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; @@ -168,11 +168,11 @@ export class AnnotationController extends Disposable { if (provider === undefined) continue; if (provider.annotationType === FileAnnotationType.RecentChanges) { - provider.reset(Decorations.recentChangesAnnotation, Decorations.recentChangesHighlight); + provider.reset({ decoration: Decorations.recentChangesAnnotation, highlightDecoration: Decorations.recentChangesHighlight }); } else { if (provider.annotationType === cfg.blame.file.annotationType) { - provider.reset(Decorations.blameAnnotation, Decorations.blameHighlight); + provider.reset({ decoration: Decorations.blameAnnotation, highlightDecoration: Decorations.blameHighlight }); } else { this.showAnnotations(provider.editor, cfg.blame.file.annotationType); @@ -189,10 +189,14 @@ export class AnnotationController extends Disposable { const provider = this.getProvider(editor); if (provider === undefined) { + this.gitContextTracker.setLineTracking(editor, false); + setCommandContext(CommandContext.AnnotationStatus, undefined); this.detachKeyboardHook(); } else { + this.gitContextTracker.setLineTracking(editor, true); + setCommandContext(CommandContext.AnnotationStatus, AnnotationStatus.Computed); this.attachKeyboardHook(); } @@ -204,13 +208,13 @@ export class AnnotationController extends Disposable { this.clear(e.editor, AnnotationClearReason.BlameabilityChanged); } - private onTextDocumentChanged(e: TextDocumentChangeEvent) { - if (!e.document.isDirty || !this.git.isTrackable(e.document.uri)) return; + private onLineDirtyStateChanged(e: LineDirtyStateChangeEvent) { + if (e.editor === undefined || !this.git.isTrackable(e.editor.document.uri)) return; - for (const [key, p] of this._annotationProviders) { - if (!TextDocumentComparer.equals(p.document, e.document)) continue; + for (const p of this._annotationProviders.values()) { + if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue; - this.clearCore(key, AnnotationClearReason.DocumentClosed); + p.reset(); } } @@ -365,19 +369,19 @@ export class AnnotationController extends Disposable { let provider: AnnotationProviderBase | undefined = undefined; switch (type) { case FileAnnotationType.Gutter: - provider = new GutterBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); + provider = new GutterBlameAnnotationProvider(this.context, editor, this.gitContextTracker, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); break; case FileAnnotationType.Heatmap: - provider = new HeatmapBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, undefined, this.git, gitUri); + provider = new HeatmapBlameAnnotationProvider(this.context, editor, this.gitContextTracker, Decorations.blameAnnotation, undefined, this.git, gitUri); break; case FileAnnotationType.Hover: - provider = new HoverBlameAnnotationProvider(this.context, editor, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); + provider = new HoverBlameAnnotationProvider(this.context, editor, this.gitContextTracker, Decorations.blameAnnotation, Decorations.blameHighlight, this.git, gitUri); break; case FileAnnotationType.RecentChanges: - provider = new RecentChangesAnnotationProvider(this.context, editor, undefined, Decorations.recentChangesHighlight!, this.git, gitUri); + provider = new RecentChangesAnnotationProvider(this.context, editor, this.gitContextTracker, undefined, Decorations.recentChangesHighlight!, this.git, gitUri); break; } if (provider === undefined || !(await provider.validate())) return false; @@ -393,9 +397,9 @@ export class AnnotationController extends Disposable { window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), window.onDidChangeTextEditorViewColumn(this.onTextEditorViewColumnChanged, this), window.onDidChangeVisibleTextEditors(this.onVisibleTextEditorsChanged, this), - workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this), - this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this), + this.gitContextTracker.onDidChangeLineDirtyState(this.onLineDirtyStateChanged, this) ); } diff --git a/src/annotations/annotationProvider.ts b/src/annotations/annotationProvider.ts index 1924741..5999274 100644 --- a/src/annotations/annotationProvider.ts +++ b/src/annotations/annotationProvider.ts @@ -3,6 +3,7 @@ import { DecorationOptions, Disposable, ExtensionContext, TextDocument, TextEdit import { FileAnnotationType } from '../annotations/annotationController'; import { TextDocumentComparer } from '../comparers'; import { configuration, IConfig } from '../configuration'; +import { GitContextTracker } from '../gitService'; export type TextEditorCorrelationKey = string; @@ -23,6 +24,7 @@ export abstract class AnnotationProviderBase extends Disposable { constructor( context: ExtensionContext, public editor: TextEditor, + protected readonly gitContextTracker: GitContextTracker, protected decoration: TextEditorDecorationType | undefined, protected highlightDecoration: TextEditorDecorationType | undefined ) { @@ -61,6 +63,8 @@ export abstract class AnnotationProviderBase extends Disposable { } async clear() { + this.gitContextTracker.setLineTracking(this.editor, false); + if (this.editor !== undefined) { try { if (this.highlightDecoration !== undefined) { @@ -71,21 +75,25 @@ export abstract class AnnotationProviderBase extends Disposable { this.editor.setDecorations(this.decoration, []); } } - catch (ex) { } + catch { } } } - async reset(decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined) { - await this.clear(); + async reset(changes?: { decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined }) { + if (changes !== undefined) { + await this.clear(); - this._config = configuration.get(); - this.decoration = decoration; - this.highlightDecoration = highlightDecoration; + this.decoration = changes.decoration; + this.highlightDecoration = changes.highlightDecoration; + } + this._config = configuration.get(); await this.provideAnnotation(this.editor === undefined ? undefined : this.editor.selection.active.line); } restore(editor: TextEditor, force: boolean = false) { + this.gitContextTracker.setLineTracking(this.editor, true); + // If the editor isn't disposed then we don't need to do anything // Explicitly check for `false` if (!force && (this.editor as any)._disposed === false) return; @@ -99,7 +107,13 @@ export abstract class AnnotationProviderBase extends Disposable { } } - abstract async provideAnnotation(shaOrLine?: string | number): Promise; + provideAnnotation(shaOrLine?: string | number): Promise { + this.gitContextTracker.setLineTracking(this.editor, true); + + return this.onProvideAnnotation(shaOrLine); + } + + abstract async onProvideAnnotation(shaOrLine?: string | number): Promise; abstract async selection(shaOrLine?: string | number): Promise; abstract async validate(): Promise; } \ No newline at end of file diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index cfbd462..8f34851 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -15,7 +15,6 @@ interface IRenderOptions extends DecorationInstanceRenderOptions, ThemableDecora uncommittedColor?: string | ThemeColor; } -export const endOfLineIndex = 1000000; const escapeMarkdownRegEx = /[`\>\#\*\_\-\+\.]/g; // const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___'; diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 7934932..fc19533 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -3,8 +3,9 @@ import { Arrays, Iterables } from '../system'; import { CancellationToken, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, TextDocument, TextEditor, TextEditorDecorationType } from 'vscode'; import { FileAnnotationType } from './annotationController'; import { AnnotationProviderBase } from './annotationProvider'; -import { Annotations, endOfLineIndex } from './annotations'; -import { GitBlame, GitCommit, GitService, GitUri } from '../gitService'; +import { Annotations } from './annotations'; +import { RangeEndOfLineIndex } from '../constants'; +import { GitBlame, GitCommit, GitContextTracker, GitService, GitUri } from '../gitService'; export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase { @@ -14,14 +15,17 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase constructor( context: ExtensionContext, editor: TextEditor, + gitContextTracker: GitContextTracker, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, protected readonly git: GitService, protected readonly uri: GitUri ) { - super(context, editor, decoration, highlightDecoration); + super(context, editor, gitContextTracker, decoration, highlightDecoration); - this._blame = this.git.getBlameForFile(this.uri); + this._blame = editor.document.isDirty + ? this.git.getBlameForFileContents(this.uri, editor.document.getText()) + : this.git.getBlameForFile(this.uri); } async clear() { @@ -29,6 +33,16 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase super.clear(); } + async reset(changes?: { decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined }) { + if (this.editor !== undefined) { + this._blame = this.editor.document.isDirty + ? this.git.getBlameForFileContents(this.uri, this.editor.document.getText()) + : this.git.getBlameForFile(this.uri); + } + + super.reset(changes); + } + async selection(shaOrLine?: string | number, blame?: GitBlame) { if (!this.highlightDecoration) return; @@ -57,7 +71,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase } const highlightDecorationRanges = Arrays.filterMap(blame.lines, - l => l.sha === sha ? this.editor.document.validateRange(new Range(l.line, 0, l.line, 1000000)) : undefined); + l => l.sha === sha ? this.editor.document.validateRange(new Range(l.line, 0, l.line, RangeEndOfLineIndex)) : undefined); this.editor.setDecorations(this.highlightDecoration, highlightDecorationRanges); } @@ -104,7 +118,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase } const message = Annotations.getHoverMessage(logCommit || commit, this._config.defaultDateFormat, await this.git.hasRemote(commit.repoPath), this._config.blame.file.annotationType); - return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex))); + return new Hover(message, document.validateRange(new Range(position.line, 0, position.line, RangeEndOfLineIndex))); } async provideChangesHover(document: TextDocument, position: Position, token: CancellationToken): Promise { @@ -112,7 +126,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase if (commit === undefined) return undefined; const hover = await Annotations.changesHover(commit, position.line, await GitUri.fromUri(document.uri, this.git), this.git); - return new Hover(hover.hoverMessage!, document.validateRange(new Range(position.line, 0, position.line, endOfLineIndex))); + return new Hover(hover.hoverMessage!, document.validateRange(new Range(position.line, 0, position.line, RangeEndOfLineIndex))); } private async getCommitForHover(position: Position): Promise { diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index b7284b0..a57b5b5 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -10,7 +10,7 @@ import { Logger } from '../logger'; export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { - async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { + async onProvideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { this.annotationType = FileAnnotationType.Gutter; const blame = await this.getBlame(); diff --git a/src/annotations/heatmapBlameAnnotationProvider.ts b/src/annotations/heatmapBlameAnnotationProvider.ts index 28879ce..ef3f23c 100644 --- a/src/annotations/heatmapBlameAnnotationProvider.ts +++ b/src/annotations/heatmapBlameAnnotationProvider.ts @@ -8,7 +8,7 @@ import { Logger } from '../logger'; export class HeatmapBlameAnnotationProvider extends BlameAnnotationProviderBase { - async provideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { + async onProvideAnnotation(shaOrLine?: string | number, type?: FileAnnotationType): Promise { this.annotationType = FileAnnotationType.Heatmap; const blame = await this.getBlame(); diff --git a/src/annotations/hoverBlameAnnotationProvider.ts b/src/annotations/hoverBlameAnnotationProvider.ts index 681ea26..941ace5 100644 --- a/src/annotations/hoverBlameAnnotationProvider.ts +++ b/src/annotations/hoverBlameAnnotationProvider.ts @@ -8,7 +8,7 @@ import { Logger } from '../logger'; export class HoverBlameAnnotationProvider extends BlameAnnotationProviderBase { - async provideAnnotation(shaOrLine?: string | number): Promise { + async onProvideAnnotation(shaOrLine?: string | number): Promise { this.annotationType = FileAnnotationType.Hover; const cfg = this._config.annotations.file.hover; diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index 5cfd2ce..b7f7676 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -1,9 +1,10 @@ 'use strict'; import { DecorationOptions, ExtensionContext, MarkdownString, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; -import { Annotations, endOfLineIndex } from './annotations'; import { FileAnnotationType } from './annotationController'; import { AnnotationProviderBase } from './annotationProvider'; -import { GitService, GitUri } from '../gitService'; +import { Annotations } from './annotations'; +import { RangeEndOfLineIndex } from '../constants'; +import { GitContextTracker, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; export class RecentChangesAnnotationProvider extends AnnotationProviderBase { @@ -11,15 +12,16 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { constructor( context: ExtensionContext, editor: TextEditor, + gitContextTracker: GitContextTracker, decoration: TextEditorDecorationType | undefined, highlightDecoration: TextEditorDecorationType | undefined, private readonly git: GitService, private readonly uri: GitUri ) { - super(context, editor, decoration, highlightDecoration); + super(context, editor, gitContextTracker, decoration, highlightDecoration); } - async provideAnnotation(shaOrLine?: string | number): Promise { + async onProvideAnnotation(shaOrLine?: string | number): Promise { this.annotationType = FileAnnotationType.RecentChanges; const commit = await this.git.getLogCommit(this.uri.repoPath, this.uri.fsPath, { previous: true }); @@ -44,7 +46,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase { if (line.state === 'unchanged') continue; - const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, endOfLineIndex))); + const range = this.editor.document.validateRange(new Range(new Position(count, 0), new Position(count, RangeEndOfLineIndex))); if (cfg.hover.details) { this._decorations.push({ diff --git a/src/codeLensController.ts b/src/codeLensController.ts index 69b64ca..8129677 100644 --- a/src/codeLensController.ts +++ b/src/codeLensController.ts @@ -21,8 +21,7 @@ export class CodeLensController extends Disposable { super(() => this.dispose()); this._disposable = Disposable.from( - configuration.onDidChange(this.onConfigurationChanged, this), - this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) + configuration.onDidChange(this.onConfigurationChanged, this) ); this.onConfigurationChanged(configuration.initializingChangeEvent); } @@ -50,7 +49,10 @@ export class CodeLensController extends Disposable { } else { this._provider = new GitCodeLensProvider(this.context, this.git); - this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._provider); + this._providerDisposable = Disposable.from( + languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._provider), + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) + ); } } else { @@ -67,26 +69,32 @@ export class CodeLensController extends Disposable { } private onBlameabilityChanged(e: BlameabilityChangeEvent) { - if (this._provider === undefined) return; + // Only reset if we have saved, since the code lens won't naturally be re-rendered + if (this._provider === undefined || !e.blameable || e.reason !== BlameabilityChangeReason.DocumentChanged) return; - // Don't reset if this was an editor change, because code lens will naturally be re-rendered - if (e.blameable && e.reason !== BlameabilityChangeReason.EditorChanged) { - Logger.log('Blameability changed; resetting CodeLens provider'); - this._provider.reset(); - } + Logger.log('Blameability changed; resetting CodeLens provider'); + this._provider.reset(); } toggleCodeLens(editor: TextEditor) { if (!this._canToggle) return; Logger.log(`toggleCodeLens()`); - if (this._providerDisposable !== undefined) { - this._providerDisposable.dispose(); - this._providerDisposable = undefined; + if (this._provider !== undefined) { + if (this._providerDisposable !== undefined) { + this._providerDisposable.dispose(); + this._providerDisposable = undefined; + } + + this._provider = undefined; return; } - this._providerDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this.git)); + this._provider = new GitCodeLensProvider(this.context, this.git); + this._providerDisposable = Disposable.from( + languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._provider), + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) + ); } } diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index 4f9cf43..f4b5ed5 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -52,13 +52,13 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand { if (args.message === undefined) { if (args.sha === undefined) { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = (editor && editor.selection.active.line) || 0; if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(gitUri, blameline); + const blame = editor && editor.document && editor.document.isDirty + ? await this.git.getBlameForLineContents(gitUri, blameline, editor.document.getText()) + : await this.git.getBlameForLine(gitUri, blameline); if (!blame) return undefined; if (blame.commit.isUncommitted) return undefined; diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index 4bf9480..04802e7 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -50,13 +50,13 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand { const gitUri = await GitUri.fromUri(uri, this.git); if (args.sha === undefined) { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = (editor && editor.selection.active.line) || 0; if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(gitUri, blameline); + const blame = editor && editor.document && editor.document.isDirty + ? await this.git.getBlameForLineContents(gitUri, blameline, editor.document.getText()) + : await this.git.getBlameForLine(gitUri, blameline); if (blame === undefined) return undefined; args.sha = blame.commit.sha; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index a0026bc..d72a645 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -33,13 +33,13 @@ export class DiffLineWithPreviousCommand extends ActiveEditorCommand { } if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = args.line; if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(gitUri, blameline); + const blame = editor && editor.document && editor.document.isDirty + ? await this.git.getBlameForLineContents(gitUri, blameline, editor.document.getText()) + : await this.git.getBlameForLine(gitUri, blameline); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare'); args.commit = blame.commit; diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index c7b5e80..0990ae3 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -33,13 +33,13 @@ export class DiffLineWithWorkingCommand extends ActiveEditorCommand { } if (args.commit === undefined || GitService.isUncommitted(args.commit.sha)) { - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const blameline = args.line; if (blameline < 0) return undefined; try { - const blame = await this.git.getBlameForLine(gitUri, blameline); + const blame = editor && editor.document && editor.document.isDirty + ? await this.git.getBlameForLineContents(gitUri, blameline, editor.document.getText()) + : await this.git.getBlameForLine(gitUri, blameline); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open compare'); args.commit = blame.commit; diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index 0a30c23..adddd38 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -40,8 +40,6 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { async execute(editor?: TextEditor, uri?: Uri, args: OpenCommitInRemoteCommandArgs = {}) { uri = getCommandUri(uri, editor); if (uri === undefined) return undefined; - if (editor !== undefined && editor.document !== undefined && editor.document.isDirty) return undefined; - const gitUri = await GitUri.fromUri(uri, this.git); if (!gitUri.repoPath) return undefined; @@ -50,7 +48,9 @@ export class OpenCommitInRemoteCommand extends ActiveEditorCommand { const blameline = editor === undefined ? 0 : editor.selection.active.line; if (blameline < 0) return undefined; - const blame = await this.git.getBlameForLine(gitUri, blameline); + const blame = editor && editor.document && editor.document.isDirty + ? await this.git.getBlameForLineContents(gitUri, blameline, editor.document.getText()) + : await this.git.getBlameForLine(gitUri, blameline); if (blame === undefined) return Messages.showFileNotUnderSourceControlWarningMessage('Unable to open commit in remote provider'); let commit = blame.commit; diff --git a/src/commands/showFileBlame.ts b/src/commands/showFileBlame.ts index 2c606b2..b1ac2b3 100644 --- a/src/commands/showFileBlame.ts +++ b/src/commands/showFileBlame.ts @@ -19,7 +19,7 @@ export class ShowFileBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; try { if (args.type === undefined) { diff --git a/src/commands/showLineBlame.ts b/src/commands/showLineBlame.ts index fc2d299..233c3e6 100644 --- a/src/commands/showLineBlame.ts +++ b/src/commands/showLineBlame.ts @@ -18,7 +18,7 @@ export class ShowLineBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; try { if (args.type === undefined) { diff --git a/src/commands/toggleFileBlame.ts b/src/commands/toggleFileBlame.ts index 67a47a3..6acf187 100644 --- a/src/commands/toggleFileBlame.ts +++ b/src/commands/toggleFileBlame.ts @@ -20,12 +20,12 @@ export class ToggleFileBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; // Handle the case where we are focused on a non-editor editor (output, debug console) if (uri !== undefined && !UriComparer.equals(uri, editor.document.uri)) { const e = window.visibleTextEditors.find(e => UriComparer.equals(uri, e.document.uri)); - if (e !== undefined && !e.document.isDirty) { + if (e !== undefined) { editor = e; } } diff --git a/src/commands/toggleFileHeatmap.ts b/src/commands/toggleFileHeatmap.ts index db7d2cc..db21cec 100644 --- a/src/commands/toggleFileHeatmap.ts +++ b/src/commands/toggleFileHeatmap.ts @@ -14,12 +14,12 @@ export class ToggleFileHeatmapCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; // Handle the case where we are focused on a non-editor editor (output, debug console) if (uri !== undefined && !UriComparer.equals(uri, editor.document.uri)) { const e = window.visibleTextEditors.find(e => UriComparer.equals(uri, e.document.uri)); - if (e !== undefined && !e.document.isDirty) { + if (e !== undefined) { editor = e; } } diff --git a/src/commands/toggleFileRecentChanges.ts b/src/commands/toggleFileRecentChanges.ts index 4a9a153..335cf19 100644 --- a/src/commands/toggleFileRecentChanges.ts +++ b/src/commands/toggleFileRecentChanges.ts @@ -14,12 +14,12 @@ export class ToggleFileRecentChangesCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; // Handle the case where we are focused on a non-editor editor (output, debug console) if (uri !== undefined && !UriComparer.equals(uri, editor.document.uri)) { const e = window.visibleTextEditors.find(e => UriComparer.equals(uri, e.document.uri)); - if (e !== undefined && !e.document.isDirty) { + if (e !== undefined) { editor = e; } } diff --git a/src/commands/toggleLineBlame.ts b/src/commands/toggleLineBlame.ts index 7de6deb..1ed9101 100644 --- a/src/commands/toggleLineBlame.ts +++ b/src/commands/toggleLineBlame.ts @@ -18,7 +18,7 @@ export class ToggleLineBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; try { if (args.type === undefined) { diff --git a/src/constants.ts b/src/constants.ts index 21d665f..9726d83 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,8 @@ export const QualifiedExtensionId = `eamodio.${ExtensionId}`; export const ApplicationInsightsKey = 'a9c302f8-6483-4d01-b92c-c159c799c679'; +export const RangeEndOfLineIndex = 100000000; + export enum BuiltInCommands { CloseActiveEditor = 'workbench.action.closeActiveEditor', CloseAllEditors = 'workbench.action.closeAllEditors', diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 3eb3258..22995fd 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -2,12 +2,12 @@ import { Functions, IDeferred } from './system'; import { CancellationToken, ConfigurationChangeEvent, debug, DecorationRangeBehavior, DecorationRenderOptions, Disposable, ExtensionContext, Hover, HoverProvider, languages, Position, Range, StatusBarAlignment, StatusBarItem, TextDocument, TextEditor, TextEditorDecorationType, TextEditorSelectionChangeEvent, window } from 'vscode'; import { AnnotationController, FileAnnotationType } from './annotations/annotationController'; -import { Annotations, endOfLineIndex } from './annotations/annotations'; +import { Annotations } from './annotations/annotations'; import { Commands } from './commands'; import { TextEditorComparer } from './comparers'; import { configuration, IConfig, StatusBarCommand } from './configuration'; -import { DocumentSchemes, isTextEditor } from './constants'; -import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from './gitService'; +import { DocumentSchemes, isTextEditor, RangeEndOfLineIndex } from './constants'; +import { BlameabilityChangeEvent, CommitFormatter, DirtyStateChangeEvent, GitCommit, GitCommitLine, GitContextTracker, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from './gitService'; // import { Logger } from './logger'; const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ @@ -28,7 +28,7 @@ export class CurrentLineController extends Disposable { private _blameable: boolean; private _blameLineAnnotationState: { enabled: boolean, annotationType: LineAnnotationType, reason: 'user' | 'debugging' } | undefined; private _config: IConfig; - private _currentLine: { line: number, commit?: GitCommit, logCommit?: GitLogCommit } = { line: -1 }; + private _currentLine: { line: number, commit?: GitCommit, logCommit?: GitLogCommit, dirty?: boolean } = { line: -1 }; private _debugSessionEndDisposable: Disposable | undefined; private _disposable: Disposable; private _editor: TextEditor | undefined; @@ -116,7 +116,8 @@ export class CurrentLineController extends Disposable { this._trackCurrentLineDisposable = this._trackCurrentLineDisposable || Disposable.from( window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this), - this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this), + this.gitContextTracker.onDidChangeDirtyState(this.onDirtyStateChanged, this) ); } else if (this._trackCurrentLineDisposable !== undefined) { @@ -178,6 +179,15 @@ export class CurrentLineController extends Disposable { this.refresh(window.activeTextEditor); } + private async onDirtyStateChanged(e: DirtyStateChangeEvent) { + if (e.dirty) { + this.clear(this._editor); + } + else { + this.refresh(this._editor); + } + } + private async onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { // Make sure this is for the editor we are tracking if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; @@ -219,7 +229,9 @@ export class CurrentLineController extends Disposable { let commitLine: GitCommitLine | undefined = undefined; // Since blame information isn't valid when there are unsaved changes -- don't show any status if (this._blameable && line >= 0) { - const blameLine = await this.git.getBlameForLine(this._uri, line); + const blameLine = editor.document.isDirty + ? await this.git.getBlameForLineContents(this._uri, line, editor.document.getText()) + : await this.git.getBlameForLine(this._uri, line); // Make sure we are still blameable after the await if (this._blameable) { @@ -239,6 +251,8 @@ export class CurrentLineController extends Disposable { } async clear(editor: TextEditor | undefined) { + this._updateBlameDebounced.cancel(); + this.unregisterHoverProviders(); this.clearAnnotations(editor, true); this._statusBarItem && this._statusBarItem.hide(); @@ -366,7 +380,7 @@ export class CurrentLineController extends Disposable { const cfg = this._config.annotations.line.trailing; const decoration = Annotations.trailing(commit, cfg.format, cfg.dateFormat === null ? this._config.defaultDateFormat : cfg.dateFormat); - decoration.range = editor.document.validateRange(new Range(line, endOfLineIndex, line, endOfLineIndex)); + decoration.range = editor.document.validateRange(new Range(line, RangeEndOfLineIndex, line, RangeEndOfLineIndex)); editor.setDecorations(annotationDecoration, [decoration]); this._isAnnotating = true; @@ -414,7 +428,7 @@ export class CurrentLineController extends Disposable { const wholeLine = state.annotationType === LineAnnotationType.Hover || (state.annotationType === LineAnnotationType.Trailing && this._config.annotations.line.trailing.hover.wholeLine) || fileAnnotations === FileAnnotationType.Hover || (fileAnnotations === FileAnnotationType.Gutter && this._config.annotations.file.gutter.hover.wholeLine); - const range = document.validateRange(new Range(position.line, wholeLine ? 0 : endOfLineIndex, position.line, endOfLineIndex)); + const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); if (!wholeLine && range.start.character !== position.character) return undefined; // Get the full commit message -- since blame only returns the summary @@ -452,7 +466,7 @@ export class CurrentLineController extends Disposable { const wholeLine = state.annotationType === LineAnnotationType.Hover || (state.annotationType === LineAnnotationType.Trailing && this._config.annotations.line.trailing.hover.wholeLine) || fileAnnotations === FileAnnotationType.Hover || (fileAnnotations === FileAnnotationType.Gutter && this._config.annotations.file.gutter.hover.wholeLine); - const range = document.validateRange(new Range(position.line, wholeLine ? 0 : endOfLineIndex, position.line, endOfLineIndex)); + const range = document.validateRange(new Range(position.line, wholeLine ? 0 : RangeEndOfLineIndex, position.line, RangeEndOfLineIndex)); if (!wholeLine && range.start.character !== position.character) return undefined; const hover = await Annotations.changesHover(commit, position.line, this._uri, this.git); diff --git a/src/git/gitContextTracker.ts b/src/git/gitContextTracker.ts index 33f3612..f4c345d 100644 --- a/src/git/gitContextTracker.ts +++ b/src/git/gitContextTracker.ts @@ -1,9 +1,9 @@ 'use strict'; import { Functions, IDeferred } from '../system'; -import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocumentChangeEvent, TextEditor, TextEditorSelectionChangeEvent, window, workspace } from 'vscode'; import { TextDocumentComparer } from '../comparers'; import { configuration } from '../configuration'; -import { CommandContext, isTextEditor, setCommandContext } from '../constants'; +import { CommandContext, isTextEditor, RangeEndOfLineIndex, setCommandContext } from '../constants'; import { GitChangeEvent, GitChangeReason, GitService, GitUri, Repository, RepositoryChangeEvent } from '../gitService'; import { Logger } from '../logger'; @@ -15,11 +15,24 @@ export enum BlameabilityChangeReason { } export interface BlameabilityChangeEvent { - blameable: boolean; editor: TextEditor | undefined; + + blameable: boolean; + dirty: boolean; reason: BlameabilityChangeReason; } +export interface DirtyStateChangeEvent { + editor: TextEditor | undefined; + + dirty: boolean; +} + +export interface LineDirtyStateChangeEvent extends DirtyStateChangeEvent { + line: number; + lineDirty: boolean; +} + interface Context { editor?: TextEditor; repo?: Repository; @@ -31,6 +44,8 @@ interface Context { interface ContextState { blameable?: boolean; dirty: boolean; + line?: number; + lineDirty?: boolean; revision?: boolean; tracked?: boolean; } @@ -42,17 +57,35 @@ export class GitContextTracker extends Disposable { return this._onDidChangeBlameability.event; } + private _onDidChangeDirtyState = new EventEmitter(); + get onDidChangeDirtyState(): Event { + return this._onDidChangeDirtyState.event; + } + + private _onDidChangeLineDirtyState = new EventEmitter(); + get onDidChangeLineDirtyState(): Event { + return this._onDidChangeLineDirtyState.event; + } + private readonly _context: Context = { state: { dirty: false } }; private readonly _disposable: Disposable; private _listenersDisposable: Disposable | undefined; - private _onDirtyStateChangedDebounced: ((dirty: boolean) => void) & IDeferred; + private _fireDirtyStateChangedDebounced: (() => void) & IDeferred; + + private _checkLineDirtyStateChangedDebounced: (() => void) & IDeferred; + private _fireLineDirtyStateChangedDebounced: (() => void) & IDeferred; + + private _insiders = false; constructor( private readonly git: GitService ) { super(() => this.dispose()); - this._onDirtyStateChangedDebounced = Functions.debounce(this.onDirtyStateChanged, 250); + this._fireDirtyStateChangedDebounced = Functions.debounce(this.fireDirtyStateChanged, 1000); + + this._checkLineDirtyStateChangedDebounced = Functions.debounce(this.checkLineDirtyStateChanged, 1000); + this._fireLineDirtyStateChangedDebounced = Functions.debounce(this.fireLineDirtyStateChanged, 1000); this._disposable = Disposable.from( workspace.onDidChangeConfiguration(this.onConfigurationChanged, this) @@ -65,29 +98,54 @@ export class GitContextTracker extends Disposable { this._disposable && this._disposable.dispose(); } - private onConfigurationChanged(e: ConfigurationChangeEvent) { - if (!configuration.initializing(e) && !e.affectsConfiguration('git.enabled', null!)) return; + private _lineTrackingEnabled: boolean = false; + + setLineTracking(editor: TextEditor | undefined, enabled: boolean) { + if (this._context.editor !== editor) return; - const enabled = workspace.getConfiguration('git', null!).get('enabled', true); - if (this._listenersDisposable !== undefined) { - this._listenersDisposable.dispose(); - this._listenersDisposable = undefined; + // If we are changing line tracking, reset the current line info, so we will refresh + if (this._lineTrackingEnabled !== enabled) { + this._context.state.line = undefined; + this._context.state.lineDirty = undefined; } + this._lineTrackingEnabled = enabled; + } - setCommandContext(CommandContext.Enabled, enabled); + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); - if (enabled) { - this._listenersDisposable = Disposable.from( - window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), - workspace.onDidChangeTextDocument(this.onTextDocumentChanged, this), - this.git.onDidBlameFail(this.onBlameFailed, this), - this.git.onDidChange(this.onGitChanged, this) - ); + const section = configuration.name('insiders').value; + if (initializing || configuration.changed(e, section)) { + this._insiders = configuration.get(section); - this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true); + if (!initializing) { + this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true); + } } - else { - this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, false); + + if (initializing || e.affectsConfiguration('git.enabled', null!)) { + const enabled = workspace.getConfiguration('git', null!).get('enabled', true); + if (this._listenersDisposable !== undefined) { + this._listenersDisposable.dispose(); + this._listenersDisposable = undefined; + } + + setCommandContext(CommandContext.Enabled, enabled); + + if (enabled) { + this._listenersDisposable = Disposable.from( + window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), + workspace.onDidChangeTextDocument(this.onTextDocumentChanged, this), + window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this), + this.git.onDidBlameFail(this.onBlameFailed, this), + this.git.onDidChange(this.onGitChanged, this) + ); + + this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, true); + } + else { + this.updateContext(BlameabilityChangeReason.EditorChanged, window.activeTextEditor, false); + } } } @@ -97,6 +155,10 @@ export class GitContextTracker extends Disposable { // Logger.log('GitContextTracker.onActiveTextEditorChanged', editor && editor.document.uri.fsPath); + // Reset the current line info, so we will refresh + this._context.state.line = undefined; + this._context.state.lineDirty = undefined; + this.updateContext(BlameabilityChangeReason.EditorChanged, editor, true); } @@ -106,11 +168,6 @@ export class GitContextTracker extends Disposable { this.updateBlameability(BlameabilityChangeReason.BlameFailed, false); } - private onDirtyStateChanged(dirty: boolean) { - this._context.state.dirty = dirty; - this.updateBlameability(BlameabilityChangeReason.DocumentChanged); - } - private onGitChanged(e: GitChangeEvent) { if (e.reason !== GitChangeReason.Repositories) return; @@ -126,28 +183,95 @@ export class GitContextTracker extends Disposable { if (this._context.editor === undefined || !TextDocumentComparer.equals(this._context.editor.document, e.document)) return; const dirty = e.document.isDirty; + const line = (this._context.editor && this._context.editor.selection.active.line) || -1; + + let changed = false; + if (this._context.state.dirty !== dirty || this._context.state.line !== line) { + changed = true; + + this._context.state.dirty = dirty; + if (this._context.state.line !== line) { + this._context.state.lineDirty = undefined; + } + this._context.state.line = line; + + if (dirty) { + this._fireDirtyStateChangedDebounced.cancel(); + setImmediate(() => this.fireDirtyStateChanged()); + } + else { + this._fireDirtyStateChangedDebounced(); + } + } - // If we haven't changed state, kick out - if (dirty === this._context.state.dirty) { - this._onDirtyStateChangedDebounced.cancel(); + if (!this._lineTrackingEnabled || !this._insiders) return; + + // If the file dirty state hasn't changed, check if the line has + if (!changed) { + this._checkLineDirtyStateChangedDebounced(); return; } - // Logger.log('GitContextTracker.onTextDocumentChanged', `Dirty(${dirty}) state changed`); + this._context.state.lineDirty = dirty; if (dirty) { - this._onDirtyStateChangedDebounced.cancel(); - this.onDirtyStateChanged(dirty); + this._fireLineDirtyStateChangedDebounced.cancel(); + setImmediate(() => this.fireLineDirtyStateChanged()); + } + else { + this._fireLineDirtyStateChangedDebounced(); + } + } - return; + private async checkLineDirtyStateChanged() { + const line = this._context.state.line; + if (this._context.editor === undefined || line === undefined || line < 0) return; + + // Since we only care about this one line, just pass empty lines to align the contents for blaming (and also skip using the cache) + const contents = `${' \n'.repeat(line)}${this._context.editor.document.getText(new Range(line, 0, line, RangeEndOfLineIndex))}\n`; + const blameLine = await this.git.getBlameForLineContents(this._context.uri!, line, contents, { skipCache: true }); + const lineDirty = blameLine !== undefined && blameLine.commit.isUncommitted; + + if (this._context.state.lineDirty !== lineDirty) { + this._context.state.lineDirty = lineDirty; + + this._fireLineDirtyStateChangedDebounced.cancel(); + setImmediate(() => this.fireLineDirtyStateChanged()); } + } - this._onDirtyStateChangedDebounced(dirty); + private fireDirtyStateChanged() { + if (this._insiders) { + this._onDidChangeDirtyState.fire({ + editor: this._context.editor, + dirty: this._context.state.dirty + } as DirtyStateChangeEvent); + } + else { + this.updateBlameability(BlameabilityChangeReason.DocumentChanged); + } + } + + private fireLineDirtyStateChanged() { + this._onDidChangeLineDirtyState.fire({ + editor: this._context.editor, + dirty: this._context.state.dirty, + line: this._context.state.line, + lineDirty: this._context.state.lineDirty + } as LineDirtyStateChangeEvent); + } + + private onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent) { + if (this._context.state.line === e.selections[0].active.line) return; + + this._context.state.line = undefined; + this._context.state.lineDirty = false; } private async updateContext(reason: BlameabilityChangeReason, editor: TextEditor | undefined, force: boolean = false) { try { + let dirty = false; let revision = false; let tracked = false; if (force || this._context.editor !== editor) { @@ -167,13 +291,12 @@ export class GitContextTracker extends Disposable { this._context.repoDisposable = repo.onDidChange(this.onRepoChanged, this); } - this._context.state.dirty = editor.document.isDirty; + dirty = editor.document.isDirty; revision = !!this._context.uri.sha; tracked = await this.git.isTracked(this._context.uri); } else { this._context.uri = undefined; - this._context.state.dirty = false; this._context.state.blameable = false; } } @@ -193,6 +316,13 @@ export class GitContextTracker extends Disposable { setCommandContext(CommandContext.ActiveFileIsTracked, tracked); } + if (this._context.state.dirty !== dirty) { + this._context.state.dirty = dirty; + if (this._insiders) { + this._fireDirtyStateChangedDebounced(); + } + } + this.updateBlameability(reason, undefined, force); this.updateRemotes(); } @@ -204,7 +334,9 @@ export class GitContextTracker extends Disposable { private updateBlameability(reason: BlameabilityChangeReason, blameable?: boolean, force: boolean = false) { try { if (blameable === undefined) { - blameable = this._context.state.tracked && !this._context.state.dirty; + blameable = this._insiders + ? this._context.state.tracked + : this._context.state.tracked && !this._context.state.dirty; } if (!force && this._context.state.blameable === blameable) return; @@ -213,10 +345,11 @@ export class GitContextTracker extends Disposable { setCommandContext(CommandContext.ActiveIsBlameable, blameable); this._onDidChangeBlameability.fire({ + editor: this._context.editor, blameable: blameable!, - editor: this._context && this._context.editor, + dirty: this._context.state.dirty, reason: reason - }); + } as BlameabilityChangeEvent); } catch (ex) { Logger.error(ex, 'GitContextTracker.updateBlameability'); diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 1de75b0..947f86e 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -69,7 +69,9 @@ export class GitCodeLensProvider implements CodeLensProvider { async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { if (!await this.git.isTracked(document.uri.fsPath)) return []; - const dirty = document.isDirty; + const dirty = configuration.get(configuration.name('insiders').value) + ? false + : document.isDirty; const cfg = configuration.get(configuration.name('codeLens').value, document.uri); this._debug = cfg.debug; @@ -103,7 +105,9 @@ export class GitCodeLensProvider implements CodeLensProvider { } else { [blame, symbols] = await Promise.all([ - this.git.getBlameForFile(gitUri), + document.isDirty + ? this.git.getBlameForFileContents(gitUri, document.getText()) + : this.git.getBlameForFile(gitUri), commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise ]); } diff --git a/src/gitService.ts b/src/gitService.ts index c2cc94c..12707dd 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -271,7 +271,7 @@ export class GitService extends Disposable { if (!initializing) { // Defer the event trigger enough to let everything unwind - setTimeout(() => this.fireChange(GitChangeReason.Repositories), 1); + setImmediate(() => this.fireChange(GitChangeReason.Repositories)); } } @@ -687,10 +687,10 @@ export class GitService extends Disposable { } } - async getBlameForLineContents(uri: GitUri, line: number, contents: string): Promise { + async getBlameForLineContents(uri: GitUri, line: number, contents: string, options: { skipCache?: boolean } = {}): Promise { Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`); - if (this.UseCaching) { + if (!options.skipCache && this.UseCaching) { const blame = await this.getBlameForFileContents(uri, contents); if (blame === undefined) return undefined; @@ -1230,11 +1230,11 @@ export class GitService extends Disposable { this._repositoryTree.set(rp, repo); // Send a notification that the repositories changed - setTimeout(async () => { + setImmediate(async () => { await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any()); this.fireChange(GitChangeReason.Repositories); - }, 0); + }); } return rp; diff --git a/src/system/function.ts b/src/system/function.ts index 2316bd4..3d5ad9c 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -13,8 +13,28 @@ interface IPropOfValue { } export namespace Functions { - export function debounce(fn: T, wait?: number, options?: { leading?: boolean, maxWait?: number, trailing?: boolean }): T & IDeferred { - return _debounce(fn, wait, options); + export function debounce(fn: T, wait?: number, options?: { leading?: boolean, maxWait?: number, track?: boolean, trailing?: boolean }): T & IDeferred & { pending?: () => boolean } { + const { track, ...opts } = { track: false, ...(options || {}) } as { leading?: boolean, maxWait?: number, track?: boolean, trailing?: boolean }; + + if (track !== true) return _debounce(fn, wait, opts); + + let pending = false; + + const debounced = _debounce(function() { + pending = false; + return fn.apply(null, arguments); + } as any as T, wait, options) as T & IDeferred; + + const tracked = function() { + pending = true; + return debounced.apply(null, arguments); + } as any as T & IDeferred & { pending(): boolean}; + + tracked.pending = function() { return pending; }; + tracked.cancel = function() { return debounced.cancel.apply(debounced, arguments); }; + tracked.flush = function(...args: any[]) { return debounced.flush.apply(debounced, arguments); }; + + return tracked; } export function once(fn: T): T {