diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 45e8a6cdd18f48b015b794256f9b0202d804c0a5..05e5e7c8089425ef9c7e51fc2c17f0091092772b 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { IDisposable, IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { IReference, dispose, Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -19,6 +19,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { toLocalResource, extUri, IExtUri } from 'vs/base/common/resources'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { Emitter } from 'vs/base/common/event'; export class BoundModelReferenceCollection { @@ -73,7 +74,36 @@ export class BoundModelReferenceCollection { } } -export class MainThreadDocuments implements MainThreadDocumentsShape { +class ModelTracker extends Disposable { + + private _knownVersionId: number; + + constructor( + private readonly _model: ITextModel, + private readonly _onIsCaughtUpWithContentChanges: Emitter, + private readonly _proxy: ExtHostDocumentsShape, + private readonly _textFileService: ITextFileService, + ) { + super(); + this._knownVersionId = this._model.getVersionId(); + this._register(this._model.onDidChangeContent((e) => { + this._knownVersionId = e.versionId; + this._proxy.$acceptModelChanged(this._model.uri, e, this._textFileService.isDirty(this._model.uri)); + if (this.isCaughtUpWithContentChanges()) { + this._onIsCaughtUpWithContentChanges.fire(this._model.uri); + } + })); + } + + public isCaughtUpWithContentChanges(): boolean { + return (this._model.getVersionId() === this._knownVersionId); + } +} + +export class MainThreadDocuments extends Disposable implements MainThreadDocumentsShape { + + private _onIsCaughtUpWithContentChanges = this._register(new Emitter()); + public readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event; private readonly _modelService: IModelService; private readonly _textModelResolverService: ITextModelService; @@ -82,8 +112,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { private readonly _environmentService: IWorkbenchEnvironmentService; private readonly _uriIdentityService: IUriIdentityService; - private readonly _toDispose = new DisposableStore(); - private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; + private _modelTrackers: { [modelUrl: string]: ModelTracker; }; private readonly _proxy: ExtHostDocumentsShape; private readonly _modelIsSynced = new Set(); private readonly _modelReferenceCollection: BoundModelReferenceCollection; @@ -99,6 +128,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { @IUriIdentityService uriIdentityService: IUriIdentityService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService ) { + super(); this._modelService = modelService; this._textModelResolverService = textModelResolverService; this._textFileService = textFileService; @@ -106,26 +136,26 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { this._environmentService = environmentService; this._uriIdentityService = uriIdentityService; - this._modelReferenceCollection = this._toDispose.add(new BoundModelReferenceCollection(uriIdentityService.extUri)); + this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(uriIdentityService.extUri)); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); - this._toDispose.add(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); - this._toDispose.add(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); - this._toDispose.add(modelService.onModelModeChanged(this._onModelModeChanged, this)); + this._register(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); + this._register(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); + this._register(modelService.onModelModeChanged(this._onModelModeChanged, this)); - this._toDispose.add(textFileService.files.onDidSave(e => { + this._register(textFileService.files.onDidSave(e => { if (this._shouldHandleFileEvent(e.model.resource)) { this._proxy.$acceptModelSaved(e.model.resource); } })); - this._toDispose.add(textFileService.files.onDidChangeDirty(m => { + this._register(textFileService.files.onDidChangeDirty(m => { if (this._shouldHandleFileEvent(m.resource)) { this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty()); } })); - this._toDispose.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + this._register(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { if (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE) { for (const { source } of e.files) { if (source) { @@ -135,15 +165,23 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } })); - this._modelToDisposeMap = Object.create(null); + this._modelTrackers = Object.create(null); } public dispose(): void { - Object.keys(this._modelToDisposeMap).forEach((modelUrl) => { - this._modelToDisposeMap[modelUrl].dispose(); + Object.keys(this._modelTrackers).forEach((modelUrl) => { + this._modelTrackers[modelUrl].dispose(); }); - this._modelToDisposeMap = Object.create(null); - this._toDispose.dispose(); + this._modelTrackers = Object.create(null); + super.dispose(); + } + + public isCaughtUpWithContentChanges(resource: URI): boolean { + const modelUrl = resource.toString(); + if (this._modelTrackers[modelUrl]) { + return this._modelTrackers[modelUrl].isCaughtUpWithContentChanges(); + } + return true; } private _shouldHandleFileEvent(resource: URI): boolean { @@ -159,9 +197,7 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } const modelUrl = model.uri; this._modelIsSynced.add(modelUrl.toString()); - this._modelToDisposeMap[modelUrl.toString()] = model.onDidChangeContent((e) => { - this._proxy.$acceptModelChanged(modelUrl, e, this._textFileService.isDirty(modelUrl)); - }); + this._modelTrackers[modelUrl.toString()] = new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService); } private _onModelModeChanged(event: { model: ITextModel; oldModeId: string; }): void { @@ -179,8 +215,8 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { return; } this._modelIsSynced.delete(strModelUrl); - this._modelToDisposeMap[strModelUrl].dispose(); - delete this._modelToDisposeMap[strModelUrl]; + this._modelTrackers[strModelUrl].dispose(); + delete this._modelTrackers[strModelUrl]; } // --- from extension host process diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 35d7c6be84953cbe36d26a036cd30774becbe277..5f69d9ee9bcd898a2f0c6fd3266340ecd5a1bd62 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -306,6 +306,7 @@ export class MainThreadDocumentsAndEditors { private readonly _toDispose = new DisposableStore(); private readonly _proxy: ExtHostDocumentsAndEditorsShape; + private readonly _mainThreadDocuments: MainThreadDocuments; private readonly _textEditors = new Map(); private readonly _onTextEditorAdd = new Emitter(); @@ -336,8 +337,8 @@ export class MainThreadDocumentsAndEditors { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); - const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService)); - extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); + this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService)); + extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments); const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService)); extHostContext.set(MainContext.MainThreadTextEditors, mainThreadTextEditors); @@ -367,7 +368,7 @@ export class MainThreadDocumentsAndEditors { // added editors for (const apiEditor of delta.addedEditors) { const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.editor.getModel(), - apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._modelService, this._clipboardService); + apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._mainThreadDocuments, this._modelService, this._clipboardService); this._textEditors.set(apiEditor.id, mainThreadEditor); addedEditors.push(mainThreadEditor); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index ba8c05003e2940fb1b31eac891044297a4b2d2d1..212aecd829eac44965869394f1641d18501a2619 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -20,6 +20,7 @@ import { equals } from 'vs/base/common/arrays'; import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; +import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocuments'; export interface IFocusTracker { onGainedFocus(): void; @@ -160,7 +161,8 @@ export class MainThreadTextEditorProperties { export class MainThreadTextEditor { private readonly _id: string; - private _model: ITextModel; + private readonly _model: ITextModel; + private readonly _mainThreadDocuments: MainThreadDocuments; private readonly _modelService: IModelService; private readonly _clipboardService: IClipboardService; private readonly _modelListeners = new DisposableStore(); @@ -176,6 +178,7 @@ export class MainThreadTextEditor { model: ITextModel, codeEditor: ICodeEditor, focusTracker: IFocusTracker, + mainThreadDocuments: MainThreadDocuments, modelService: IModelService, clipboardService: IClipboardService, ) { @@ -184,6 +187,7 @@ export class MainThreadTextEditor { this._codeEditor = null; this._properties = null; this._focusTracker = focusTracker; + this._mainThreadDocuments = mainThreadDocuments; this._modelService = modelService; this._clipboardService = clipboardService; @@ -198,7 +202,6 @@ export class MainThreadTextEditor { } public dispose(): void { - this._model = null!; this._modelListeners.dispose(); this._codeEditor = null; this._codeEditorListeners.dispose(); @@ -257,21 +260,46 @@ export class MainThreadTextEditor { this._focusTracker.onLostFocus(); })); + let nextSelectionChangeSource: string | null = null; + this._codeEditorListeners.add(this._mainThreadDocuments.onIsCaughtUpWithContentChanges((uri) => { + if (uri.toString() === this._model.uri.toString()) { + const selectionChangeSource = nextSelectionChangeSource; + nextSelectionChangeSource = null; + this._updatePropertiesNow(selectionChangeSource); + } + })); + + const updateProperties = (selectionChangeSource: string | null) => { + // Some editor events get delivered faster than model content changes. This is + // problematic, as this leads to editor properties reaching the extension host + // too soon, before the model content change that was the root cause. + // + // If this case is identified, then let's update editor properties on the next model + // content change instead. + if (this._mainThreadDocuments.isCaughtUpWithContentChanges(this._model.uri)) { + nextSelectionChangeSource = null; + this._updatePropertiesNow(selectionChangeSource); + } else { + // update editor properties on the next model content change + nextSelectionChangeSource = selectionChangeSource; + } + }; + this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => { // selection - this._updatePropertiesNow(e.source); + updateProperties(e.source); })); this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration(() => { // options - this._updatePropertiesNow(null); + updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => { // visibleRanges - this._updatePropertiesNow(null); + updateProperties(null); })); this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => { // visibleRanges - this._updatePropertiesNow(null); + updateProperties(null); })); this._updatePropertiesNow(null); }