From bb460c482aedf3ace99320c4467787f2a49f8ee0 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 24 Oct 2020 02:29:24 -0400 Subject: [PATCH] Ensures tracked documents are always initialized --- src/trackers/documentTracker.ts | 118 +++++++++++++++++----------------------- src/trackers/trackedDocument.ts | 64 ++++++++++++---------- 2 files changed, 84 insertions(+), 98 deletions(-) diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index 6fe769e..cc8e2c5 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -64,7 +64,7 @@ export class DocumentTracker implements Disposable { private _dirtyIdleTriggerDelay!: number; private readonly _disposable: Disposable | undefined; - private readonly _documentMap = new Map>(); + private readonly _documentMap = new Map>>(); constructor() { this._disposable = Disposable.from( @@ -76,20 +76,20 @@ export class DocumentTracker implements Disposable { workspace.onDidSaveTextDocument(this.onTextDocumentSaved, this), ); - this.onConfigurationChanged(configuration.initializingChangeEvent); + void this.onConfigurationChanged(configuration.initializingChangeEvent); } dispose() { this._disposable?.dispose(); - this.clear(); + void this.clear(); } initialize() { - this.onActiveTextEditorChanged(window.activeTextEditor); + void this.onActiveTextEditorChanged(window.activeTextEditor); } - private onConfigurationChanged(e: ConfigurationChangeEvent) { + private async onConfigurationChanged(e: ConfigurationChangeEvent) { // Only rest the cached state if we aren't initializing if ( !configuration.initializing(e) && @@ -97,7 +97,7 @@ export class DocumentTracker implements Disposable { configuration.changed(e, 'advanced', 'caching', 'enabled')) ) { for (const d of this._documentMap.values()) { - d.reset('config'); + (await d).reset('config'); } } @@ -108,15 +108,15 @@ export class DocumentTracker implements Disposable { } private _timer: NodeJS.Timer | undefined; - private onActiveTextEditorChanged(editor: TextEditor | undefined) { - if (editor !== undefined && !isTextEditor(editor)) return; + private async onActiveTextEditorChanged(editor: TextEditor | undefined) { + if (editor != null && !isTextEditor(editor)) return; - if (this._timer !== undefined) { + if (this._timer != null) { clearTimeout(this._timer); this._timer = undefined; } - if (editor === undefined) { + if (editor == null) { this._timer = setTimeout(() => { this._timer = undefined; @@ -127,34 +127,30 @@ export class DocumentTracker implements Disposable { } const doc = this._documentMap.get(editor.document); - if (doc !== undefined) { - doc.activate(); + if (doc != null) { + (await doc).activate(); return; } // No need to activate this, as it is implicit in initialization if currently active - this.addCore(editor.document); + void this.addCore(editor.document); } - private onTextDocumentChanged(e: TextDocumentChangeEvent) { + private async onTextDocumentChanged(e: TextDocumentChangeEvent) { const { scheme } = e.document.uri; if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) { return; } - let doc = this._documentMap.get(e.document); - if (doc === undefined) { - doc = this.addCore(e.document); - } - + const doc = await (this._documentMap.get(e.document) ?? this.addCore(e.document)); doc.reset('document'); const dirty = e.document.isDirty; const editor = window.activeTextEditor; // If we have an idle tracker, either reset or cancel it - if (this._dirtyIdleTriggeredDebounced !== undefined) { + if (this._dirtyIdleTriggeredDebounced != null) { if (dirty) { this._dirtyIdleTriggeredDebounced({ editor: editor!, document: doc }); } else { @@ -163,7 +159,7 @@ export class DocumentTracker implements Disposable { } // Only fire change events for the active document - if (editor !== undefined && editor.document === e.document) { + if (editor?.document === e.document) { this._onDidChangeContent.fire({ editor: editor, document: doc, contentChanges: e.contentChanges }); } @@ -173,24 +169,25 @@ export class DocumentTracker implements Disposable { doc.dirty = dirty; // Only fire state change events for the active document - if (editor === undefined || editor.document !== e.document) return; + if (editor == null || editor.document !== e.document) return; this.fireDocumentDirtyStateChanged({ editor: editor, document: doc, dirty: doc.dirty }); } - private onTextDocumentClosed(document: TextDocument) { + private async onTextDocumentClosed(document: TextDocument) { const doc = this._documentMap.get(document); - if (doc === undefined) return; + if (doc == null) return; - doc.dispose(); this._documentMap.delete(document); - this._documentMap.delete(doc.key); + this._documentMap.delete(GitUri.toKey(document.uri)); + + (await doc).dispose(); } - private onTextDocumentSaved(document: TextDocument) { + private async onTextDocumentSaved(document: TextDocument) { const doc = this._documentMap.get(document); - if (doc !== undefined) { - void doc.update({ forceBlameChange: true }); + if (doc != null) { + void (await doc).update({ forceBlameChange: true }); return; } @@ -213,31 +210,30 @@ export class DocumentTracker implements Disposable { add(document: TextDocument): Promise>; add(uri: Uri): Promise>; add(documentOrId: TextDocument | Uri): Promise> { - return this._add(documentOrId); + const doc = this._add(documentOrId); + return doc; } - clear() { + async clear() { for (const d of this._documentMap.values()) { - d.dispose(); + (await d).dispose(); } this._documentMap.clear(); } - get(fileName: string): Promise | undefined>; - get(document: TextDocument): Promise | undefined>; - get(uri: Uri): Promise | undefined>; - get(documentOrId: string | TextDocument | Uri): Promise | undefined> { - return this._get(documentOrId); + get(fileName: string): Promise> | undefined; + get(document: TextDocument): Promise> | undefined; + get(uri: Uri): Promise> | undefined; + get(documentOrId: string | TextDocument | Uri): Promise> | undefined { + const doc = this._get(documentOrId); + return doc; } async getOrAdd(document: TextDocument): Promise>; async getOrAdd(uri: Uri): Promise>; async getOrAdd(documentOrId: TextDocument | Uri): Promise> { - let doc = await this._get(documentOrId); - if (doc === undefined) { - doc = await this._add(documentOrId); - } + const doc = this._get(documentOrId) ?? this._add(documentOrId); return doc; } @@ -269,7 +265,7 @@ export class DocumentTracker implements Disposable { document = new MissingRevisionTextDocument(documentOrId); // const [fileName, repoPath] = await Container.git.findWorkingFileName(documentOrId, undefined, ref); - // if (fileName === undefined) throw new Error(`Failed to add tracking for document: ${documentOrId}`); + // if (fileName == null) throw new Error(`Failed to add tracking for document: ${documentOrId}`); // documentOrId = await workspace.openTextDocument(path.resolve(repoPath!, fileName)); } else { @@ -283,12 +279,10 @@ export class DocumentTracker implements Disposable { } const doc = this.addCore(document); - await doc.ensureInitialized(); - return doc; } - private async _get(documentOrId: string | TextDocument | Uri) { + private _get(documentOrId: string | TextDocument | Uri) { if (GitUri.is(documentOrId)) { documentOrId = GitUri.toKey(documentOrId.documentUri({ useVersionedPath: true })); } else if (typeof documentOrId === 'string' || documentOrId instanceof Uri) { @@ -296,19 +290,17 @@ export class DocumentTracker implements Disposable { } const doc = this._documentMap.get(documentOrId); - if (doc === undefined) return undefined; - - await doc.ensureInitialized(); return doc; } - private addCore(document: TextDocument): TrackedDocument { + private async addCore(document: TextDocument): Promise> { const key = GitUri.toKey(document.uri); // Always start out false, so we will fire the event if needed - const doc = new TrackedDocument(document, key, false, { + const doc = TrackedDocument.create(document, key, false, { onDidBlameStateChange: (e: DocumentBlameStateChangeEvent) => this._onDidChangeBlameState.fire(e), }); + this._documentMap.set(document, doc); this._documentMap.set(key, doc); @@ -323,29 +315,18 @@ export class DocumentTracker implements Disposable { | undefined; private fireDocumentDirtyStateChanged(e: DocumentDirtyStateChangeEvent) { if (e.dirty) { - setImmediate(async () => { - if (this._dirtyStateChangedDebounced !== undefined) { - this._dirtyStateChangedDebounced.cancel(); - } - + setImmediate(() => { + this._dirtyStateChangedDebounced?.cancel(); if (window.activeTextEditor !== e.editor) return; - await e.document.ensureInitialized(); this._onDidChangeDirtyState.fire(e); }); if (this._dirtyIdleTriggerDelay > 0) { - if (this._dirtyIdleTriggeredDebounced === undefined) { + if (this._dirtyIdleTriggeredDebounced == null) { this._dirtyIdleTriggeredDebounced = Functions.debounce( - async (e: DocumentDirtyIdleTriggerEvent) => { - if ( - this._dirtyIdleTriggeredDebounced !== undefined && - this._dirtyIdleTriggeredDebounced.pending!() - ) { - return; - } - - await e.document.ensureInitialized(); + (e: DocumentDirtyIdleTriggerEvent) => { + if (this._dirtyIdleTriggeredDebounced?.pending!()) return; e.document.isDirtyIdle = true; this._onDidTriggerDirtyIdle.fire(e); @@ -361,11 +342,10 @@ export class DocumentTracker implements Disposable { return; } - if (this._dirtyStateChangedDebounced === undefined) { - this._dirtyStateChangedDebounced = Functions.debounce(async (e: DocumentDirtyStateChangeEvent) => { + if (this._dirtyStateChangedDebounced == null) { + this._dirtyStateChangedDebounced = Functions.debounce((e: DocumentDirtyStateChangeEvent) => { if (window.activeTextEditor !== e.editor) return; - await e.document.ensureInitialized(); this._onDidChangeDirtyState.fire(e); }, 250); } diff --git a/src/trackers/trackedDocument.ts b/src/trackers/trackedDocument.ts index b622c30..a0653af 100644 --- a/src/trackers/trackedDocument.ts +++ b/src/trackers/trackedDocument.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Disposable, Event, EventEmitter, TextDocument, TextEditor, Uri } from 'vscode'; +import { Disposable, Event, EventEmitter, TextDocument, TextEditor } from 'vscode'; import { ContextKeys, getEditorIfActive, isActiveDocument, setContext } from '../constants'; import { Container } from '../container'; import { GitRevision, Repository, RepositoryChange, RepositoryChangeEvent } from '../git/git'; @@ -14,6 +14,17 @@ export interface DocumentBlameStateChangeEvent { } export class TrackedDocument implements Disposable { + static async create( + document: TextDocument, + key: string, + dirty: boolean, + eventDelegates: { onDidBlameStateChange(e: DocumentBlameStateChangeEvent): void }, + ) { + const doc = new TrackedDocument(document, key, dirty, eventDelegates); + await doc.initialize(); + return doc; + } + private _onDidBlameStateChange = new EventEmitter>(); get onDidBlameStateChange(): Event> { return this._onDidBlameStateChange.event; @@ -23,17 +34,15 @@ export class TrackedDocument implements Disposable { private _disposable: Disposable | undefined; private _disposed: boolean = false; - private _repo: Promise; + private _repo: Repository | undefined; private _uri!: GitUri; - constructor( + private constructor( private readonly _document: TextDocument, public readonly key: string, public dirty: boolean, private _eventDelegates: { onDidBlameStateChange(e: DocumentBlameStateChangeEvent): void }, - ) { - this._repo = this.initialize(_document.uri); - } + ) {} dispose() { this._disposed = true; @@ -41,10 +50,13 @@ export class TrackedDocument implements Disposable { this._disposable?.dispose(); } - private async initialize(uri: Uri): Promise { + private initializing = true; + private async initialize(): Promise { + const uri = this._document.uri; + // Since there is a bit of a chicken & egg problem with the DocumentTracker and the GitService, wait for the GitService to load if it isn't - if (Container.git === undefined) { - if (!(await Functions.waitUntil(() => Container.git !== undefined, 2000))) { + if (Container.git == null) { + if (!(await Functions.waitUntil(() => Container.git != null, 2000))) { Logger.log( `TrackedDocument.initialize(${uri.toString(true)})`, 'Timed out waiting for the GitService to start', @@ -57,13 +69,16 @@ export class TrackedDocument implements Disposable { if (this._disposed) return undefined; const repo = await Container.git.getRepository(this._uri); + this._repo = repo; if (this._disposed) return undefined; - if (repo !== undefined) { + if (repo != null) { this._disposable = repo.onDidChange(this.onRepositoryChanged, this); } - await this.update({ initializing: true, repo: repo }); + await this.update(); + + this.initializing = false; return repo; } @@ -107,9 +122,7 @@ export class TrackedDocument implements Disposable { } get isRevision() { - return this._uri !== undefined - ? Boolean(this._uri.sha) && this._uri.sha !== GitRevision.deletedOrMissing - : false; + return this._uri != null ? Boolean(this._uri.sha) && this._uri.sha !== GitRevision.deletedOrMissing : false; } private _isTracked: boolean = false; @@ -129,10 +142,6 @@ export class TrackedDocument implements Disposable { void setContext(ContextKeys.ActiveFileStatus, this.getStatus()); } - async ensureInitialized() { - await this._repo; - } - is(document: TextDocument) { return document === this._document; } @@ -141,7 +150,7 @@ export class TrackedDocument implements Disposable { this._blameFailed = false; this._isDirtyIdle = false; - if (this.state === undefined) return; + if (this.state == null) return; // // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) // if (!this.state.hasErrors) { @@ -171,8 +180,8 @@ export class TrackedDocument implements Disposable { this._forceDirtyStateChangeOnNextDocumentChange = true; } - async update(options: { forceBlameChange?: boolean; initializing?: boolean; repo?: Repository } = {}) { - if (this._disposed || this._uri === undefined) { + async update({ forceBlameChange }: { forceBlameChange?: boolean } = {}) { + if (this._disposed || this._uri == null) { this._hasRemotes = false; this._isTracked = false; @@ -182,30 +191,27 @@ export class TrackedDocument implements Disposable { this._isDirtyIdle = false; const active = getEditorIfActive(this._document); - const wasBlameable = options.forceBlameChange ? undefined : this.isBlameable; + const wasBlameable = forceBlameChange ? undefined : this.isBlameable; this._isTracked = await Container.git.isTracked(this._uri); let repo = undefined; if (this._isTracked) { - repo = options.repo; - if (repo === undefined) { - repo = await this._repo; - } + repo = this._repo; } - if (repo !== undefined) { + if (repo != null) { this._hasRemotes = await repo.hasRemotes(); } else { this._hasRemotes = false; } - if (active !== undefined) { + if (active != null) { const blameable = this.isBlameable; void setContext(ContextKeys.ActiveFileStatus, this.getStatus()); - if (!options.initializing && wasBlameable !== blameable) { + if (!this.initializing && wasBlameable !== blameable) { const e: DocumentBlameStateChangeEvent = { editor: active, document: this, blameable: blameable }; this._onDidBlameStateChange.fire(e); this._eventDelegates.onDidBlameStateChange(e);