Browse Source

Ensures tracked documents are always initialized

main
Eric Amodio 4 years ago
parent
commit
bb460c482a
2 changed files with 84 additions and 98 deletions
  1. +49
    -69
      src/trackers/documentTracker.ts
  2. +35
    -29
      src/trackers/trackedDocument.ts

+ 49
- 69
src/trackers/documentTracker.ts View File

@ -64,7 +64,7 @@ export class DocumentTracker implements Disposable {
private _dirtyIdleTriggerDelay!: number; private _dirtyIdleTriggerDelay!: number;
private readonly _disposable: Disposable | undefined; private readonly _disposable: Disposable | undefined;
private readonly _documentMap = new Map<TextDocument | string, TrackedDocument<T>>();
private readonly _documentMap = new Map<TextDocument | string, Promise<TrackedDocument<T>>>();
constructor() { constructor() {
this._disposable = Disposable.from( this._disposable = Disposable.from(
@ -76,20 +76,20 @@ export class DocumentTracker implements Disposable {
workspace.onDidSaveTextDocument(this.onTextDocumentSaved, this), workspace.onDidSaveTextDocument(this.onTextDocumentSaved, this),
); );
this.onConfigurationChanged(configuration.initializingChangeEvent);
void this.onConfigurationChanged(configuration.initializingChangeEvent);
} }
dispose() { dispose() {
this._disposable?.dispose(); this._disposable?.dispose();
this.clear();
void this.clear();
} }
initialize() { 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 // Only rest the cached state if we aren't initializing
if ( if (
!configuration.initializing(e) && !configuration.initializing(e) &&
@ -97,7 +97,7 @@ export class DocumentTracker implements Disposable {
configuration.changed(e, 'advanced', 'caching', 'enabled')) configuration.changed(e, 'advanced', 'caching', 'enabled'))
) { ) {
for (const d of this._documentMap.values()) { 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 _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); clearTimeout(this._timer);
this._timer = undefined; this._timer = undefined;
} }
if (editor === undefined) {
if (editor == null) {
this._timer = setTimeout(() => { this._timer = setTimeout(() => {
this._timer = undefined; this._timer = undefined;
@ -127,34 +127,30 @@ export class DocumentTracker implements Disposable {
} }
const doc = this._documentMap.get(editor.document); const doc = this._documentMap.get(editor.document);
if (doc !== undefined) {
doc.activate();
if (doc != null) {
(await doc).activate();
return; return;
} }
// No need to activate this, as it is implicit in initialization if currently active // 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; const { scheme } = e.document.uri;
if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) { if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) {
return; 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'); doc.reset('document');
const dirty = e.document.isDirty; const dirty = e.document.isDirty;
const editor = window.activeTextEditor; const editor = window.activeTextEditor;
// If we have an idle tracker, either reset or cancel it // If we have an idle tracker, either reset or cancel it
if (this._dirtyIdleTriggeredDebounced !== undefined) {
if (this._dirtyIdleTriggeredDebounced != null) {
if (dirty) { if (dirty) {
this._dirtyIdleTriggeredDebounced({ editor: editor!, document: doc }); this._dirtyIdleTriggeredDebounced({ editor: editor!, document: doc });
} else { } else {
@ -163,7 +159,7 @@ export class DocumentTracker implements Disposable {
} }
// Only fire change events for the active document // 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 }); this._onDidChangeContent.fire({ editor: editor, document: doc, contentChanges: e.contentChanges });
} }
@ -173,24 +169,25 @@ export class DocumentTracker implements Disposable {
doc.dirty = dirty; doc.dirty = dirty;
// Only fire state change events for the active document // 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 }); this.fireDocumentDirtyStateChanged({ editor: editor, document: doc, dirty: doc.dirty });
} }
private onTextDocumentClosed(document: TextDocument) {
private async onTextDocumentClosed(document: TextDocument) {
const doc = this._documentMap.get(document); const doc = this._documentMap.get(document);
if (doc === undefined) return;
if (doc == null) return;
doc.dispose();
this._documentMap.delete(document); 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); const doc = this._documentMap.get(document);
if (doc !== undefined) {
void doc.update({ forceBlameChange: true });
if (doc != null) {
void (await doc).update({ forceBlameChange: true });
return; return;
} }
@ -213,31 +210,30 @@ export class DocumentTracker implements Disposable {
add(document: TextDocument): Promise<TrackedDocument<T>>; add(document: TextDocument): Promise<TrackedDocument<T>>;
add(uri: Uri): Promise<TrackedDocument<T>>; add(uri: Uri): Promise<TrackedDocument<T>>;
add(documentOrId: TextDocument | Uri): Promise<TrackedDocument<T>> { add(documentOrId: TextDocument | Uri): Promise<TrackedDocument<T>> {
return this._add(documentOrId);
const doc = this._add(documentOrId);
return doc;
} }
clear() {
async clear() {
for (const d of this._documentMap.values()) { for (const d of this._documentMap.values()) {
d.dispose();
(await d).dispose();
} }
this._documentMap.clear(); this._documentMap.clear();
} }
get(fileName: string): Promise<TrackedDocument<T> | undefined>;
get(document: TextDocument): Promise<TrackedDocument<T> | undefined>;
get(uri: Uri): Promise<TrackedDocument<T> | undefined>;
get(documentOrId: string | TextDocument | Uri): Promise<TrackedDocument<T> | undefined> {
return this._get(documentOrId);
get(fileName: string): Promise<TrackedDocument<T>> | undefined;
get(document: TextDocument): Promise<TrackedDocument<T>> | undefined;
get(uri: Uri): Promise<TrackedDocument<T>> | undefined;
get(documentOrId: string | TextDocument | Uri): Promise<TrackedDocument<T>> | undefined {
const doc = this._get(documentOrId);
return doc;
} }
async getOrAdd(document: TextDocument): Promise<TrackedDocument<T>>; async getOrAdd(document: TextDocument): Promise<TrackedDocument<T>>;
async getOrAdd(uri: Uri): Promise<TrackedDocument<T>>; async getOrAdd(uri: Uri): Promise<TrackedDocument<T>>;
async getOrAdd(documentOrId: TextDocument | Uri): Promise<TrackedDocument<T>> { async getOrAdd(documentOrId: TextDocument | Uri): Promise<TrackedDocument<T>> {
let doc = await this._get(documentOrId);
if (doc === undefined) {
doc = await this._add(documentOrId);
}
const doc = this._get(documentOrId) ?? this._add(documentOrId);
return doc; return doc;
} }
@ -269,7 +265,7 @@ export class DocumentTracker implements Disposable {
document = new MissingRevisionTextDocument(documentOrId); document = new MissingRevisionTextDocument(documentOrId);
// const [fileName, repoPath] = await Container.git.findWorkingFileName(documentOrId, undefined, ref); // 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)); // documentOrId = await workspace.openTextDocument(path.resolve(repoPath!, fileName));
} else { } else {
@ -283,12 +279,10 @@ export class DocumentTracker implements Disposable {
} }
const doc = this.addCore(document); const doc = this.addCore(document);
await doc.ensureInitialized();
return doc; return doc;
} }
private async _get(documentOrId: string | TextDocument | Uri) {
private _get(documentOrId: string | TextDocument | Uri) {
if (GitUri.is(documentOrId)) { if (GitUri.is(documentOrId)) {
documentOrId = GitUri.toKey(documentOrId.documentUri({ useVersionedPath: true })); documentOrId = GitUri.toKey(documentOrId.documentUri({ useVersionedPath: true }));
} else if (typeof documentOrId === 'string' || documentOrId instanceof Uri) { } else if (typeof documentOrId === 'string' || documentOrId instanceof Uri) {
@ -296,19 +290,17 @@ export class DocumentTracker implements Disposable {
} }
const doc = this._documentMap.get(documentOrId); const doc = this._documentMap.get(documentOrId);
if (doc === undefined) return undefined;
await doc.ensureInitialized();
return doc; return doc;
} }
private addCore(document: TextDocument): TrackedDocument<T> {
private async addCore(document: TextDocument): Promise<TrackedDocument<T>> {
const key = GitUri.toKey(document.uri); const key = GitUri.toKey(document.uri);
// Always start out false, so we will fire the event if needed // Always start out false, so we will fire the event if needed
const doc = new TrackedDocument<T>(document, key, false, {
const doc = TrackedDocument.create<T>(document, key, false, {
onDidBlameStateChange: (e: DocumentBlameStateChangeEvent<T>) => this._onDidChangeBlameState.fire(e), onDidBlameStateChange: (e: DocumentBlameStateChangeEvent<T>) => this._onDidChangeBlameState.fire(e),
}); });
this._documentMap.set(document, doc); this._documentMap.set(document, doc);
this._documentMap.set(key, doc); this._documentMap.set(key, doc);
@ -323,29 +315,18 @@ export class DocumentTracker implements Disposable {
| undefined; | undefined;
private fireDocumentDirtyStateChanged(e: DocumentDirtyStateChangeEvent<T>) { private fireDocumentDirtyStateChanged(e: DocumentDirtyStateChangeEvent<T>) {
if (e.dirty) { if (e.dirty) {
setImmediate(async () => {
if (this._dirtyStateChangedDebounced !== undefined) {
this._dirtyStateChangedDebounced.cancel();
}
setImmediate(() => {
this._dirtyStateChangedDebounced?.cancel();
if (window.activeTextEditor !== e.editor) return; if (window.activeTextEditor !== e.editor) return;
await e.document.ensureInitialized();
this._onDidChangeDirtyState.fire(e); this._onDidChangeDirtyState.fire(e);
}); });
if (this._dirtyIdleTriggerDelay > 0) { if (this._dirtyIdleTriggerDelay > 0) {
if (this._dirtyIdleTriggeredDebounced === undefined) {
if (this._dirtyIdleTriggeredDebounced == null) {
this._dirtyIdleTriggeredDebounced = Functions.debounce( this._dirtyIdleTriggeredDebounced = Functions.debounce(
async (e: DocumentDirtyIdleTriggerEvent<T>) => {
if (
this._dirtyIdleTriggeredDebounced !== undefined &&
this._dirtyIdleTriggeredDebounced.pending!()
) {
return;
}
await e.document.ensureInitialized();
(e: DocumentDirtyIdleTriggerEvent<T>) => {
if (this._dirtyIdleTriggeredDebounced?.pending!()) return;
e.document.isDirtyIdle = true; e.document.isDirtyIdle = true;
this._onDidTriggerDirtyIdle.fire(e); this._onDidTriggerDirtyIdle.fire(e);
@ -361,11 +342,10 @@ export class DocumentTracker implements Disposable {
return; return;
} }
if (this._dirtyStateChangedDebounced === undefined) {
this._dirtyStateChangedDebounced = Functions.debounce(async (e: DocumentDirtyStateChangeEvent<T>) => {
if (this._dirtyStateChangedDebounced == null) {
this._dirtyStateChangedDebounced = Functions.debounce((e: DocumentDirtyStateChangeEvent<T>) => {
if (window.activeTextEditor !== e.editor) return; if (window.activeTextEditor !== e.editor) return;
await e.document.ensureInitialized();
this._onDidChangeDirtyState.fire(e); this._onDidChangeDirtyState.fire(e);
}, 250); }, 250);
} }

+ 35
- 29
src/trackers/trackedDocument.ts View File

@ -1,5 +1,5 @@
'use strict'; '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 { ContextKeys, getEditorIfActive, isActiveDocument, setContext } from '../constants';
import { Container } from '../container'; import { Container } from '../container';
import { GitRevision, Repository, RepositoryChange, RepositoryChangeEvent } from '../git/git'; import { GitRevision, Repository, RepositoryChange, RepositoryChangeEvent } from '../git/git';
@ -14,6 +14,17 @@ export interface DocumentBlameStateChangeEvent {
} }
export class TrackedDocument<T> implements Disposable { export class TrackedDocument<T> implements Disposable {
static async create<T>(
document: TextDocument,
key: string,
dirty: boolean,
eventDelegates: { onDidBlameStateChange(e: DocumentBlameStateChangeEvent<T>): void },
) {
const doc = new TrackedDocument(document, key, dirty, eventDelegates);
await doc.initialize();
return doc;
}
private _onDidBlameStateChange = new EventEmitter<DocumentBlameStateChangeEvent<T>>(); private _onDidBlameStateChange = new EventEmitter<DocumentBlameStateChangeEvent<T>>();
get onDidBlameStateChange(): Event<DocumentBlameStateChangeEvent<T>> { get onDidBlameStateChange(): Event<DocumentBlameStateChangeEvent<T>> {
return this._onDidBlameStateChange.event; return this._onDidBlameStateChange.event;
@ -23,17 +34,15 @@ export class TrackedDocument implements Disposable {
private _disposable: Disposable | undefined; private _disposable: Disposable | undefined;
private _disposed: boolean = false; private _disposed: boolean = false;
private _repo: Promise<Repository | undefined>;
private _repo: Repository | undefined;
private _uri!: GitUri; private _uri!: GitUri;
constructor(
private constructor(
private readonly _document: TextDocument, private readonly _document: TextDocument,
public readonly key: string, public readonly key: string,
public dirty: boolean, public dirty: boolean,
private _eventDelegates: { onDidBlameStateChange(e: DocumentBlameStateChangeEvent<T>): void }, private _eventDelegates: { onDidBlameStateChange(e: DocumentBlameStateChangeEvent<T>): void },
) {
this._repo = this.initialize(_document.uri);
}
) {}
dispose() { dispose() {
this._disposed = true; this._disposed = true;
@ -41,10 +50,13 @@ export class TrackedDocument implements Disposable {
this._disposable?.dispose(); this._disposable?.dispose();
} }
private async initialize(uri: Uri): Promise<Repository | undefined> {
private initializing = true;
private async initialize(): Promise<Repository | undefined> {
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 // 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( Logger.log(
`TrackedDocument.initialize(${uri.toString(true)})`, `TrackedDocument.initialize(${uri.toString(true)})`,
'Timed out waiting for the GitService to start', 'Timed out waiting for the GitService to start',
@ -57,13 +69,16 @@ export class TrackedDocument implements Disposable {
if (this._disposed) return undefined; if (this._disposed) return undefined;
const repo = await Container.git.getRepository(this._uri); const repo = await Container.git.getRepository(this._uri);
this._repo = repo;
if (this._disposed) return undefined; if (this._disposed) return undefined;
if (repo !== undefined) {
if (repo != null) {
this._disposable = repo.onDidChange(this.onRepositoryChanged, this); this._disposable = repo.onDidChange(this.onRepositoryChanged, this);
} }
await this.update({ initializing: true, repo: repo });
await this.update();
this.initializing = false;
return repo; return repo;
} }
@ -107,9 +122,7 @@ export class TrackedDocument implements Disposable {
} }
get isRevision() { 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; private _isTracked: boolean = false;
@ -129,10 +142,6 @@ export class TrackedDocument implements Disposable {
void setContext(ContextKeys.ActiveFileStatus, this.getStatus()); void setContext(ContextKeys.ActiveFileStatus, this.getStatus());
} }
async ensureInitialized() {
await this._repo;
}
is(document: TextDocument) { is(document: TextDocument) {
return document === this._document; return document === this._document;
} }
@ -141,7 +150,7 @@ export class TrackedDocument implements Disposable {
this._blameFailed = false; this._blameFailed = false;
this._isDirtyIdle = 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) // // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again)
// if (!this.state.hasErrors) { // if (!this.state.hasErrors) {
@ -171,8 +180,8 @@ export class TrackedDocument implements Disposable {
this._forceDirtyStateChangeOnNextDocumentChange = true; 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._hasRemotes = false;
this._isTracked = false; this._isTracked = false;
@ -182,30 +191,27 @@ export class TrackedDocument implements Disposable {
this._isDirtyIdle = false; this._isDirtyIdle = false;
const active = getEditorIfActive(this._document); 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); this._isTracked = await Container.git.isTracked(this._uri);
let repo = undefined; let repo = undefined;
if (this._isTracked) { 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(); this._hasRemotes = await repo.hasRemotes();
} else { } else {
this._hasRemotes = false; this._hasRemotes = false;
} }
if (active !== undefined) {
if (active != null) {
const blameable = this.isBlameable; const blameable = this.isBlameable;
void setContext(ContextKeys.ActiveFileStatus, this.getStatus()); void setContext(ContextKeys.ActiveFileStatus, this.getStatus());
if (!options.initializing && wasBlameable !== blameable) {
if (!this.initializing && wasBlameable !== blameable) {
const e: DocumentBlameStateChangeEvent<T> = { editor: active, document: this, blameable: blameable }; const e: DocumentBlameStateChangeEvent<T> = { editor: active, document: this, blameable: blameable };
this._onDidBlameStateChange.fire(e); this._onDidBlameStateChange.fire(e);
this._eventDelegates.onDidBlameStateChange(e); this._eventDelegates.onDidBlameStateChange(e);

Loading…
Cancel
Save