Browse Source

Refactors all-the-things to reduce lag for #178

main
Eric Amodio 7 years ago
parent
commit
29eabe06d1
20 changed files with 474 additions and 400 deletions
  1. +7
    -1
      CHANGELOG.md
  2. +33
    -34
      src/annotations/annotationController.ts
  3. +22
    -10
      src/codeLensController.ts
  4. +1
    -1
      src/commands/clearFileAnnotations.ts
  5. +1
    -1
      src/commands/showFileBlame.ts
  6. +1
    -1
      src/commands/showLineBlame.ts
  7. +10
    -1
      src/commands/toggleFileBlame.ts
  8. +10
    -1
      src/commands/toggleFileRecentChanges.ts
  9. +1
    -1
      src/commands/toggleLineBlame.ts
  10. +6
    -5
      src/comparers.ts
  11. +9
    -2
      src/constants.ts
  12. +50
    -44
      src/currentLineController.ts
  13. +4
    -2
      src/extension.ts
  14. +28
    -9
      src/git/git.ts
  15. +122
    -130
      src/git/gitContextTracker.ts
  16. +1
    -1
      src/git/gitUri.ts
  17. +100
    -71
      src/gitCodeLensProvider.ts
  18. +56
    -77
      src/gitService.ts
  19. +11
    -6
      src/logger.ts
  20. +1
    -2
      src/views/gitExplorer.ts

+ 7
- 1
CHANGELOG.md View File

