diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 81de34983fec52ac3c66c814cd62090d07ee37cd..ee197c016ea344dac1ef4b2ec77a2fc1d4be3a05 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -17,7 +17,6 @@ import {IEditorRegistry, Extensions, EditorModel, EncodingMode, ConfirmResult, I import {BinaryEditorModel} from 'vs/workbench/common/editor/binaryEditorModel'; import {IFileOperationResult, FileOperationResult} from 'vs/platform/files/common/files'; import {ITextFileService, BINARY_FILE_EDITOR_ID, FILE_EDITOR_INPUT_ID, FileEditorInput as CommonFileEditorInput, AutoSaveMode, ModelState, EventType as FileEventType, TextFileChangeEvent, IFileEditorDescriptor} from 'vs/workbench/parts/files/common/files'; -import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IDisposable, dispose} from 'vs/base/common/lifecycle'; @@ -31,9 +30,6 @@ export class FileEditorInput extends CommonFileEditorInput { // Do ref counting for all inputs that resolved to a model to be able to dispose when count = 0 private static FILE_EDITOR_MODEL_CLIENTS: { [resource: string]: FileEditorInput[]; } = Object.create(null); - // Keep promises that load a file editor model to avoid loading the same model twice - private static FILE_EDITOR_MODEL_LOADERS: { [resource: string]: TPromise; } = Object.create(null); - private resource: URI; private mime: string; private preferredEncoding: string; @@ -225,7 +221,6 @@ export class FileEditorInput extends CommonFileEditorInput { } public resolve(refresh?: boolean): TPromise { - let modelPromise: TPromise; const resource = this.resource.toString(); // Keep clients who resolved the input to support proper disposal @@ -236,39 +231,14 @@ export class FileEditorInput extends CommonFileEditorInput { FileEditorInput.FILE_EDITOR_MODEL_CLIENTS[resource].push(this); } - // Check for running loader to ensure the model is only ever loaded once - if (FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource]) { - return FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource]; - } - - // Use Cached Model if present - const cachedModel = this.textFileService.models.get(this.resource); - if (cachedModel instanceof TextFileEditorModel && !refresh) { - modelPromise = TPromise.as(cachedModel); - } - - // Refresh Cached Model if present - else if (cachedModel && refresh) { - modelPromise = cachedModel.load(); - FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource] = modelPromise; - } - - // Otherwise Create Model and Load - else { - modelPromise = this.createAndLoadModel(); - FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource] = modelPromise; - } + return this.textFileService.models.loadOrCreate(this.resource, this.preferredEncoding, refresh).then(null, error => { - return modelPromise.then((resolvedModel: TextFileEditorModel | BinaryEditorModel) => { - if (resolvedModel instanceof TextFileEditorModel) { - this.textFileService.models.add(this.resource, resolvedModel); // Store into the text model cache unless this file is binary + // In case of an error that indicates that the file is binary or too large, just return with the binary editor model + if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { + return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load(); } - FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource] = null; // Remove from pending loaders - - return resolvedModel; - }, (error) => { - FileEditorInput.FILE_EDITOR_MODEL_LOADERS[resource] = null; // Remove from pending loaders in case of an error + // Bubble any other error up return TPromise.wrapError(error); }); } @@ -287,28 +257,6 @@ export class FileEditorInput extends CommonFileEditorInput { return -1; } - private createAndLoadModel(): TPromise { - const descriptor = (Registry.as(Extensions.Editors)).getEditor(this); - if (!descriptor) { - throw new Error('Unable to find an editor in the registry for this input.'); - } - - // Optimistically create a text model assuming that the file is not binary - const textModel = this.instantiationService.createInstance(TextFileEditorModel, this.resource, this.preferredEncoding); - return textModel.load().then(() => textModel, (error) => { - - // In case of an error that indicates that the file is binary or too large, just return with the binary editor model - if ((error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { - textModel.dispose(); - - return this.instantiationService.createInstance(BinaryEditorModel, this.resource, this.getName()).load(); - } - - // Bubble any other error up - return TPromise.wrapError(error); - }); - } - public dispose(): void { // Listeners diff --git a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts index 1b386975d43397d57cdc7f8344a13e3b2aa8fdd7..7b1412e908ea96e429afe73a7beed398a1396c90 100644 --- a/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts +++ b/src/vs/workbench/parts/files/common/editors/textFileEditorModelManager.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import {TPromise} from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import {TextFileEditorModel} from 'vs/workbench/parts/files/common/editors/textFileEditorModel'; import {ITextFileEditorModelManager} from 'vs/workbench/parts/files/common/files'; @@ -12,6 +13,7 @@ import {IEditorGroupService} from 'vs/workbench/services/group/common/groupServi import {ModelState, ITextFileEditorModel, LocalFileChangeEvent} from 'vs/workbench/parts/files/common/files'; import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle'; import {IEventService} from 'vs/platform/event/common/event'; +import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {FileChangesEvent, EventType as CommonFileEventType} from 'vs/platform/files/common/files'; export class TextFileEditorModelManager implements ITextFileEditorModelManager { @@ -24,16 +26,19 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { private mapResourceToDisposeListener: { [resource: string]: IDisposable; }; private mapResourcePathToModel: { [resource: string]: TextFileEditorModel; }; + private mapResourceToPendingModelLoaders: { [resource: string]: TPromise}; constructor( @ILifecycleService private lifecycleService: ILifecycleService, @IEventService private eventService: IEventService, + @IInstantiationService private instantiationService: IInstantiationService, @IEditorGroupService private editorGroupService: IEditorGroupService ) { this.toUnbind = []; this.mapResourcePathToModel = Object.create(null); this.mapResourceToDisposeListener = Object.create(null); + this.mapResourceToPendingModelLoaders = Object.create(null); this.registerListeners(); } @@ -51,6 +56,17 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { this.lifecycleService.onShutdown(this.dispose, this); } + private onEditorsChanged(): void { + this.disposeUnusedModels(); + } + + private disposeModelIfPossible(resource: URI): void { + const model = this.get(resource); + if (this.canDispose(model)) { + model.dispose(); + } + } + private onLocalFileChange(e: LocalFileChangeEvent): void { if (e.gotMoved() || e.gotDeleted()) { this.disposeModelIfPossible(e.getBefore().resource); // dispose models of moved or deleted files @@ -82,17 +98,6 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { .forEach(model => this.disposeModelIfPossible(model.getResource())); } - private onEditorsChanged(): void { - this.disposeUnusedModels(); - } - - private disposeModelIfPossible(resource: URI): void { - const model = this.get(resource); - if (this.canDispose(model)) { - model.dispose(); - } - } - private canDispose(textModel: ITextFileEditorModel): boolean { if (!textModel) { return false; // we need data! @@ -117,6 +122,56 @@ export class TextFileEditorModelManager implements ITextFileEditorModelManager { return this.mapResourcePathToModel[resource.toString()]; } + public loadOrCreate(resource: URI, encoding: string, refresh?: boolean): TPromise { + + // Return early if model is currently being loaded + const pendingLoad = this.mapResourceToPendingModelLoaders[resource.toString()]; + if (pendingLoad) { + return pendingLoad; + } + + let modelPromise: TPromise; + + // Model exists + let model = this.get(resource); + if (model) { + if (!refresh) { + modelPromise = TPromise.as(model); + } else { + modelPromise = model.load(); + } + } + + // Model does not exist + else { + model = this.instantiationService.createInstance(TextFileEditorModel, resource, encoding); + modelPromise = model.load(); + } + + // Store pending loads to avoid race conditions + this.mapResourceToPendingModelLoaders[resource.toString()] = modelPromise; + + return modelPromise.then(model => { + + // Make known to manager (if not already known) + this.add(resource, model); + + // Remove from pending loads + this.mapResourceToPendingModelLoaders[resource.toString()] = null; + + return model; + }, error => { + + // Free resources of this invalid model + model.dispose(); + + // Remove from pending loads + this.mapResourceToPendingModelLoaders[resource.toString()] = null; + + return TPromise.wrapError(error); + }); + } + public getAll(resource?: URI): TextFileEditorModel[] { return Object.keys(this.mapResourcePathToModel) .filter(r => !resource || resource.toString() === r) diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 0eaefd453941305ef53f1a4251157c71d2e30523..6260d1d62b5e42c2e43ce0d450dbffb82d892134 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -281,7 +281,7 @@ export interface ITextFileEditorModelManager { getAll(resource?: URI): ITextFileEditorModel[]; - add(resource: URI, model: ITextFileEditorModel): void; + loadOrCreate(resource: URI, preferredEncoding: string, refresh?: boolean): TPromise; } export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport { diff --git a/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts index 4a4b0cbd89762d3bd9a4e34da04c46182eae4e24..3a81b80795d01f914c7bd9a84ad7f4f696125a56 100644 --- a/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/parts/files/test/browser/textFileEditorModelManager.test.ts @@ -54,7 +54,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('add, remove, clear, get, getAll', function () { - const manager = instantiationService.createInstance(TextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const model1 = new EditorModel(); const model2 = new EditorModel(); @@ -94,8 +94,33 @@ suite('Files - TextFileEditorModelManager', () => { assert.strictEqual(0, result.length); }); + test('loadOrCreate', function (done) { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); + const resource = URI.file('/test.html'); + const encoding = 'utf8'; + + manager.loadOrCreate(resource, encoding, true).then(model => { + assert.ok(model); + assert.equal(model.getEncoding(), encoding); + assert.equal(manager.get(resource), model); + + return manager.loadOrCreate(resource, encoding).then(model2 => { + assert.equal(model2, model); + + model.dispose(); + + return manager.loadOrCreate(resource, encoding).then(model3 => { + assert.notEqual(model3, model2); + assert.equal(manager.get(resource), model3); + + done(); + }); + }); + }); + }); + test('removed from cache when model disposed', function () { - const manager = instantiationService.createInstance(TextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const model1 = new EditorModel(); const model2 = new EditorModel();