From 29eabe06d161feeaaa17245c8091ecafbb7f1ec6 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 24 Oct 2017 22:09:59 -0400 Subject: [PATCH] Refactors all-the-things to reduce lag for #178 --- CHANGELOG.md | 8 +- src/annotations/annotationController.ts | 67 +++++---- src/codeLensController.ts | 32 ++-- src/commands/clearFileAnnotations.ts | 2 +- src/commands/showFileBlame.ts | 2 +- src/commands/showLineBlame.ts | 2 +- src/commands/toggleFileBlame.ts | 11 +- src/commands/toggleFileRecentChanges.ts | 11 +- src/commands/toggleLineBlame.ts | 2 +- src/comparers.ts | 11 +- src/constants.ts | 11 +- src/currentLineController.ts | 94 ++++++------ src/extension.ts | 6 +- src/git/git.ts | 37 +++-- src/git/gitContextTracker.ts | 252 ++++++++++++++++---------------- src/git/gitUri.ts | 2 +- src/gitCodeLensProvider.ts | 171 +++++++++++++--------- src/gitService.ts | 133 +++++++---------- src/logger.ts | 17 ++- src/views/gitExplorer.ts | 3 +- 20 files changed, 474 insertions(+), 400 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f193daa..f060a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] -## [6.0.0-alpha] - 2017-10-22 +## [6.0.0-alpha.1] - 2017-10-24 ATTENTION! To support multi-root workspaces some underlying fundamentals had to change, so please expect and report issues. Thanks! @@ -20,6 +20,12 @@ ATTENTION! To support multi-root workspaces some underlying fundamentals had to ### Changed - `GitLens` custom view will no longer show if there is no Git repository -- closes [#159](https://github.com/eamodio/vscode-gitlens/issues/159) +- Refactors event handling, executing git commands, and general processing to improve performance and reduce lag +- Protects credentials from possibly being affected by poor network conditions via Git Credential Manager (GCM) for Windows environment variables + +### Fixed +- Fixes jumpy code lens when deleting characters from a line with a Git code lens +- Fixes? [#178](https://github.com/eamodio/vscode-gitlens/issues/178) - Slight but noticeable keyboard lag with Gitlens ## [5.7.1] - 2017-10-19 ### Fixed diff --git a/src/annotations/annotationController.ts b/src/annotations/annotationController.ts index 2d6ce92..ff4cb03 100644 --- a/src/annotations/annotationController.ts +++ b/src/annotations/annotationController.ts @@ -1,14 +1,14 @@ 'use strict'; -import { Iterables, Objects } from '../system'; +import { Functions, Iterables, Objects } from '../system'; import { DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode'; import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider'; -import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard'; import { TextDocumentComparer } from '../comparers'; import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration'; -import { CommandContext, setCommandContext } from '../constants'; +import { CommandContext, isTextEditor, setCommandContext } from '../constants'; import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; +import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard'; import { Logger } from '../logger'; import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider'; import * as path from 'path'; @@ -64,10 +64,10 @@ export class AnnotationController extends Disposable { ) { super(() => this.dispose()); - this._onConfigurationChanged(); + this.onConfigurationChanged(); const subscriptions: Disposable[] = [ - workspace.onDidChangeConfiguration(this._onConfigurationChanged, this) + workspace.onDidChangeConfiguration(this.onConfigurationChanged, this) ]; this._disposable = Disposable.from(...subscriptions); } @@ -82,7 +82,7 @@ export class AnnotationController extends Disposable { this._disposable && this._disposable.dispose(); } - private _onConfigurationChanged() { + private onConfigurationChanged() { let changed = false; const cfg = workspace.getConfiguration().get(ExtensionKey)!; @@ -190,50 +190,49 @@ export class AnnotationController extends Disposable { } } - private async _onActiveTextEditorChanged(e: TextEditor) { - const provider = this.getProvider(e); + private onActiveTextEditorChanged(editor: TextEditor | undefined) { + if (editor !== undefined && !isTextEditor(editor)) return; + + // Logger.log('AnnotationController.onActiveTextEditorChanged', editor && editor.document.uri.fsPath); + + const provider = this.getProvider(editor); if (provider === undefined) { - await setCommandContext(CommandContext.AnnotationStatus, undefined); - await this.detachKeyboardHook(); + setCommandContext(CommandContext.AnnotationStatus, undefined); + this.detachKeyboardHook(); } else { - await setCommandContext(CommandContext.AnnotationStatus, AnnotationStatus.Computed); - await this.attachKeyboardHook(); + setCommandContext(CommandContext.AnnotationStatus, AnnotationStatus.Computed); + this.attachKeyboardHook(); } } - private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + private onBlameabilityChanged(e: BlameabilityChangeEvent) { if (e.blameable || e.editor === undefined) return; this.clear(e.editor, AnnotationClearReason.BlameabilityChanged); } - private _onTextDocumentChanged(e: TextDocumentChangeEvent) { + private onTextDocumentChanged(e: TextDocumentChangeEvent) { + if (!e.document.isDirty || !this.git.isTrackable(e.document.uri)) return; + for (const [key, p] of this._annotationProviders) { if (!TextDocumentComparer.equals(p.document, e.document)) continue; - // We have to defer because isDirty is not reliable inside this event - // https://github.com/Microsoft/vscode/issues/27231 - setTimeout(() => { - // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it - if (e.document.isDirty) return; - - // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document - // Which means the document has been reloaded and the annotations have been removed, so we need to update (clear) our state tracking - this.clearCore(key, AnnotationClearReason.DocumentChanged); - }, 1); + this.clearCore(key, AnnotationClearReason.DocumentClosed); } } - private _onTextDocumentClosed(e: TextDocument) { + private onTextDocumentClosed(document: TextDocument) { + if (!this.git.isTrackable(document.uri)) return; + for (const [key, p] of this._annotationProviders) { - if (!TextDocumentComparer.equals(p.document, e)) continue; + if (!TextDocumentComparer.equals(p.document, document)) continue; this.clearCore(key, AnnotationClearReason.DocumentClosed); } } - private _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { + private onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) { // FYI https://github.com/Microsoft/vscode/issues/35602 const provider = this.getProvider(e.textEditor); if (provider === undefined) { @@ -249,7 +248,7 @@ export class AnnotationController extends Disposable { provider.restore(e.textEditor); } - private async _onVisibleTextEditorsChanged(editors: TextEditor[]) { + private async onVisibleTextEditorsChanged(editors: TextEditor[]) { let provider: AnnotationProviderBase | undefined; for (const e of editors) { provider = this.getProvider(e); @@ -391,12 +390,12 @@ export class AnnotationController extends Disposable { Logger.log(`Add listener registrations for annotations`); const subscriptions: Disposable[] = [ - window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this), - window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this), - window.onDidChangeVisibleTextEditors(this._onVisibleTextEditorsChanged, this), - workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this), - workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this), - this.gitContextTracker.onDidChangeBlameability(this._onBlameabilityChanged, this) + 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._annotationsDisposable = Disposable.from(...subscriptions); diff --git a/src/codeLensController.ts b/src/codeLensController.ts index 58380db..b526739 100644 --- a/src/codeLensController.ts +++ b/src/codeLensController.ts @@ -4,7 +4,7 @@ import { Disposable, ExtensionContext, languages, TextEditor, workspace } from ' import { IConfig } from './configuration'; import { CommandContext, ExtensionKey, setCommandContext } from './constants'; import { GitCodeLensProvider } from './gitCodeLensProvider'; -import { GitService } from './gitService'; +import { BlameabilityChangeEvent, BlameabilityChangeReason, GitContextTracker, GitService } from './gitService'; import { Logger } from './logger'; export class CodeLensController extends Disposable { @@ -14,14 +14,18 @@ export class CodeLensController extends Disposable { private _config: IConfig; private _disposable: Disposable | undefined; - constructor(private context: ExtensionContext, private git: GitService) { + constructor( + private context: ExtensionContext, + private git: GitService, + private gitContextTracker: GitContextTracker + ) { super(() => this.dispose()); - this._onConfigurationChanged(); + this.onConfigurationChanged(); const subscriptions: Disposable[] = [ - workspace.onDidChangeConfiguration(this._onConfigurationChanged, this), - git.onDidChangeGitCache(this._onGitCacheChanged, this) + workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) ]; this._disposable = Disposable.from(...subscriptions); } @@ -34,11 +38,14 @@ export class CodeLensController extends Disposable { this._codeLensProvider = undefined; } - private _onConfigurationChanged() { + private onConfigurationChanged() { const cfg = workspace.getConfiguration().get(ExtensionKey)!; if (!Objects.areEquivalent(cfg.codeLens, this._config && this._config.codeLens)) { - Logger.log('CodeLens config changed; resetting CodeLens provider'); + if (this._config !== undefined) { + Logger.log('CodeLens config changed; resetting CodeLens provider'); + } + if (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) { if (this._codeLensProvider) { this._codeLensProvider.reset(); @@ -60,9 +67,14 @@ export class CodeLensController extends Disposable { this._config = cfg; } - private _onGitCacheChanged() { - Logger.log('Git cache changed; resetting CodeLens provider'); - this._codeLensProvider && this._codeLensProvider.reset(); + private onBlameabilityChanged(e: BlameabilityChangeEvent) { + if (this._codeLensProvider === undefined) 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._codeLensProvider.reset(); + } } toggleCodeLens(editor: TextEditor) { diff --git a/src/commands/clearFileAnnotations.ts b/src/commands/clearFileAnnotations.ts index 9713784..185ded2 100644 --- a/src/commands/clearFileAnnotations.ts +++ b/src/commands/clearFileAnnotations.ts @@ -11,7 +11,7 @@ export class ClearFileAnnotationsCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined) return undefined; try { return this.annotationController.clear(editor); diff --git a/src/commands/showFileBlame.ts b/src/commands/showFileBlame.ts index 9231f98..4f00db2 100644 --- a/src/commands/showFileBlame.ts +++ b/src/commands/showFileBlame.ts @@ -17,7 +17,7 @@ export class ShowFileBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined || editor.document.isDirty) return undefined; try { if (args.type === undefined) { diff --git a/src/commands/showLineBlame.ts b/src/commands/showLineBlame.ts index 8ef5402..53f4e21 100644 --- a/src/commands/showLineBlame.ts +++ b/src/commands/showLineBlame.ts @@ -16,7 +16,7 @@ export class ShowLineBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined || editor.document.isDirty) return undefined; try { if (args.type === undefined) { diff --git a/src/commands/toggleFileBlame.ts b/src/commands/toggleFileBlame.ts index e9e1d98..9a334fc 100644 --- a/src/commands/toggleFileBlame.ts +++ b/src/commands/toggleFileBlame.ts @@ -2,6 +2,7 @@ import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { Commands, EditorCommand } from './common'; +import { UriComparer } from '../comparers'; import { ExtensionKey, IConfig } from '../configuration'; import { Logger } from '../logger'; @@ -17,7 +18,15 @@ export class ToggleFileBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined || editor.document.isDirty) 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) { + editor = e; + } + } try { if (args.type === undefined) { diff --git a/src/commands/toggleFileRecentChanges.ts b/src/commands/toggleFileRecentChanges.ts index 8b901b3..62a5ef9 100644 --- a/src/commands/toggleFileRecentChanges.ts +++ b/src/commands/toggleFileRecentChanges.ts @@ -2,6 +2,7 @@ import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { Commands, EditorCommand } from './common'; +import { UriComparer } from '../comparers'; import { Logger } from '../logger'; export class ToggleFileRecentChangesCommand extends EditorCommand { @@ -11,7 +12,15 @@ export class ToggleFileRecentChangesCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined || editor.document.isDirty) 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) { + editor = e; + } + } try { return this.annotationController.toggleAnnotations(editor, FileAnnotationType.RecentChanges); diff --git a/src/commands/toggleLineBlame.ts b/src/commands/toggleLineBlame.ts index 6ca04bf..e55f8db 100644 --- a/src/commands/toggleLineBlame.ts +++ b/src/commands/toggleLineBlame.ts @@ -16,7 +16,7 @@ export class ToggleLineBlameCommand extends EditorCommand { } async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise { - if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined; + if (editor === undefined || editor.document.isDirty) return undefined; try { if (args.type === undefined) { diff --git a/src/comparers.ts b/src/comparers.ts index fc0500c..185345b 100644 --- a/src/comparers.ts +++ b/src/comparers.ts @@ -8,7 +8,7 @@ abstract class Comparer { class UriComparer extends Comparer { equals(lhs: Uri | undefined, rhs: Uri | undefined) { - if (lhs === undefined && rhs === undefined) return true; + if (lhs === rhs) return true; if (lhs === undefined || rhs === undefined) return false; return lhs.scheme === rhs.scheme && lhs.fsPath === rhs.fsPath; @@ -18,17 +18,18 @@ class UriComparer extends Comparer { class TextDocumentComparer extends Comparer { equals(lhs: TextDocument | undefined, rhs: TextDocument | undefined) { - if (lhs === undefined && rhs === undefined) return true; - if (lhs === undefined || rhs === undefined) return false; + return lhs === rhs; + // if (lhs === rhs) return true; + // if (lhs === undefined || rhs === undefined) return false; - return uriComparer.equals(lhs.uri, rhs.uri); + // return uriComparer.equals(lhs.uri, rhs.uri); } } class TextEditorComparer extends Comparer { equals(lhs: TextEditor | undefined, rhs: TextEditor | undefined, options: { useId: boolean, usePosition: boolean } = { useId: false, usePosition: false }) { - if (lhs === undefined && rhs === undefined) return true; + if (lhs === rhs) return true; if (lhs === undefined || rhs === undefined) return false; if (options.usePosition && (lhs.viewColumn !== rhs.viewColumn)) return false; diff --git a/src/constants.ts b/src/constants.ts index 4f55749..fa28123 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ 'use strict'; -import { commands } from 'vscode'; +import { commands, TextEditor } from 'vscode'; export const ExtensionId = 'gitlens'; export const ExtensionKey = ExtensionId; @@ -44,9 +44,16 @@ export function setCommandContext(key: CommandContext | string, value: any) { } export enum DocumentSchemes { + DebugConsole = 'debug', File = 'file', Git = 'git', - GitLensGit = 'gitlens-git' + GitLensGit = 'gitlens-git', + Output = 'output' +} + +export function isTextEditor(editor: TextEditor): boolean { + const scheme = editor.document.uri.scheme; + return scheme !== DocumentSchemes.Output && scheme !== DocumentSchemes.DebugConsole; } export enum GlyphChars { diff --git a/src/currentLineController.ts b/src/currentLineController.ts index 4418772..6db823a 100644 --- a/src/currentLineController.ts +++ b/src/currentLineController.ts @@ -6,9 +6,9 @@ import { Annotations, endOfLineIndex } from './annotations/annotations'; import { Commands } from './commands'; import { TextEditorComparer } from './comparers'; import { IConfig, StatusBarCommand } from './configuration'; -import { DocumentSchemes, ExtensionKey } from './constants'; +import { DocumentSchemes, ExtensionKey, isTextEditor } from './constants'; import { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri, ICommitFormatOptions } from './gitService'; -import { Logger } from './logger'; +// import { Logger } from './logger'; const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ after: { @@ -38,24 +38,28 @@ export class CurrentLineController extends Disposable { private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise; private _uri: GitUri; - constructor(context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker, private annotationController: AnnotationController) { + constructor( + context: ExtensionContext, + private git: GitService, + private gitContextTracker: GitContextTracker, + private annotationController: AnnotationController + ) { super(() => this.dispose()); - this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250); + this._updateBlameDebounced = Functions.debounce(this.updateBlame, 250); - this._onConfigurationChanged(); + this.onConfigurationChanged(); const subscriptions: Disposable[] = [ - workspace.onDidChangeConfiguration(this._onConfigurationChanged, this), - git.onDidChangeGitCache(this._onGitCacheChanged, this), - annotationController.onDidToggleAnnotations(this._onFileAnnotationsToggled, this), - debug.onDidStartDebugSession(this._onDebugSessionStarted, this) + workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + annotationController.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), + debug.onDidStartDebugSession(this.onDebugSessionStarted, this) ]; this._disposable = Disposable.from(...subscriptions); } dispose() { - this._clearAnnotations(this._editor, true); + this.clearAnnotations(this._editor, true); this._trackCurrentLineDisposable && this._trackCurrentLineDisposable.dispose(); this._statusBarItem && this._statusBarItem.dispose(); @@ -63,7 +67,7 @@ export class CurrentLineController extends Disposable { this._disposable && this._disposable.dispose(); } - private _onConfigurationChanged() { + private onConfigurationChanged() { const cfg = workspace.getConfiguration().get(ExtensionKey)!; let changed = false; @@ -72,14 +76,14 @@ export class CurrentLineController extends Disposable { changed = true; this._blameLineAnnotationState = undefined; - this._clearAnnotations(this._editor); + this.clearAnnotations(this._editor); } if (!Objects.areEquivalent(cfg.annotations.line.trailing, this._config && this._config.annotations.line.trailing) || !Objects.areEquivalent(cfg.annotations.line.hover, this._config && this._config.annotations.line.hover) || !Objects.areEquivalent(cfg.theme.annotations.line.trailing, this._config && this._config.theme.annotations.line.trailing)) { changed = true; - this._clearAnnotations(this._editor); + this.clearAnnotations(this._editor); } if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { @@ -107,9 +111,9 @@ export class CurrentLineController extends Disposable { const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled || (this._blameLineAnnotationState && this._blameLineAnnotationState.enabled); if (trackCurrentLine && !this._trackCurrentLineDisposable) { const subscriptions: Disposable[] = [ - window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this), - window.onDidChangeTextEditorSelection(this._onTextEditorSelectionChanged, this), - this.gitContextTracker.onDidChangeBlameability(this._onBlameabilityChanged, this) + window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), + window.onDidChangeTextEditorSelection(this.onTextEditorSelectionChanged, this), + this.gitContextTracker.onDidChangeBlameability(this.onBlameabilityChanged, this) ]; this._trackCurrentLineDisposable = Disposable.from(...subscriptions); } @@ -121,13 +125,20 @@ export class CurrentLineController extends Disposable { this.refresh(window.activeTextEditor); } - private _onActiveTextEditorChanged(editor?: TextEditor) { + private onActiveTextEditorChanged(editor: TextEditor | undefined) { + if (this._editor === editor) return; + if (editor !== undefined && !isTextEditor(editor)) return; + + // Logger.log('CurrentLineController.onActiveTextEditorChanged', editor && editor.document.uri.fsPath); + this.refresh(editor); } - private _onBlameabilityChanged(e: BlameabilityChangeEvent) { + private onBlameabilityChanged(e: BlameabilityChangeEvent) { + if (!this._blameable && !e.blameable) return; + this._blameable = e.blameable; - if (!e.blameable || !this._editor) { + if (!e.blameable || this._editor === undefined) { this.clear(e.editor); return; } @@ -138,15 +149,15 @@ export class CurrentLineController extends Disposable { this._updateBlameDebounced(this._editor.selection.active.line, this._editor); } - private _onDebugSessionStarted() { + private onDebugSessionStarted() { const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : this._config.blame.line; if (!state.enabled) return; - this._debugSessionEndDisposable = debug.onDidTerminateDebugSession(this._onDebugSessionEnded, this); + this._debugSessionEndDisposable = debug.onDidTerminateDebugSession(this.onDebugSessionEnded, this); this.toggleAnnotations(window.activeTextEditor, state.annotationType, 'debugging'); } - private _onDebugSessionEnded() { + private onDebugSessionEnded() { this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose(); this._debugSessionEndDisposable = undefined; @@ -155,16 +166,11 @@ export class CurrentLineController extends Disposable { this.toggleAnnotations(window.activeTextEditor, this._blameLineAnnotationState.annotationType); } - private _onFileAnnotationsToggled() { - this.refresh(window.activeTextEditor); - } - - private _onGitCacheChanged() { - Logger.log('Git cache changed; resetting current line annotations'); + private onFileAnnotationsToggled() { this.refresh(window.activeTextEditor); } - private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise { + 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; @@ -177,11 +183,11 @@ export class CurrentLineController extends Disposable { this._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git); } - this._clearAnnotations(e.textEditor); + this.clearAnnotations(e.textEditor); this._updateBlameDebounced(line, e.textEditor); } - private _isEditorBlameable(editor: TextEditor | undefined): boolean { + private isEditorBlameable(editor: TextEditor | undefined): boolean { if (editor === undefined || editor.document === undefined) return false; if (!this.git.isTrackable(editor.document.uri)) return false; @@ -190,7 +196,7 @@ export class CurrentLineController extends Disposable { return this.git.isEditorBlameable(editor); } - private async _updateBlame(line: number, editor: TextEditor) { + private async updateBlame(line: number, editor: TextEditor) { let commit: GitCommit | undefined = undefined; let commitLine: GitCommitLine | undefined = undefined; // Since blame information isn't valid when there are unsaved changes -- don't show any status @@ -209,11 +215,11 @@ export class CurrentLineController extends Disposable { } async clear(editor: TextEditor | undefined) { - this._clearAnnotations(editor, true); + this.clearAnnotations(editor, true); this._statusBarItem && this._statusBarItem.hide(); } - private async _clearAnnotations(editor: TextEditor | undefined, force: boolean = false) { + private async clearAnnotations(editor: TextEditor | undefined, force: boolean = false) { if (editor === undefined || (!this._isAnnotating && !force)) return; editor.setDecorations(annotationDecoration, []); @@ -228,9 +234,9 @@ export class CurrentLineController extends Disposable { async refresh(editor?: TextEditor) { this._currentLine = -1; - this._clearAnnotations(this._editor); + this.clearAnnotations(this._editor); - if (editor === undefined || !this._isEditorBlameable(editor)) { + if (editor === undefined || !this.isEditorBlameable(editor)) { this.clear(editor); this._editor = undefined; @@ -254,8 +260,8 @@ export class CurrentLineController extends Disposable { // I have no idea why I need this protection -- but it happens if (editor.document === undefined) return; - this._updateStatusBar(commit); - await this._updateAnnotations(commit, blameLine, editor, line); + this.updateStatusBar(commit); + await this.updateAnnotations(commit, blameLine, editor, line); } async showAnnotations(editor: TextEditor | undefined, type: LineAnnotationType, reason: 'user' | 'debugging' = 'user') { @@ -265,8 +271,8 @@ export class CurrentLineController extends Disposable { if (!state.enabled || state.annotationType !== type) { this._blameLineAnnotationState = { enabled: true, annotationType: type, reason: reason }; - await this._clearAnnotations(editor); - await this._updateBlame(editor.selection.active.line, editor); + await this.clearAnnotations(editor); + await this.updateBlame(editor.selection.active.line, editor); } } @@ -276,11 +282,11 @@ export class CurrentLineController extends Disposable { const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : this._config.blame.line; this._blameLineAnnotationState = { enabled: !state.enabled, annotationType: type, reason: reason }; - await this._clearAnnotations(editor); - await this._updateBlame(editor.selection.active.line, editor); + await this.clearAnnotations(editor); + await this.updateBlame(editor.selection.active.line, editor); } - private async _updateAnnotations(commit: GitCommit, blameLine: GitCommitLine, editor: TextEditor, line?: number) { + private async updateAnnotations(commit: GitCommit, blameLine: GitCommitLine, editor: TextEditor, line?: number) { const cfg = this._config.blame.line; const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : cfg; @@ -399,7 +405,7 @@ export class CurrentLineController extends Disposable { } } - private _updateStatusBar(commit: GitCommit) { + private updateStatusBar(commit: GitCommit) { const cfg = this._config.statusBar; if (!cfg.enabled || this._statusBarItem === undefined) return; diff --git a/src/extension.ts b/src/extension.ts index 6547117..9ff4c50 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -66,7 +66,7 @@ export async function activate(context: ExtensionContext) { const annotationController = new AnnotationController(context, git, gitContextTracker); context.subscriptions.push(annotationController); - const codeLensController = new CodeLensController(context, git); + const codeLensController = new CodeLensController(context, git, gitContextTracker); context.subscriptions.push(codeLensController); const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController); @@ -220,7 +220,9 @@ async function notifyOnNewGitLensVersion(context: ExtensionContext, version: str return; } - Logger.log(`GitLens upgraded from v${previousVersion} to v${version}`); + if (previousVersion !== version) { + Logger.log(`GitLens upgraded from v${previousVersion} to v${version}`); + } const [major, minor] = version.split('.'); const [prevMajor, prevMinor] = previousVersion.split('.'); diff --git a/src/git/git.ts b/src/git/git.ts index 8845984..79c2d14 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -59,21 +59,40 @@ async function gitCommand(options: GitCommandOptions, ...args: any[]): Promise> = new Map(); + async function gitCommandCore(options: GitCommandOptions, ...args: any[]): Promise { // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x args.splice(0, 0, '-c', 'core.quotepath=false', '-c', 'color.ui=false'); - Logger.log('git', ...args, ` cwd='${options.cwd}'`); - const opts = { encoding: 'utf8', ...options }; - const s = await spawnPromise(git.path, args, { - cwd: options.cwd, - // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 - // Shouldn't *really* be needed but better safe than sorry - env: { ...(options.env || process.env), GCM_INTERACTIVE: 'NEVER', GCM_PRESERVE_CREDS: 'TRUE' }, - encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' - } as SpawnOptions); + + const command = `(${options.cwd}): git ` + args.join(' '); + + let promise = pendingCommands.get(command); + if (promise === undefined) { + Logger.log(`Spawning${command}`); + + promise = spawnPromise(git.path, args, { + cwd: options.cwd, + // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 + // Shouldn't *really* be needed but better safe than sorry + env: { ...(options.env || process.env), GCM_INTERACTIVE: 'NEVER', GCM_PRESERVE_CREDS: 'TRUE' }, + encoding: (opts.encoding === 'utf8') ? 'utf8' : 'binary' + } as SpawnOptions); + + pendingCommands.set(command, promise); + } + else { + Logger.log(`Awaiting${command}`); + } + + const s = await promise; + pendingCommands.delete(command); + + Logger.log(`Completed${command}`); if (opts.encoding === 'utf8' || opts.encoding === 'binary') return s; diff --git a/src/git/gitContextTracker.ts b/src/git/gitContextTracker.ts index 7a2d483..92a1222 100644 --- a/src/git/gitContextTracker.ts +++ b/src/git/gitContextTracker.ts @@ -1,13 +1,22 @@ 'use strict'; -import { Disposable, Event, EventEmitter, TextDocument, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; +import { Functions } from '../system'; +import { Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; import { TextDocumentComparer } from '../comparers'; -import { CommandContext, setCommandContext } from '../constants'; +import { CommandContext, isTextEditor, setCommandContext } from '../constants'; import { GitService, GitUri, RepoChangedReasons } from '../gitService'; -import { Logger } from '../logger'; +// import { Logger } from '../logger'; + +export enum BlameabilityChangeReason { + BlameFailed = 'blame-failed', + DocumentChanged = 'document-changed', + EditorChanged = 'editor-changed', + RepoChanged = 'repo-changed' +} export interface BlameabilityChangeEvent { blameable: boolean; editor: TextEditor | undefined; + reason: BlameabilityChangeReason; } export class GitContextTracker extends Disposable { @@ -17,184 +26,167 @@ export class GitContextTracker extends Disposable { return this._onDidChangeBlameability.event; } - private _disposable: Disposable; - private _documentChangeDisposable: Disposable | undefined; - private _editor: TextEditor | undefined; + private _context: { editor?: TextEditor, uri?: GitUri, blameable?: boolean, dirty: boolean, tracked?: boolean } = { dirty: false }; + private _disposable: Disposable | undefined; private _gitEnabled: boolean; - private _isBlameable: boolean; constructor(private git: GitService) { super(() => this.dispose()); - const subscriptions: Disposable[] = [ - window.onDidChangeActiveTextEditor(this._onActiveTextEditorChanged, this), - workspace.onDidChangeConfiguration(this._onConfigurationChanged, this), - workspace.onDidSaveTextDocument(this._onTextDocumentSaved, this), - this.git.onDidBlameFail(this._onBlameFailed, this), - this.git.onDidChangeRepo(this._onRepoChanged, this) - ]; - this._disposable = Disposable.from(...subscriptions); - - this._onConfigurationChanged(); + this.onConfigurationChanged(); } dispose() { this._disposable && this._disposable.dispose(); - this._documentChangeDisposable && this._documentChangeDisposable.dispose(); } - _onConfigurationChanged() { + private onConfigurationChanged() { const gitEnabled = workspace.getConfiguration('git').get('enabled', true); if (this._gitEnabled !== gitEnabled) { this._gitEnabled = gitEnabled; + + if (this._disposable !== undefined) { + this._disposable.dispose(); + this._disposable = undefined; + } + setCommandContext(CommandContext.Enabled, gitEnabled); - this._onActiveTextEditorChanged(window.activeTextEditor); + + if (gitEnabled) { + const subscriptions: Disposable[] = [ + window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveTextEditorChanged, 50), this), + workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), + this.git.onDidBlameFail(this.onBlameFailed, this), + this.git.onDidChangeRepo(this.onRepoChanged, this) + ]; + this._disposable = Disposable.from(...subscriptions); + + this.onActiveTextEditorChanged(window.activeTextEditor); + } } } - async _onRepoChanged(reasons: RepoChangedReasons[]) { - // TODO: Support multi-root - if (!reasons.includes(RepoChangedReasons.Remotes) && !reasons.includes(RepoChangedReasons.Repositories)) return; + private onActiveTextEditorChanged(editor: TextEditor | undefined) { + if (editor === this._context.editor) return; + if (editor !== undefined && !isTextEditor(editor)) return; - const gitUri = this._editor === undefined ? undefined : await GitUri.fromUri(this._editor.document.uri, this.git); - this._updateContextHasRemotes(gitUri); - } + // Logger.log('GitContextTracker.onActiveTextEditorChanged', editor && editor.document.uri.fsPath); - private _onActiveTextEditorChanged(editor: TextEditor | undefined) { - this._editor = editor; - this._updateContext(this._gitEnabled ? editor : undefined); - this._subscribeToDocumentChanges(); + this.updateContext(BlameabilityChangeReason.EditorChanged, editor, true); } - private _onBlameFailed(key: string) { - if (this._editor === undefined || this._editor.document === undefined || this._editor.document.uri === undefined) return; - if (key !== this.git.getCacheEntryKey(this._editor.document.uri)) return; + private onBlameFailed(key: string) { + if (this._context.editor === undefined || key !== this.git.getCacheEntryKey(this._context.editor.document.uri)) return; - this._updateBlameability(false); + this.updateBlameability(BlameabilityChangeReason.BlameFailed, false); } - private _onTextDocumentChanged(e: TextDocumentChangeEvent) { - if (!TextDocumentComparer.equals(this._editor && this._editor.document, e && e.document)) return; + private onRepoChanged(reasons: RepoChangedReasons[]) { + if (reasons.includes(RepoChangedReasons.CacheReset) || reasons.includes(RepoChangedReasons.Unknown)) { + this.updateContext(BlameabilityChangeReason.RepoChanged, this._context.editor); - // Can't unsubscribe here because undo doesn't trigger any other event - // this._unsubscribeToDocumentChanges(); - // this.updateBlameability(false); - - // We have to defer because isDirty is not reliable inside this event - // https://github.com/Microsoft/vscode/issues/27231 - setTimeout(async () => { - let blameable = !e.document.isDirty; - if (blameable) { - blameable = await this.git.getBlameability(await GitUri.fromUri(e.document.uri, this.git)); - } - this._updateBlameability(blameable); - }, 1); - } - - private async _onTextDocumentSaved(e: TextDocument) { - if (!TextDocumentComparer.equals(this._editor && this._editor.document, e)) return; + return; + } - // Don't need to resubscribe as we aren't unsubscribing on document changes anymore - // this._subscribeToDocumentChanges(); + // TODO: Support multi-root + if (!reasons.includes(RepoChangedReasons.Remotes) && !reasons.includes(RepoChangedReasons.Repositories)) return; - let blameable = !e.isDirty; - if (blameable) { - blameable = await this.git.getBlameability(await GitUri.fromUri(e.uri, this.git)); - } - this._updateBlameability(blameable); + this.updateRemotes(this._context.uri); } - private _subscribeToDocumentChanges() { - this._unsubscribeToDocumentChanges(); - this._documentChangeDisposable = workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this); - } + private onTextDocumentChanged(e: TextDocumentChangeEvent) { + if (this._context.editor === undefined || !TextDocumentComparer.equals(this._context.editor.document, e.document)) return; - private _unsubscribeToDocumentChanges() { - this._documentChangeDisposable && this._documentChangeDisposable.dispose(); - this._documentChangeDisposable = undefined; - } + // If we haven't changed state, kick out + if (this._context.dirty === e.document.isDirty) return; - private async _updateContext(editor: TextEditor | undefined) { - try { - const gitUri = editor === undefined ? undefined : await GitUri.fromUri(editor.document.uri, this.git); + // Logger.log('GitContextTracker.onTextDocumentChanged', 'Dirty state changed', e); - await Promise.all([ - this._updateEditorContext(gitUri, editor), - this._updateContextHasRemotes(gitUri) - ]); - } - catch (ex) { - Logger.error(ex, 'GitEditorTracker._updateContext'); - } + this._context.dirty = e.document.isDirty; + this.updateBlameability(BlameabilityChangeReason.DocumentChanged); } - private async _updateContextHasRemotes(uri: GitUri | undefined) { - try { - const repositories = await this.git.getRepositories(); - - let hasRemotes = false; - if (uri !== undefined && this.git.isTrackable(uri)) { - const remotes = await this.git.getRemotes(uri.repoPath); + private async updateContext(reason: BlameabilityChangeReason, editor: TextEditor | undefined, force: boolean = false) { + let tracked: boolean; + if (force || this._context.editor !== editor) { + this._context.editor = editor; - await setCommandContext(CommandContext.ActiveHasRemotes, remotes.length !== 0); + if (editor !== undefined) { + this._context.uri = await GitUri.fromUri(editor.document.uri, this.git); + this._context.dirty = editor.document.isDirty; + tracked = await this.git.isTracked(this._context.uri); } else { - if (repositories.length === 1) { - const remotes = await this.git.getRemotes(repositories[0].path); - hasRemotes = remotes.length !== 0; - - await setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes); - } - else { - await setCommandContext(CommandContext.ActiveHasRemotes, false); - } + this._context.uri = undefined; + this._context.dirty = false; + this._context.blameable = false; + tracked = false; } + } + else { + // Since the tracked state could have changed, update it + tracked = this._context.uri !== undefined + ? await this.git.isTracked(this._context.uri!) + : false; + } - if (!hasRemotes) { - for (const repo of repositories) { - const remotes = await this.git.getRemotes(repo.path); - hasRemotes = remotes.length !== 0; + if (this._context.tracked !== tracked) { + this._context.tracked = tracked; + setCommandContext(CommandContext.ActiveFileIsTracked, tracked); + } - if (hasRemotes) break; - } - } + this.updateBlameability(reason, undefined, force); + this.updateRemotes(this._context.uri); + } - await setCommandContext(CommandContext.HasRemotes, hasRemotes); - } - catch (ex) { - Logger.error(ex, 'GitEditorTracker._updateContextHasRemotes'); + private updateBlameability(reason: BlameabilityChangeReason, blameable?: boolean, force: boolean = false) { + if (blameable === undefined) { + blameable = this._context.tracked && !this._context.dirty; } + + if (!force && this._context.blameable === blameable) return; + + this._context.blameable = blameable; + + setCommandContext(CommandContext.ActiveIsBlameable, blameable); + this._onDidChangeBlameability.fire({ + blameable: blameable!, + editor: this._context && this._context.editor, + reason: reason + }); } - private async _updateEditorContext(uri: GitUri | undefined, editor: TextEditor | undefined) { - try { - const tracked = uri === undefined ? false : await this.git.isTracked(uri); - setCommandContext(CommandContext.ActiveFileIsTracked, tracked); + private async updateRemotes(uri: GitUri | undefined) { + const repositories = await this.git.getRepositories(); - let blameable = tracked && (editor !== undefined && editor.document !== undefined && !editor.document.isDirty); - if (blameable) { - blameable = uri === undefined ? false : await this.git.getBlameability(uri); - } + let hasRemotes = false; + if (uri !== undefined && this.git.isTrackable(uri)) { + const remotes = await this.git.getRemotes(uri.repoPath); - this._updateBlameability(blameable, true); + setCommandContext(CommandContext.ActiveHasRemotes, remotes.length !== 0); } - catch (ex) { - Logger.error(ex, 'GitEditorTracker._updateEditorContext'); + else { + if (repositories.length === 1) { + const remotes = await this.git.getRemotes(repositories[0].path); + hasRemotes = remotes.length !== 0; + + setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes); + } + else { + setCommandContext(CommandContext.ActiveHasRemotes, false); + } } - } - private _updateBlameability(blameable: boolean, force: boolean = false) { - if (!force && this._isBlameable === blameable) return; + if (!hasRemotes) { + for (const repo of repositories) { + const remotes = await this.git.getRemotes(repo.path); + hasRemotes = remotes.length !== 0; - try { - setCommandContext(CommandContext.ActiveIsBlameable, blameable); - this._onDidChangeBlameability.fire({ - blameable: blameable, - editor: this._editor - }); - } - catch (ex) { - Logger.error(ex, 'GitEditorTracker._updateBlameability'); + if (hasRemotes) break; + } } + + setCommandContext(CommandContext.HasRemotes, hasRemotes); } } \ No newline at end of file diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 5e0530c..cf24a47 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -110,7 +110,7 @@ export class GitUri extends ((Uri as any) as UriEx) { const gitUri = git.getGitUriForFile(uri); if (gitUri) return gitUri; - return new GitUri(uri, await git.getRepoPath(uri.fsPath)); + return new GitUri(uri, await git.getRepoPath(uri)); } static fromFileStatus(status: IGitStatusFile, repoPath: string, original?: boolean): GitUri; diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 03edf6b..23f2433 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -9,18 +9,33 @@ import { Logger } from './logger'; export class GitRecentChangeCodeLens extends CodeLens { - constructor(private blame: () => GitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { + constructor( + public symbolKind: SymbolKind, + public uri: GitUri | undefined, + private blame: (() => GitBlameLines | undefined) | undefined, + public blameRange: Range, + public isFullRange: boolean, + range: Range, + public dirty: boolean + ) { super(range); } getBlame(): GitBlameLines | undefined { - return this.blame(); + return this.blame && this.blame(); } } export class GitAuthorsCodeLens extends CodeLens { - constructor(private blame: () => GitBlameLines | undefined, public uri: GitUri, public symbolKind: SymbolKind, public blameRange: Range, public isFullRange: boolean, range: Range) { + constructor( + public symbolKind: SymbolKind, + public uri: GitUri | undefined, + private blame: () => GitBlameLines | undefined, + public blameRange: Range, + public isFullRange: boolean, + range: Range + ) { super(range); } @@ -39,7 +54,6 @@ export class GitCodeLensProvider implements CodeLensProvider { static selector: DocumentSelector = { scheme: DocumentSchemes.File }; private _config: IConfig; - private _documentIsDirty: boolean; constructor(context: ExtensionContext, private git: GitService) { this._config = workspace.getConfiguration().get(ExtensionKey)!; @@ -53,7 +67,7 @@ export class GitCodeLensProvider implements CodeLensProvider { } async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { - this._documentIsDirty = document.isDirty; + const dirty = document.isDirty; let languageLocations = this._config.codeLens.perLanguageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId); if (languageLocations == null) { @@ -70,44 +84,59 @@ export class GitCodeLensProvider implements CodeLensProvider { const lenses: CodeLens[] = []; - const gitUri = await GitUri.fromUri(document.uri, this.git); - - const blamePromise = this.git.getBlameForFile(gitUri); + let gitUri: GitUri | undefined; let blame: GitBlame | undefined; - if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) { - blame = await blamePromise; + let symbols: SymbolInformation[] | undefined; + + if (!dirty) { + gitUri = await GitUri.fromUri(document.uri, this.git); + + if (token.isCancellationRequested) return lenses; + + if (languageLocations.locations.length === 1 && languageLocations.locations.includes(CodeLensLocations.Document)) { + blame = await this.git.getBlameForFile(gitUri); + } + else { + [blame, symbols] = await Promise.all([ + this.git.getBlameForFile(gitUri), + commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise + ]); + } + if (blame === undefined || blame.lines.length === 0) return lenses; } else { - const values = await Promise.all([ - blamePromise as Promise, - commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise - ]); + if (languageLocations.locations.length !== 1 || !languageLocations.locations.includes(CodeLensLocations.Document)) { + symbols = await commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as SymbolInformation[]; + } + } - blame = values[0] as GitBlame; - if (blame === undefined || blame.lines.length === 0) return lenses; + if (token.isCancellationRequested) return lenses; - const symbols = values[1] as SymbolInformation[]; + const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000))); + + if (symbols !== undefined) { Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`); - symbols.forEach(sym => this._provideCodeLens(gitUri, document, sym, languageLocations!, blame!, lenses)); + symbols.forEach(sym => this.provideCodeLens(lenses, document, dirty, sym, languageLocations!, documentRangeFn, blame, gitUri)); } if ((languageLocations.locations.includes(CodeLensLocations.Document) || languageLocations.customSymbols.includes('file')) && !languageLocations.customSymbols.includes('!file')) { // Check if we have a lens for the whole document -- if not add one if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { - const blameRange = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); + const blameRange = documentRangeFn(); + let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined; - if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { - blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri, blameRange)); - lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 0, 0, blameRange.start.character))); + if (dirty || this._config.codeLens.recentChange.enabled) { + if (!dirty) { + blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); + } + lenses.push(new GitRecentChangeCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 0, 0, blameRange.start.character), dirty)); } - if (this._config.codeLens.authors.enabled) { + if (!dirty && this._config.codeLens.authors.enabled) { if (blameForRangeFn === undefined) { - blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri, blameRange)); - } - if (!this._documentIsDirty) { - lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, SymbolKind.File, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); + blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } + lenses.push(new GitAuthorsCodeLens(SymbolKind.File, gitUri, blameForRangeFn, blameRange, true, new Range(0, 1, 0, blameRange.start.character))); } } } @@ -115,7 +144,7 @@ export class GitCodeLensProvider implements CodeLensProvider { return lenses; } - private _validateSymbolAndGetBlameRange(document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation): Range | undefined { + private validateSymbolAndGetBlameRange(symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range): Range | undefined { let valid = false; let range: Range | undefined; @@ -128,7 +157,7 @@ export class GitCodeLensProvider implements CodeLensProvider { if (valid) { // Adjust the range to be for the whole file - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); + range = documentRangeFn(); } break; @@ -140,7 +169,7 @@ export class GitCodeLensProvider implements CodeLensProvider { if (valid) { // Adjust the range to be for the whole file if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) { - range = document.validateRange(new Range(0, 1000000, 1000000, 1000000)); + range = documentRangeFn(); } } break; @@ -174,9 +203,9 @@ export class GitCodeLensProvider implements CodeLensProvider { return valid ? range || symbol.location.range : undefined; } - private _provideCodeLens(gitUri: GitUri, document: TextDocument, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, blame: GitBlame, lenses: CodeLens[]): void { - const blameRange = this._validateSymbolAndGetBlameRange(document, symbol, languageLocation); - if (!blameRange) return; + private provideCodeLens(lenses: CodeLens[], document: TextDocument, dirty: boolean, symbol: SymbolInformation, languageLocation: ICodeLensLanguageLocation, documentRangeFn: () => Range, blame: GitBlame | undefined, gitUri: GitUri | undefined): void { + const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageLocation, documentRangeFn); + if (blameRange === undefined) return; const line = document.lineAt(symbol.location.range.start); // Make sure there is only 1 lens per line @@ -185,10 +214,12 @@ export class GitCodeLensProvider implements CodeLensProvider { // Anchor the code lens to the end of the line -- so they are somewhat consistently placed let startChar = line.range.end.character - 1; - let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined; - if (this._documentIsDirty || this._config.codeLens.recentChange.enabled) { - blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri, blameRange)); - lenses.push(new GitRecentChangeCodeLens(blameForRangeFn, gitUri, symbol.kind, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)))); + let blameForRangeFn: (() => GitBlameLines | undefined) | undefined; + if (dirty || this._config.codeLens.recentChange.enabled) { + if (!dirty) { + blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); + } + lenses.push(new GitRecentChangeCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)), dirty)); startChar++; } @@ -213,27 +244,25 @@ export class GitCodeLensProvider implements CodeLensProvider { } } - if (multiline) { + if (multiline && !dirty) { if (blameForRangeFn === undefined) { - blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame, gitUri, blameRange)); - } - if (!this._documentIsDirty) { - lenses.push(new GitAuthorsCodeLens(blameForRangeFn, gitUri, symbol.kind, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)))); + blameForRangeFn = Functions.once(() => this.git.getBlameForRangeSync(blame!, gitUri!, blameRange)); } + lenses.push(new GitAuthorsCodeLens(symbol.kind, gitUri, blameForRangeFn, blameRange, false, line.range.with(new Position(line.range.start.line, startChar)))); } } } resolveCodeLens(lens: CodeLens, token: CancellationToken): CodeLens | Thenable { - if (lens instanceof GitRecentChangeCodeLens) return this._resolveGitRecentChangeCodeLens(lens, token); - if (lens instanceof GitAuthorsCodeLens) return this._resolveGitAuthorsCodeLens(lens, token); + if (lens instanceof GitRecentChangeCodeLens) return this.resolveGitRecentChangeCodeLens(lens, token); + if (lens instanceof GitAuthorsCodeLens) return this.resolveGitAuthorsCodeLens(lens, token); return Promise.reject(undefined); } - _resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens { + private 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 (lens.dirty) { if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) { title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors; } @@ -258,17 +287,17 @@ export class GitCodeLensProvider implements CodeLensProvider { } switch (this._config.codeLens.recentChange.command) { - case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ShowQuickCommitDetails: return this._applyShowQuickCommitDetailsCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ShowQuickCommitFileDetails: return this._applyShowQuickCommitFileDetailsCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ShowQuickCurrentBranchHistory: return this._applyShowQuickCurrentBranchHistoryCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ShowQuickFileHistory: return this._applyShowQuickFileHistoryCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ToggleFileBlame: return this._applyToggleFileBlameCommand(title, lens, blame); + case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand(title, lens, blame); default: return lens; } } - _resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens { + private resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens { const blame = lens.getBlame(); if (blame === undefined) return lens; @@ -279,17 +308,17 @@ export class GitCodeLensProvider implements CodeLensProvider { } switch (this._config.codeLens.authors.command) { - case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCommitDetails: return this._applyShowQuickCommitDetailsCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCommitFileDetails: return this._applyShowQuickCommitFileDetailsCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCurrentBranchHistory: return this._applyShowQuickCurrentBranchHistoryCommand(title, lens, blame); - case CodeLensCommand.ShowQuickFileHistory: return this._applyShowQuickFileHistoryCommand(title, lens, blame); - case CodeLensCommand.ToggleFileBlame: return this._applyToggleFileBlameCommand(title, lens, blame); + case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand(title, lens, blame); + case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand(title, lens, blame); + case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand(title, lens, blame); default: return lens; } } - _applyDiffWithPreviousCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { + private applyDiffWithPreviousCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { if (commit === undefined) { const blameLine = blame.allLines[lens.range.start.line]; commit = blame.commits.get(blameLine.sha); @@ -299,7 +328,7 @@ export class GitCodeLensProvider implements CodeLensProvider { title: title, command: Commands.DiffWithPrevious, arguments: [ - Uri.file(lens.uri.fsPath), + Uri.file(lens.uri!.fsPath), { commit: commit, range: lens.isFullRange ? undefined : lens.blameRange @@ -309,12 +338,12 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickCommitDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { + private applyShowQuickCommitDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails, arguments: [ - Uri.file(lens.uri.fsPath), + Uri.file(lens.uri!.fsPath), { commit, sha: commit === undefined ? undefined : commit.sha @@ -323,12 +352,12 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickCommitFileDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { + private applyShowQuickCommitFileDetailsCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails, arguments: [ - Uri.file(lens.uri.fsPath), + Uri.file(lens.uri!.fsPath), { commit, sha: commit === undefined ? undefined : commit.sha @@ -337,21 +366,21 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyShowQuickCurrentBranchHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { + private applyShowQuickCurrentBranchHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: CodeLensCommand.ShowQuickCurrentBranchHistory, - arguments: [Uri.file(lens.uri.fsPath)] + arguments: [Uri.file(lens.uri!.fsPath)] }; return lens; } - _applyShowQuickFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { + private applyShowQuickFileHistoryCommand(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T { lens.command = { title: title, command: CodeLensCommand.ShowQuickFileHistory, arguments: [ - Uri.file(lens.uri.fsPath), + Uri.file(lens.uri!.fsPath), { range: lens.isFullRange ? undefined : lens.blameRange } as ShowQuickFileHistoryCommandArgs @@ -360,11 +389,11 @@ export class GitCodeLensProvider implements CodeLensProvider { return lens; } - _applyToggleFileBlameCommand(title: string, lens: T, blame: GitBlameLines): T { + private applyToggleFileBlameCommand(title: string, lens: T, blame: GitBlameLines): T { lens.command = { title: title, command: Commands.ToggleFileBlame, - arguments: [Uri.file(lens.uri.fsPath)] + arguments: [Uri.file(lens.uri!.fsPath)] }; return lens; } diff --git a/src/gitService.ts b/src/gitService.ts index d7f4730..41560c5 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -52,8 +52,8 @@ interface CachedDiff extends CachedItem { } interface CachedLog extends CachedItem { } enum RemoveCacheReason { - DocumentClosed, - DocumentSaved + DocumentChanged, + DocumentClosed } export enum GitRepoSearchBy { @@ -66,6 +66,7 @@ export enum GitRepoSearchBy { } export enum RepoChangedReasons { + CacheReset = 'cache-reset', Remotes = 'remotes', Repositories = 'Repositories', Stash = 'stash', @@ -85,11 +86,6 @@ export class GitService extends Disposable { return this._onDidBlameFail.event; } - private _onDidChangeGitCache = new EventEmitter(); - get onDidChangeGitCache(): Event { - return this._onDidChangeGitCache.event; - } - // TODO: Support multi-root { repo, reasons }[]? private _onDidChangeRepo = new EventEmitter(); get onDidChangeRepo(): Event { @@ -98,18 +94,20 @@ export class GitService extends Disposable { private _cacheDisposable: Disposable | undefined; private _disposable: Disposable | undefined; + private _documentKeyMap: Map; private _gitCache: Map; private _pendingChanges: { repo: boolean } = { repo: false }; private _remotesCache: Map; private _repositories: Map; private _repositoriesPromise: Promise | undefined; private _suspended: boolean = false; - private _trackedCache: Map; + private _trackedCache: Map>; private _versionedUriCache: Map; constructor() { super(() => this.dispose()); + this._documentKeyMap = new Map(); this._gitCache = new Map(); this._remotesCache = new Map(); this._repositories = new Map(); @@ -136,6 +134,7 @@ export class GitService extends Disposable { this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable = undefined; + this._documentKeyMap.clear(); this._gitCache.clear(); this._remotesCache.clear(); this._trackedCache.clear(); @@ -164,9 +163,8 @@ export class GitService extends Disposable { this._cacheDisposable && this._cacheDisposable.dispose(); const subscriptions: Disposable[] = [ - workspace.onDidCloseTextDocument(d => this.removeCachedEntry(d, RemoveCacheReason.DocumentClosed)), - workspace.onDidChangeTextDocument(this.onTextDocumentChanged, this), - workspace.onDidSaveTextDocument(d => this.removeCachedEntry(d, RemoveCacheReason.DocumentSaved)) + workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), + workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this) ]; this._cacheDisposable = Disposable.from(...subscriptions); } @@ -174,17 +172,21 @@ export class GitService extends Disposable { this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable = undefined; + this._documentKeyMap.clear(); this._gitCache.clear(); } } - const ignoreWhitespace = this.config && this.config.blame.ignoreWhitespace; + // Only count the change if we aren't just starting up + const ignoreWhitespace = this.config === undefined + ? cfg.blame.ignoreWhitespace + : this.config.blame.ignoreWhitespace; this.config = cfg; if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) { this._gitCache.clear(); - this.fireGitCacheChange(); + this.fireRepoChange(RepoChangedReasons.CacheReset); } } @@ -258,19 +260,28 @@ export class GitService extends Disposable { } private onTextDocumentChanged(e: TextDocumentChangeEvent) { - if (!this.UseCaching) return; - if (e.document.uri.scheme !== DocumentSchemes.File) return; + let key = this._documentKeyMap.get(e.document); + if (key === undefined) { + key = this.getCacheEntryKey(e.document.uri); + this._documentKeyMap.set(e.document, key); + } - // We have to defer because isDirty is not reliable inside this event - // https://github.com/Microsoft/vscode/issues/27231 - setTimeout(() => { - // If the document is dirty all is fine, we'll just wait for the save before clearing our cache - if (e.document.isDirty) return; + // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) + const entry = this._gitCache.get(key); + if (entry === undefined || entry.hasErrors) return; - // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document - // Which means the document has been reloaded and we should clear our cache for it - this.removeCachedEntry(e.document, RemoveCacheReason.DocumentSaved); - }, 1); + if (this._gitCache.delete(key)) { + Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentChanged]}`); + } + } + + private onTextDocumentClosed(document: TextDocument) { + this._documentKeyMap.delete(document); + + const key = this.getCacheEntryKey(document.uri); + if (this._gitCache.delete(key)) { + Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentClosed]}`); + } } private onRepoChanged(uri: Uri) { @@ -284,21 +295,6 @@ export class GitService extends Disposable { this._trackedCache.clear(); this.fireRepoChange(); - this.fireGitCacheChange(); - } - - private fireGitCacheChangeDebounced: (() => void) | undefined = undefined; - - private fireGitCacheChange() { - if (this.fireGitCacheChangeDebounced === undefined) { - this.fireGitCacheChangeDebounced = Functions.debounce(this.fireGitCacheChangeCore, 50); - } - - return this.fireGitCacheChangeDebounced(); - } - - private fireGitCacheChangeCore() { - this._onDidChangeGitCache.fire(); } private _fireRepoChangeDebounced: (() => void) | undefined = undefined; @@ -306,7 +302,7 @@ export class GitService extends Disposable { private fireRepoChange(reason: RepoChangedReasons = RepoChangedReasons.Unknown) { if (this._fireRepoChangeDebounced === undefined) { - this._fireRepoChangeDebounced = Functions.debounce(this.fireRepoChangeCore, 50); + this._fireRepoChangeDebounced = Functions.debounce(this.fireRepoChangeCore, 250); } if (!this._repoChangedReasons.includes(reason)) { @@ -328,27 +324,6 @@ export class GitService extends Disposable { this._onDidChangeRepo.fire(reasons); } - private removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) { - if (!this.UseCaching) return; - if (document.uri.scheme !== DocumentSchemes.File) return; - - const cacheKey = this.getCacheEntryKey(document.uri); - - if (reason === RemoveCacheReason.DocumentSaved) { - // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again) - const entry = this._gitCache.get(cacheKey); - if (entry && entry.hasErrors) return; - } - - if (this._gitCache.delete(cacheKey)) { - Logger.log(`Clear cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`); - - if (reason === RemoveCacheReason.DocumentSaved) { - this.fireGitCacheChange(); - } - } - } - public async getRepositories(): Promise { if (this._repositoriesPromise !== undefined) { await this._repositoriesPromise; @@ -981,6 +956,10 @@ export class GitService extends Disposable { const folder = workspace.getWorkspaceFolder(filePathOrUri); if (folder !== undefined) { + if (this._repositoriesPromise !== undefined) { + await this._repositoriesPromise; + } + const rp = this._repositories.get(folder.uri.fsPath); if (rp !== undefined) return rp.path; } @@ -988,10 +967,8 @@ export class GitService extends Disposable { return this.getRepoPathCore(filePathOrUri.fsPath, false); } - private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise { - const rp = await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); - console.log(filePath, rp); - return rp; + private getRepoPathCore(filePath: string, isDirectory: boolean): Promise { + return Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); } async getStashList(repoPath: string | undefined): Promise { @@ -1076,9 +1053,9 @@ export class GitService extends Disposable { return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLensGit; } - async isTracked(fileName: string, repoPath: string | undefined): Promise; + async isTracked(fileName: string, repoPath?: string): Promise; async isTracked(uri: GitUri): Promise; - async isTracked(fileNameOrUri: string | GitUri, repoPath?: string | undefined): Promise { + async isTracked(fileNameOrUri: string | GitUri, repoPath?: string): Promise { let cacheKey: string; let fileName: string; if (typeof fileNameOrUri === 'string') { @@ -1096,15 +1073,24 @@ export class GitService extends Disposable { Logger.log(`isTracked('${fileName}', '${repoPath}')`); let tracked = this._trackedCache.get(cacheKey); - if (tracked !== undefined) return tracked; + if (tracked !== undefined) { + if (typeof tracked === 'boolean') return tracked; + return await tracked; + } - const result = await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName); - tracked = !!result; + tracked = this.isTrackedCore(repoPath === undefined ? '' : repoPath, fileName); + this._trackedCache.set(cacheKey, tracked); + + tracked = await tracked; this._trackedCache.set(cacheKey, tracked); return tracked; } + private async isTrackedCore(repoPath: string, fileName: string) { + const result = await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName); + return !!result; + } openDiffTool(repoPath: string, uri: Uri, staged: boolean) { Logger.log(`openDiffTool('${repoPath}', '${uri}', ${staged})`); @@ -1149,13 +1135,6 @@ export class GitService extends Disposable { return Git.gitInfo().version; } - // static async getRepoPath(cwd: string | undefined): Promise { - // const repoPath = await Git.getRepoPath(cwd); - // if (!repoPath) return ''; - - // return repoPath; - // } - static fromGitContentUri(uri: Uri): IGitUriData { if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); return GitService._fromGitContentUri(uri); diff --git a/src/logger.ts b/src/logger.ts index 28b512c..32a61ec 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -42,21 +42,21 @@ export class Logger { static log(message?: any, ...params: any[]): void { if (debug) { - console.log(ConsolePrefix, message, ...params); + console.log(this.timestamp, ConsolePrefix, message, ...params); } if (level === OutputLevel.Verbose) { - output.appendLine([message, ...params].join(' ')); + output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); } } static error(ex: Error, classOrMethod?: string, ...params: any[]): void { if (debug) { - console.error(ConsolePrefix, classOrMethod, ex, ...params); + console.error(this.timestamp, ConsolePrefix, classOrMethod, ex, ...params); } if (level !== OutputLevel.Silent) { - output.appendLine([classOrMethod, ex, ...params].join(' ')); + output.appendLine((debug ? [this.timestamp, classOrMethod, ex, ...params] : [classOrMethod, ex, ...params]).join(' ')); } Telemetry.trackException(ex); @@ -64,11 +64,16 @@ export class Logger { static warn(message?: any, ...params: any[]): void { if (debug) { - console.warn(ConsolePrefix, message, ...params); + console.warn(this.timestamp, ConsolePrefix, message, ...params); } if (level !== OutputLevel.Silent) { - output.appendLine([message, ...params].join(' ')); + output.appendLine((debug ? [this.timestamp, message, ...params] : [message, ...params]).join(' ')); } } + + private static get timestamp(): string { + const now = new Date(); + return `[${now.toISOString().replace(/T/, ' ').replace(/\..+/, '')}:${now.getUTCMilliseconds()}]`; + } } \ No newline at end of file diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 6073cd4..b4f5459 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -344,8 +344,7 @@ export class GitExplorer implements TreeDataProvider { } if (enabled) { - const repoChangedFn = Functions.debounce(this.onRepoChanged, 250); - this._autoRefreshDisposable = this.git.onDidChangeRepo(repoChangedFn, this); + this._autoRefreshDisposable = this.git.onDidChangeRepo(this.onRepoChanged, this); this.context.subscriptions.push(this._autoRefreshDisposable); } }