@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased] ## [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! 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 ### Changed
- `GitLens` custom view will no longer show if there is no Git repository -- closes [#159](https://github.com/eamodio/vscode-gitlens/issues/159) - `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 ## [5.7.1] - 2017-10-19
### Fixed ### Fixed

+ 33
- 34
src/annotations/annotationController.ts View File

@ -1,14 +1,14 @@
'use strict'; '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 { DecorationRangeBehavior, DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, Progress, ProgressLocation, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider'; import { AnnotationProviderBase, TextEditorCorrelationKey } from './annotationProvider';
import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard';
import { TextDocumentComparer } from '../comparers'; import { TextDocumentComparer } from '../comparers';
import { ExtensionKey, IConfig, LineHighlightLocations, themeDefaults } from '../configuration'; 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 { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from '../gitService';
import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider'; import { GutterBlameAnnotationProvider } from './gutterBlameAnnotationProvider';
import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider'; import { HoverBlameAnnotationProvider } from './hoverBlameAnnotationProvider';
import { Keyboard, KeyboardScope, KeyCommand, Keys } from '../keyboard';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider'; import { RecentChangesAnnotationProvider } from './recentChangesAnnotationProvider';
import * as path from 'path'; import * as path from 'path';
@ -64,10 +64,10 @@ export class AnnotationController extends Disposable {
) { ) {
super(() => this.dispose()); super(() => this.dispose());
this._onConfigurationChanged();
this.onConfigurationChanged();
const subscriptions: Disposable[] = [ const subscriptions: Disposable[] = [
workspace.onDidChangeConfiguration(this._onConfigurationChanged, this)
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
]; ];
this._disposable = Disposable.from(...subscriptions); this._disposable = Disposable.from(...subscriptions);
} }
@ -82,7 +82,7 @@ export class AnnotationController extends Disposable {
this._disposable && this._disposable.dispose(); this._disposable && this._disposable.dispose();
} }
private _onConfigurationChanged() {
private onConfigurationChanged() {
let changed = false; let changed = false;
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; const cfg = workspace.getConfiguration().get<IConfig>(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) { if (provider === undefined) {
await setCommandContext(CommandContext.AnnotationStatus, undefined);
await this.detachKeyboardHook();
setCommandContext(CommandContext.AnnotationStatus, undefined);
this.detachKeyboardHook();
} }
else { 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; if (e.blameable || e.editor === undefined) return;
this.clear(e.editor, AnnotationClearReason.BlameabilityChanged); 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) { for (const [key, p] of this._annotationProviders) {
if (!TextDocumentComparer.equals(p.document, e.document)) continue; 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) { 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); this.clearCore(key, AnnotationClearReason.DocumentClosed);
} }
} }
private _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
private onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
// FYI https://github.com/Microsoft/vscode/issues/35602 // FYI https://github.com/Microsoft/vscode/issues/35602
const provider = this.getProvider(e.textEditor); const provider = this.getProvider(e.textEditor);
if (provider === undefined) { if (provider === undefined) {
@ -249,7 +248,7 @@ export class AnnotationController extends Disposable {
provider.restore(e.textEditor); provider.restore(e.textEditor);
} }
private async _onVisibleTextEditorsChanged(editors: TextEditor[]) {
private async onVisibleTextEditorsChanged(editors: TextEditor[]) {
let provider: AnnotationProviderBase | undefined; let provider: AnnotationProviderBase | undefined;
for (const e of editors) { for (const e of editors) {
provider = this.getProvider(e); provider = this.getProvider(e);
@ -391,12 +390,12 @@ export class AnnotationController extends Disposable {
Logger.log(`Add listener registrations for annotations`); Logger.log(`Add listener registrations for annotations`);
const subscriptions: Disposable[] = [ 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); this._annotationsDisposable = Disposable.from(...subscriptions);

+ 22
- 10
src/codeLensController.ts View File

@ -4,7 +4,7 @@ import { Disposable, ExtensionContext, languages, TextEditor, workspace } from '
import { IConfig } from './configuration'; import { IConfig } from './configuration';
import { CommandContext, ExtensionKey, setCommandContext } from './constants'; import { CommandContext, ExtensionKey, setCommandContext } from './constants';
import { GitCodeLensProvider } from './gitCodeLensProvider'; import { GitCodeLensProvider } from './gitCodeLensProvider';
import { GitService } from './gitService';
import { BlameabilityChangeEvent, BlameabilityChangeReason, GitContextTracker, GitService } from './gitService';
import { Logger } from './logger'; import { Logger } from './logger';
export class CodeLensController extends Disposable { export class CodeLensController extends Disposable {
@ -14,14 +14,18 @@ export class CodeLensController extends Disposable {
private _config: IConfig; private _config: IConfig;
private _disposable: Disposable | undefined; private _disposable: Disposable | undefined;
constructor(private context: ExtensionContext, private git: GitService) {
constructor(
private context: ExtensionContext,
private git: GitService,
private gitContextTracker: GitContextTracker
) {
super(() => this.dispose()); super(() => this.dispose());
this._onConfigurationChanged();
this.onConfigurationChanged();
const subscriptions: Disposable[] = [ 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); this._disposable = Disposable.from(...subscriptions);
} }
@ -34,11 +38,14 @@ export class CodeLensController extends Disposable {
this._codeLensProvider = undefined; this._codeLensProvider = undefined;
} }
private _onConfigurationChanged() {
private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
if (!Objects.areEquivalent(cfg.codeLens, this._config && this._config.codeLens)) { 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 (cfg.codeLens.enabled && (cfg.codeLens.recentChange.enabled || cfg.codeLens.authors.enabled)) {
if (this._codeLensProvider) { if (this._codeLensProvider) {
this._codeLensProvider.reset(); this._codeLensProvider.reset();
@ -60,9 +67,14 @@ export class CodeLensController extends Disposable {
this._config = cfg; 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) { toggleCodeLens(editor: TextEditor) {

+ 1
- 1
src/commands/clearFileAnnotations.ts View File

@ -11,7 +11,7 @@ export class ClearFileAnnotationsCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise<any> {
if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined;
if (editor === undefined) return undefined;
try { try {
return this.annotationController.clear(editor); return this.annotationController.clear(editor);

+ 1
- 1
src/commands/showFileBlame.ts View File

@ -17,7 +17,7 @@ export class ShowFileBlameCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowFileBlameCommandArgs = {}): Promise<any> {
if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined;
if (editor === undefined || editor.document.isDirty) return undefined;
try { try {
if (args.type === undefined) { if (args.type === undefined) {

+ 1
- 1
src/commands/showLineBlame.ts View File

@ -16,7 +16,7 @@ export class ShowLineBlameCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ShowLineBlameCommandArgs = {}): Promise<any> {
if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined;
if (editor === undefined || editor.document.isDirty) return undefined;
try { try {
if (args.type === undefined) { if (args.type === undefined) {

+ 10
- 1
src/commands/toggleFileBlame.ts View File

@ -2,6 +2,7 @@
import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode'; import { TextEditor, TextEditorEdit, Uri, window, workspace } from 'vscode';
import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { AnnotationController, FileAnnotationType } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common'; import { Commands, EditorCommand } from './common';
import { UriComparer } from '../comparers';
import { ExtensionKey, IConfig } from '../configuration'; import { ExtensionKey, IConfig } from '../configuration';
import { Logger } from '../logger'; import { Logger } from '../logger';
@ -17,7 +18,15 @@ export class ToggleFileBlameCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleFileBlameCommandArgs = {}): Promise<any> {
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 { try {
if (args.type === undefined) { if (args.type === undefined) {

+ 10
- 1
src/commands/toggleFileRecentChanges.ts View File

@ -2,6 +2,7 @@
import { TextEditor, TextEditorEdit, Uri, window } from 'vscode'; import { TextEditor, TextEditorEdit, Uri, window } from 'vscode';
import { AnnotationController, FileAnnotationType } from '../annotations/annotationController'; import { AnnotationController, FileAnnotationType } from '../annotations/annotationController';
import { Commands, EditorCommand } from './common'; import { Commands, EditorCommand } from './common';
import { UriComparer } from '../comparers';
import { Logger } from '../logger'; import { Logger } from '../logger';
export class ToggleFileRecentChangesCommand extends EditorCommand { export class ToggleFileRecentChangesCommand extends EditorCommand {
@ -11,7 +12,15 @@ export class ToggleFileRecentChangesCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri): Promise<any> {
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 { try {
return this.annotationController.toggleAnnotations(editor, FileAnnotationType.RecentChanges); return this.annotationController.toggleAnnotations(editor, FileAnnotationType.RecentChanges);

+ 1
- 1
src/commands/toggleLineBlame.ts View File

@ -16,7 +16,7 @@ export class ToggleLineBlameCommand extends EditorCommand {
} }
async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise<any> { async execute(editor: TextEditor, edit: TextEditorEdit, uri?: Uri, args: ToggleLineBlameCommandArgs = {}): Promise<any> {
if (editor === undefined || editor.document === undefined || editor.document.isDirty) return undefined;
if (editor === undefined || editor.document.isDirty) return undefined;
try { try {
if (args.type === undefined) { if (args.type === undefined) {

+ 6
- 5
src/comparers.ts View File

@ -8,7 +8,7 @@ abstract class Comparer {
class UriComparer extends Comparer<Uri> { class UriComparer extends Comparer<Uri> {
equals(lhs: Uri | undefined, rhs: Uri | undefined) { 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; if (lhs === undefined || rhs === undefined) return false;
return lhs.scheme === rhs.scheme && lhs.fsPath === rhs.fsPath; return lhs.scheme === rhs.scheme && lhs.fsPath === rhs.fsPath;
@ -18,17 +18,18 @@ class UriComparer extends Comparer {
class TextDocumentComparer extends Comparer<TextDocument> { class TextDocumentComparer extends Comparer<TextDocument> {
equals(lhs: TextDocument | undefined, rhs: TextDocument | undefined) { 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<TextEditor> { class TextEditorComparer extends Comparer<TextEditor> {
equals(lhs: TextEditor | undefined, rhs: TextEditor | undefined, options: { useId: boolean, usePosition: boolean } = { useId: false, usePosition: false }) { 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 (lhs === undefined || rhs === undefined) return false;
if (options.usePosition && (lhs.viewColumn !== rhs.viewColumn)) return false; if (options.usePosition && (lhs.viewColumn !== rhs.viewColumn)) return false;

+ 9
- 2
src/constants.ts View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { commands } from 'vscode';
import { commands, TextEditor } from 'vscode';
export const ExtensionId = 'gitlens'; export const ExtensionId = 'gitlens';
export const ExtensionKey = ExtensionId; export const ExtensionKey = ExtensionId;
@ -44,9 +44,16 @@ export function setCommandContext(key: CommandContext | string, value: any) {
} }
export enum DocumentSchemes { export enum DocumentSchemes {
DebugConsole = 'debug',
File = 'file', File = 'file',
Git = 'git', 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 { export enum GlyphChars {

+ 50
- 44
src/currentLineController.ts View File

@ -6,9 +6,9 @@ import { Annotations, endOfLineIndex } from './annotations/annotations';
import { Commands } from './commands'; import { Commands } from './commands';
import { TextEditorComparer } from './comparers'; import { TextEditorComparer } from './comparers';
import { IConfig, StatusBarCommand } from './configuration'; 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 { BlameabilityChangeEvent, CommitFormatter, GitCommit, GitCommitLine, GitContextTracker, GitService, GitUri, ICommitFormatOptions } from './gitService';
import { Logger } from './logger';
// import { Logger } from './logger';
const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({
after: { after: {
@ -38,24 +38,28 @@ export class CurrentLineController extends Disposable {
private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>; private _updateBlameDebounced: (line: number, editor: TextEditor) => Promise<void>;
private _uri: GitUri; 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()); super(() => this.dispose());
this._updateBlameDebounced = Functions.debounce(this._updateBlame, 250);
this._updateBlameDebounced = Functions.debounce(this.updateBlame, 250);
this._onConfigurationChanged();
this.onConfigurationChanged();
const subscriptions: Disposable[] = [ 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); this._disposable = Disposable.from(...subscriptions);
} }
dispose() { dispose() {
this._clearAnnotations(this._editor, true);
this.clearAnnotations(this._editor, true);
this._trackCurrentLineDisposable && this._trackCurrentLineDisposable.dispose(); this._trackCurrentLineDisposable && this._trackCurrentLineDisposable.dispose();
this._statusBarItem && this._statusBarItem.dispose(); this._statusBarItem && this._statusBarItem.dispose();
@ -63,7 +67,7 @@ export class CurrentLineController extends Disposable {
this._disposable && this._disposable.dispose(); this._disposable && this._disposable.dispose();
} }
private _onConfigurationChanged() {
private onConfigurationChanged() {
const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; const cfg = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
let changed = false; let changed = false;
@ -72,14 +76,14 @@ export class CurrentLineController extends Disposable {
changed = true; changed = true;
this._blameLineAnnotationState = undefined; 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) || 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.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)) { !Objects.areEquivalent(cfg.theme.annotations.line.trailing, this._config && this._config.theme.annotations.line.trailing)) {
changed = true; changed = true;
this._clearAnnotations(this._editor);
this.clearAnnotations(this._editor);
} }
if (!Objects.areEquivalent(cfg.statusBar, this._config && this._config.statusBar)) { 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); const trackCurrentLine = cfg.statusBar.enabled || cfg.blame.line.enabled || (this._blameLineAnnotationState && this._blameLineAnnotationState.enabled);
if (trackCurrentLine && !this._trackCurrentLineDisposable) { if (trackCurrentLine && !this._trackCurrentLineDisposable) {
const subscriptions: Disposable[] = [ 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); this._trackCurrentLineDisposable = Disposable.from(...subscriptions);
} }
@ -121,13 +125,20 @@ export class CurrentLineController extends Disposable {
this.refresh(window.activeTextEditor); 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); this.refresh(editor);
} }
private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
private onBlameabilityChanged(e: BlameabilityChangeEvent) {
if (!this._blameable && !e.blameable) return;
this._blameable = e.blameable; this._blameable = e.blameable;
if (!e.blameable || !this._editor) {
if (!e.blameable || this._editor === undefined) {
this.clear(e.editor); this.clear(e.editor);
return; return;
} }
@ -138,15 +149,15 @@ export class CurrentLineController extends Disposable {
this._updateBlameDebounced(this._editor.selection.active.line, this._editor); this._updateBlameDebounced(this._editor.selection.active.line, this._editor);
} }
private _onDebugSessionStarted() {
private onDebugSessionStarted() {
const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : this._config.blame.line; const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : this._config.blame.line;
if (!state.enabled) return; 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'); this.toggleAnnotations(window.activeTextEditor, state.annotationType, 'debugging');
} }
private _onDebugSessionEnded() {
private onDebugSessionEnded() {
this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose(); this._debugSessionEndDisposable && this._debugSessionEndDisposable.dispose();
this._debugSessionEndDisposable = undefined; this._debugSessionEndDisposable = undefined;
@ -155,16 +166,11 @@ export class CurrentLineController extends Disposable {
this.toggleAnnotations(window.activeTextEditor, this._blameLineAnnotationState.annotationType); 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); this.refresh(window.activeTextEditor);
} }
private async _onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<void> {
private async onTextEditorSelectionChanged(e: TextEditorSelectionChangeEvent): Promise<void> {
// Make sure this is for the editor we are tracking // Make sure this is for the editor we are tracking
if (!this._blameable || !TextEditorComparer.equals(this._editor, e.textEditor)) return; 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._uri = await GitUri.fromUri(e.textEditor.document.uri, this.git);
} }
this._clearAnnotations(e.textEditor);
this.clearAnnotations(e.textEditor);
this._updateBlameDebounced(line, 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 (editor === undefined || editor.document === undefined) return false;
if (!this.git.isTrackable(editor.document.uri)) 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); 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 commit: GitCommit | undefined = undefined;
let commitLine: GitCommitLine | undefined = undefined; let commitLine: GitCommitLine | undefined = undefined;
// Since blame information isn't valid when there are unsaved changes -- don't show any status // 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) { async clear(editor: TextEditor | undefined) {
this._clearAnnotations(editor, true);
this.clearAnnotations(editor, true);
this._statusBarItem && this._statusBarItem.hide(); 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; if (editor === undefined || (!this._isAnnotating && !force)) return;
editor.setDecorations(annotationDecoration, []); editor.setDecorations(annotationDecoration, []);
@ -228,9 +234,9 @@ export class CurrentLineController extends Disposable {
async refresh(editor?: TextEditor) { async refresh(editor?: TextEditor) {
this._currentLine = -1; 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.clear(editor);
this._editor = undefined; this._editor = undefined;
@ -254,8 +260,8 @@ export class CurrentLineController extends Disposable {
// I have no idea why I need this protection -- but it happens // I have no idea why I need this protection -- but it happens
if (editor.document === undefined) return; 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') { 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) { if (!state.enabled || state.annotationType !== type) {
this._blameLineAnnotationState = { enabled: true, annotationType: type, reason: reason }; 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; const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : this._config.blame.line;
this._blameLineAnnotationState = { enabled: !state.enabled, annotationType: type, reason: reason }; 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 cfg = this._config.blame.line;
const state = this._blameLineAnnotationState !== undefined ? this._blameLineAnnotationState : cfg; 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; const cfg = this._config.statusBar;
if (!cfg.enabled || this._statusBarItem === undefined) return; if (!cfg.enabled || this._statusBarItem === undefined) return;

+ 4
- 2
src/extension.ts View File

@ -66,7 +66,7 @@ export async function activate(context: ExtensionContext) {
const annotationController = new AnnotationController(context, git, gitContextTracker); const annotationController = new AnnotationController(context, git, gitContextTracker);
context.subscriptions.push(annotationController); context.subscriptions.push(annotationController);
const codeLensController = new CodeLensController(context, git);
const codeLensController = new CodeLensController(context, git, gitContextTracker);
context.subscriptions.push(codeLensController); context.subscriptions.push(codeLensController);
const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController); const currentLineController = new CurrentLineController(context, git, gitContextTracker, annotationController);
@ -220,7 +220,9 @@ async function notifyOnNewGitLensVersion(context: ExtensionContext, version: str
return; 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 [major, minor] = version.split('.');
const [prevMajor, prevMinor] = previousVersion.split('.'); const [prevMajor, prevMinor] = previousVersion.split('.');

+ 28
- 9
src/git/git.ts View File

@ -59,21 +59,40 @@ async function gitCommand(options: GitCommandOptions, ...args: any[]): Promise
} }
} }
// A map of running git commands -- avoids running duplicate overlaping commands
const pendingCommands: Map<string, Promise<string>> = new Map();
async function gitCommandCore(options: GitCommandOptions, ...args: any[]): Promise<string> { async function gitCommandCore(options: GitCommandOptions, ...args: any[]): Promise<string> {
// Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 // 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 // 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'); 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 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; if (opts.encoding === 'utf8' || opts.encoding === 'binary') return s;

+ 122
- 130
src/git/gitContextTracker.ts View File

@ -1,13 +1,22 @@
'use strict'; '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 { TextDocumentComparer } from '../comparers';
import { CommandContext, setCommandContext } from '../constants';
import { CommandContext, isTextEditor, setCommandContext } from '../constants';
import { GitService, GitUri, RepoChangedReasons } from '../gitService'; 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 { export interface BlameabilityChangeEvent {
blameable: boolean; blameable: boolean;
editor: TextEditor | undefined; editor: TextEditor | undefined;
reason: BlameabilityChangeReason;
} }
export class GitContextTracker extends Disposable { export class GitContextTracker extends Disposable {
@ -17,184 +26,167 @@ export class GitContextTracker extends Disposable {
return this._onDidChangeBlameability.event; 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 _gitEnabled: boolean;
private _isBlameable: boolean;
constructor(private git: GitService) { constructor(private git: GitService) {
super(() => this.dispose()); 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() { dispose() {
this._disposable && this._disposable.dispose(); this._disposable && this._disposable.dispose();
this._documentChangeDisposable && this._documentChangeDisposable.dispose();
} }
_onConfigurationChanged() {
private onConfigurationChanged() {
const gitEnabled = workspace.getConfiguration('git').get<boolean>('enabled', true); const gitEnabled = workspace.getConfiguration('git').get<boolean>('enabled', true);
if (this._gitEnabled !== gitEnabled) { if (this._gitEnabled !== gitEnabled) {
this._gitEnabled = gitEnabled; this._gitEnabled = gitEnabled;
if (this._disposable !== undefined) {
this._disposable.dispose();
this._disposable = undefined;
}
setCommandContext(CommandContext.Enabled, gitEnabled); 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 { 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);
} }
} }

+ 1
- 1
src/git/gitUri.ts View File

@ -110,7 +110,7 @@ export class GitUri extends ((Uri as any) as UriEx) {
const gitUri = git.getGitUriForFile(uri); const gitUri = git.getGitUriForFile(uri);
if (gitUri) return gitUri; 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; static fromFileStatus(status: IGitStatusFile, repoPath: string, original?: boolean): GitUri;

+ 100
- 71
src/gitCodeLensProvider.ts View File

@ -9,18 +9,33 @@ import { Logger } from './logger';
export class GitRecentChangeCodeLens extends CodeLens { 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); super(range);
} }
getBlame(): GitBlameLines | undefined { getBlame(): GitBlameLines | undefined {
return this.blame();
return this.blame && this.blame();
} }
} }
export class GitAuthorsCodeLens extends CodeLens { 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); super(range);
} }
@ -39,7 +54,6 @@ export class GitCodeLensProvider implements CodeLensProvider {
static selector: DocumentSelector = { scheme: DocumentSchemes.File }; static selector: DocumentSelector = { scheme: DocumentSchemes.File };
private _config: IConfig; private _config: IConfig;
private _documentIsDirty: boolean;
constructor(context: ExtensionContext, private git: GitService) { constructor(context: ExtensionContext, private git: GitService) {
this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!; this._config = workspace.getConfiguration().get<IConfig>(ExtensionKey)!;
@ -53,7 +67,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
} }
async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> { async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
this._documentIsDirty = document.isDirty;
const dirty = document.isDirty;
let languageLocations = this._config.codeLens.perLanguageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId); let languageLocations = this._config.codeLens.perLanguageLocations.find(_ => _.language !== undefined && _.language.toLowerCase() === document.languageId);
if (languageLocations == null) { if (languageLocations == null) {
@ -70,44 +84,59 @@ export class GitCodeLensProvider implements CodeLensProvider {
const lenses: CodeLens[] = []; 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; 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<SymbolInformation[]>
]);
}
if (blame === undefined || blame.lines.length === 0) return lenses; if (blame === undefined || blame.lines.length === 0) return lenses;
} }
else { else {
const values = await Promise.all([
blamePromise as Promise<any>,
commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise<any>
]);
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`); 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')) { 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 // 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)) { 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; 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) { 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; 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 valid = false;
let range: Range | undefined; let range: Range | undefined;
@ -128,7 +157,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (valid) { if (valid) {
// Adjust the range to be for the whole file // Adjust the range to be for the whole file
range = document.validateRange(new Range(0, 1000000, 1000000, 1000000));
range = documentRangeFn();
} }
break; break;
@ -140,7 +169,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
if (valid) { if (valid) {
// Adjust the range to be for the whole file // Adjust the range to be for the whole file
if (symbol.location.range.start.line === 0 && symbol.location.range.end.line === 0) { 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; break;
@ -174,9 +203,9 @@ export class GitCodeLensProvider implements CodeLensProvider {
return valid ? range || symbol.location.range : undefined; 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); const line = document.lineAt(symbol.location.range.start);
// Make sure there is only 1 lens per line // 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 // Anchor the code lens to the end of the line -- so they are somewhat consistently placed
let startChar = line.range.end.character - 1; 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++; startChar++;
} }
@ -213,27 +244,25 @@ export class GitCodeLensProvider implements CodeLensProvider {
} }
} }
if (multiline) {
if (multiline && !dirty) {
if (blameForRangeFn === undefined) { 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<CodeLens> { resolveCodeLens(lens: CodeLens, token: CancellationToken): CodeLens | Thenable<CodeLens> {
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<CodeLens>(undefined); return Promise.reject<CodeLens>(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 // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately
let title: string; let title: string;
if (this._documentIsDirty) {
if (lens.dirty) {
if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) { if (this._config.codeLens.recentChange.enabled && this._config.codeLens.authors.enabled) {
title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors; title = this._config.strings.codeLens.unsavedChanges.recentChangeAndAuthors;
} }
@ -258,17 +287,17 @@ export class GitCodeLensProvider implements CodeLensProvider {
} }
switch (this._config.codeLens.recentChange.command) { switch (this._config.codeLens.recentChange.command) {
case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitDetails: return this._applyShowQuickCommitDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitFileDetails: return this._applyShowQuickCommitFileDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this._applyShowQuickCurrentBranchHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickFileHistory: return this._applyShowQuickFileHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ToggleFileBlame: return this._applyToggleFileBlameCommand<GitRecentChangeCodeLens>(title, lens, blame);
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand<GitRecentChangeCodeLens>(title, lens, blame, recentCommit);
case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand<GitRecentChangeCodeLens>(title, lens, blame);
default: return lens; default: return lens;
} }
} }
_resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens {
private resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens {
const blame = lens.getBlame(); const blame = lens.getBlame();
if (blame === undefined) return lens; if (blame === undefined) return lens;
@ -279,17 +308,17 @@ export class GitCodeLensProvider implements CodeLensProvider {
} }
switch (this._config.codeLens.authors.command) { switch (this._config.codeLens.authors.command) {
case CodeLensCommand.DiffWithPrevious: return this._applyDiffWithPreviousCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitDetails: return this._applyShowQuickCommitDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitFileDetails: return this._applyShowQuickCommitFileDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this._applyShowQuickCurrentBranchHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickFileHistory: return this._applyShowQuickFileHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ToggleFileBlame: return this._applyToggleFileBlameCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.DiffWithPrevious: return this.applyDiffWithPreviousCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitDetails: return this.applyShowQuickCommitDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCommitFileDetails: return this.applyShowQuickCommitFileDetailsCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickCurrentBranchHistory: return this.applyShowQuickCurrentBranchHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ShowQuickFileHistory: return this.applyShowQuickFileHistoryCommand<GitAuthorsCodeLens>(title, lens, blame);
case CodeLensCommand.ToggleFileBlame: return this.applyToggleFileBlameCommand<GitAuthorsCodeLens>(title, lens, blame);
default: return lens; default: return lens;
} }
} }
_applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
private applyDiffWithPreviousCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
if (commit === undefined) { if (commit === undefined) {
const blameLine = blame.allLines[lens.range.start.line]; const blameLine = blame.allLines[lens.range.start.line];
commit = blame.commits.get(blameLine.sha); commit = blame.commits.get(blameLine.sha);
@ -299,7 +328,7 @@ export class GitCodeLensProvider implements CodeLensProvider {
title: title, title: title,
command: Commands.DiffWithPrevious, command: Commands.DiffWithPrevious,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath),
Uri.file(lens.uri!.fsPath),
{ {
commit: commit, commit: commit,
range: lens.isFullRange ? undefined : lens.blameRange range: lens.isFullRange ? undefined : lens.blameRange
@ -309,12 +338,12 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
private applyShowQuickCommitDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath),
Uri.file(lens.uri!.fsPath),
{ {
commit, commit,
sha: commit === undefined ? undefined : commit.sha sha: commit === undefined ? undefined : commit.sha
@ -323,12 +352,12 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
private applyShowQuickCommitFileDetailsCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails, command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath),
Uri.file(lens.uri!.fsPath),
{ {
commit, commit,
sha: commit === undefined ? undefined : commit.sha sha: commit === undefined ? undefined : commit.sha
@ -337,21 +366,21 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyShowQuickCurrentBranchHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
private applyShowQuickCurrentBranchHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickCurrentBranchHistory, command: CodeLensCommand.ShowQuickCurrentBranchHistory,
arguments: [Uri.file(lens.uri.fsPath)]
arguments: [Uri.file(lens.uri!.fsPath)]
}; };
return lens; return lens;
} }
_applyShowQuickFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
private applyShowQuickFileHistoryCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines, commit?: GitBlameCommit): T {
lens.command = { lens.command = {
title: title, title: title,
command: CodeLensCommand.ShowQuickFileHistory, command: CodeLensCommand.ShowQuickFileHistory,
arguments: [ arguments: [
Uri.file(lens.uri.fsPath),
Uri.file(lens.uri!.fsPath),
{ {
range: lens.isFullRange ? undefined : lens.blameRange range: lens.isFullRange ? undefined : lens.blameRange
} as ShowQuickFileHistoryCommandArgs } as ShowQuickFileHistoryCommandArgs
@ -360,11 +389,11 @@ export class GitCodeLensProvider implements CodeLensProvider {
return lens; return lens;
} }
_applyToggleFileBlameCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines): T {
private applyToggleFileBlameCommand<T extends GitRecentChangeCodeLens | GitAuthorsCodeLens>(title: string, lens: T, blame: GitBlameLines): T {
lens.command = { lens.command = {
title: title, title: title,
command: Commands.ToggleFileBlame, command: Commands.ToggleFileBlame,
arguments: [Uri.file(lens.uri.fsPath)]
arguments: [Uri.file(lens.uri!.fsPath)]
}; };
return lens; return lens;
} }

+ 56
- 77
src/gitService.ts View File

@ -52,8 +52,8 @@ interface CachedDiff extends CachedItem { }
interface CachedLog extends CachedItem<GitLog> { } interface CachedLog extends CachedItem<GitLog> { }
enum RemoveCacheReason { enum RemoveCacheReason {
DocumentClosed,
DocumentSaved
DocumentChanged,
DocumentClosed
} }
export enum GitRepoSearchBy { export enum GitRepoSearchBy {
@ -66,6 +66,7 @@ export enum GitRepoSearchBy {
} }
export enum RepoChangedReasons { export enum RepoChangedReasons {
CacheReset = 'cache-reset',
Remotes = 'remotes', Remotes = 'remotes',
Repositories = 'Repositories', Repositories = 'Repositories',
Stash = 'stash', Stash = 'stash',
@ -85,11 +86,6 @@ export class GitService extends Disposable {
return this._onDidBlameFail.event; return this._onDidBlameFail.event;
} }
private _onDidChangeGitCache = new EventEmitter<void>();
get onDidChangeGitCache(): Event<void> {
return this._onDidChangeGitCache.event;
}
// TODO: Support multi-root { repo, reasons }[]? // TODO: Support multi-root { repo, reasons }[]?
private _onDidChangeRepo = new EventEmitter<RepoChangedReasons[]>(); private _onDidChangeRepo = new EventEmitter<RepoChangedReasons[]>();
get onDidChangeRepo(): Event<RepoChangedReasons[]> { get onDidChangeRepo(): Event<RepoChangedReasons[]> {
@ -98,18 +94,20 @@ export class GitService extends Disposable {
private _cacheDisposable: Disposable | undefined; private _cacheDisposable: Disposable | undefined;
private _disposable: Disposable | undefined; private _disposable: Disposable | undefined;
private _documentKeyMap: Map<TextDocument, string>;
private _gitCache: Map<string, GitCacheEntry>; private _gitCache: Map<string, GitCacheEntry>;
private _pendingChanges: { repo: boolean } = { repo: false }; private _pendingChanges: { repo: boolean } = { repo: false };
private _remotesCache: Map<string, GitRemote[]>; private _remotesCache: Map<string, GitRemote[]>;
private _repositories: Map<string, Repository | undefined>; private _repositories: Map<string, Repository | undefined>;
private _repositoriesPromise: Promise<void> | undefined; private _repositoriesPromise: Promise<void> | undefined;
private _suspended: boolean = false; private _suspended: boolean = false;
private _trackedCache: Map<string, boolean>;
private _trackedCache: Map<string, boolean | Promise<boolean>>;
private _versionedUriCache: Map<string, UriCacheEntry>; private _versionedUriCache: Map<string, UriCacheEntry>;
constructor() { constructor() {
super(() => this.dispose()); super(() => this.dispose());
this._documentKeyMap = new Map();
this._gitCache = new Map(); this._gitCache = new Map();
this._remotesCache = new Map(); this._remotesCache = new Map();
this._repositories = new Map(); this._repositories = new Map();
@ -136,6 +134,7 @@ export class GitService extends Disposable {
this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose();
this._cacheDisposable = undefined; this._cacheDisposable = undefined;
this._documentKeyMap.clear();
this._gitCache.clear(); this._gitCache.clear();
this._remotesCache.clear(); this._remotesCache.clear();
this._trackedCache.clear(); this._trackedCache.clear();
@ -164,9 +163,8 @@ export class GitService extends Disposable {
this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose();
const subscriptions: Disposable[] = [ 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); this._cacheDisposable = Disposable.from(...subscriptions);
} }
@ -174,17 +172,21 @@ export class GitService extends Disposable {
this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose();
this._cacheDisposable = undefined; this._cacheDisposable = undefined;
this._documentKeyMap.clear();
this._gitCache.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; this.config = cfg;
if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) { if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) {
this._gitCache.clear(); this._gitCache.clear();
this.fireGitCacheChange();
this.fireRepoChange(RepoChangedReasons.CacheReset);
} }
} }
@ -258,19 +260,28 @@ export class GitService extends Disposable {
} }
private onTextDocumentChanged(e: TextDocumentChangeEvent) { 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) { private onRepoChanged(uri: Uri) {
@ -284,21 +295,6 @@ export class GitService extends Disposable {
this._trackedCache.clear(); this._trackedCache.clear();
this.fireRepoChange(); 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; private _fireRepoChangeDebounced: (() => void) | undefined = undefined;
@ -306,7 +302,7 @@ export class GitService extends Disposable {
private fireRepoChange(reason: RepoChangedReasons = RepoChangedReasons.Unknown) { private fireRepoChange(reason: RepoChangedReasons = RepoChangedReasons.Unknown) {
if (this._fireRepoChangeDebounced === undefined) { if (this._fireRepoChangeDebounced === undefined) {
this._fireRepoChangeDebounced = Functions.debounce(this.fireRepoChangeCore, 50);
this._fireRepoChangeDebounced = Functions.debounce(this.fireRepoChangeCore, 250);
} }
if (!this._repoChangedReasons.includes(reason)) { if (!this._repoChangedReasons.includes(reason)) {
@ -328,27 +324,6 @@ export class GitService extends Disposable {
this._onDidChangeRepo.fire(reasons); 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<Repository[]> { public async getRepositories(): Promise<Repository[]> {
if (this._repositoriesPromise !== undefined) { if (this._repositoriesPromise !== undefined) {
await this._repositoriesPromise; await this._repositoriesPromise;
@ -981,6 +956,10 @@ export class GitService extends Disposable {
const folder = workspace.getWorkspaceFolder(filePathOrUri); const folder = workspace.getWorkspaceFolder(filePathOrUri);
if (folder !== undefined) { if (folder !== undefined) {
if (this._repositoriesPromise !== undefined) {
await this._repositoriesPromise;
}
const rp = this._repositories.get(folder.uri.fsPath); const rp = this._repositories.get(folder.uri.fsPath);
if (rp !== undefined) return rp.path; if (rp !== undefined) return rp.path;
} }
@ -988,10 +967,8 @@ export class GitService extends Disposable {
return this.getRepoPathCore(filePathOrUri.fsPath, false); return this.getRepoPathCore(filePathOrUri.fsPath, false);
} }
private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise<string | undefined> {
const rp = await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath));
console.log(filePath, rp);
return rp;
private getRepoPathCore(filePath: string, isDirectory: boolean): Promise<string | undefined> {
return Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath));
} }
async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> { async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
@ -1076,9 +1053,9 @@ export class GitService extends Disposable {
return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLensGit; return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLensGit;
} }
async isTracked(fileName: string, repoPath: string | undefined): Promise<boolean>;
async isTracked(fileName: string, repoPath?: string): Promise<boolean>;
async isTracked(uri: GitUri): Promise<boolean>; async isTracked(uri: GitUri): Promise<boolean>;
async isTracked(fileNameOrUri: string | GitUri, repoPath?: string | undefined): Promise<boolean> {
async isTracked(fileNameOrUri: string | GitUri, repoPath?: string): Promise<boolean> {
let cacheKey: string; let cacheKey: string;
let fileName: string; let fileName: string;
if (typeof fileNameOrUri === 'string') { if (typeof fileNameOrUri === 'string') {
@ -1096,15 +1073,24 @@ export class GitService extends Disposable {
Logger.log(`isTracked('${fileName}', '${repoPath}')`); Logger.log(`isTracked('${fileName}', '${repoPath}')`);
let tracked = this._trackedCache.get(cacheKey); 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); this._trackedCache.set(cacheKey, tracked);
return 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) { openDiffTool(repoPath: string, uri: Uri, staged: boolean) {
Logger.log(`openDiffTool('${repoPath}', '${uri}', ${staged})`); Logger.log(`openDiffTool('${repoPath}', '${uri}', ${staged})`);
@ -1149,13 +1135,6 @@ export class GitService extends Disposable {
return Git.gitInfo().version; return Git.gitInfo().version;
} }
// static async getRepoPath(cwd: string | undefined): Promise<string> {
// const repoPath = await Git.getRepoPath(cwd);
// if (!repoPath) return '';
// return repoPath;
// }
static fromGitContentUri(uri: Uri): IGitUriData { static fromGitContentUri(uri: Uri): IGitUriData {
if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`);
return GitService._fromGitContentUri<IGitUriData>(uri); return GitService._fromGitContentUri<IGitUriData>(uri);

+ 11
- 6
src/logger.ts View File

@ -42,21 +42,21 @@ export class Logger {
static log(message?: any, ...params: any[]): void { static log(message?: any, ...params: any[]): void {
if (debug) { if (debug) {
console.log(ConsolePrefix, message, ...params);
console.log(this.timestamp, ConsolePrefix, message, ...params);
} }
if (level === OutputLevel.Verbose) { 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 { static error(ex: Error, classOrMethod?: string, ...params: any[]): void {
if (debug) { if (debug) {
console.error(ConsolePrefix, classOrMethod, ex, ...params);
console.error(this.timestamp, ConsolePrefix, classOrMethod, ex, ...params);
} }
if (level !== OutputLevel.Silent) { 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); Telemetry.trackException(ex);
@ -64,11 +64,16 @@ export class Logger {
static warn(message?: any, ...params: any[]): void { static warn(message?: any, ...params: any[]): void {
if (debug) { if (debug) {
console.warn(ConsolePrefix, message, ...params);
console.warn(this.timestamp, ConsolePrefix, message, ...params);
} }
if (level !== OutputLevel.Silent) { 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()}]`;
}
} }

+ 1
- 2
src/views/gitExplorer.ts View File

@ -344,8 +344,7 @@ export class GitExplorer implements TreeDataProvider {
} }
if (enabled) { 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); this.context.subscriptions.push(this._autoRefreshDisposable);
} }
} }

Loading…
Cancel
Save