/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import Event, { Emitter, debounceEvent } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; export class TextFileEditorModelManager implements ITextFileEditorModelManager { private toUnbind: IDisposable[]; private readonly _onModelDisposed: Emitter; private readonly _onModelContentChanged: Emitter; private readonly _onModelDirty: Emitter; private readonly _onModelSaveError: Emitter; private readonly _onModelSaved: Emitter; private readonly _onModelReverted: Emitter; private readonly _onModelEncodingChanged: Emitter; private readonly _onModelOrphanedChanged: Emitter; private _onModelsDirtyEvent: Event; private _onModelsSaveError: Event; private _onModelsSaved: Event; private _onModelsReverted: Event; private mapResourceToDisposeListener: ResourceMap; private mapResourceToStateChangeListener: ResourceMap; private mapResourceToModelContentChangeListener: ResourceMap; private mapResourceToModel: ResourceMap; private mapResourceToPendingModelLoaders: ResourceMap>; constructor( @ILifecycleService private lifecycleService: ILifecycleService, @IInstantiationService private instantiationService: IInstantiationService ) { this.toUnbind = []; this._onModelDisposed = new Emitter(); this._onModelContentChanged = new Emitter(); this._onModelDirty = new Emitter(); this._onModelSaveError = new Emitter(); this._onModelSaved = new Emitter(); this._onModelReverted = new Emitter(); this._onModelEncodingChanged = new Emitter(); this._onModelOrphanedChanged = new Emitter(); this.toUnbind.push(this._onModelDisposed); this.toUnbind.push(this._onModelContentChanged); this.toUnbind.push(this._onModelDirty); this.toUnbind.push(this._onModelSaveError); this.toUnbind.push(this._onModelSaved); this.toUnbind.push(this._onModelReverted); this.toUnbind.push(this._onModelEncodingChanged); this.toUnbind.push(this._onModelOrphanedChanged); this.mapResourceToModel = new ResourceMap(); this.mapResourceToDisposeListener = new ResourceMap(); this.mapResourceToStateChangeListener = new ResourceMap(); this.mapResourceToModelContentChangeListener = new ResourceMap(); this.mapResourceToPendingModelLoaders = new ResourceMap>(); this.registerListeners(); } private registerListeners(): void { // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); } public get onModelDisposed(): Event { return this._onModelDisposed.event; } public get onModelContentChanged(): Event { return this._onModelContentChanged.event; } public get onModelDirty(): Event { return this._onModelDirty.event; } public get onModelSaveError(): Event { return this._onModelSaveError.event; } public get onModelSaved(): Event { return this._onModelSaved.event; } public get onModelReverted(): Event { return this._onModelReverted.event; } public get onModelEncodingChanged(): Event { return this._onModelEncodingChanged.event; } public get onModelOrphanedChanged(): Event { return this._onModelOrphanedChanged.event; } public get onModelsDirty(): Event { if (!this._onModelsDirtyEvent) { this._onModelsDirtyEvent = this.debounce(this.onModelDirty); } return this._onModelsDirtyEvent; } public get onModelsSaveError(): Event { if (!this._onModelsSaveError) { this._onModelsSaveError = this.debounce(this.onModelSaveError); } return this._onModelsSaveError; } public get onModelsSaved(): Event { if (!this._onModelsSaved) { this._onModelsSaved = this.debounce(this.onModelSaved); } return this._onModelsSaved; } public get onModelsReverted(): Event { if (!this._onModelsReverted) { this._onModelsReverted = this.debounce(this.onModelReverted); } return this._onModelsReverted; } private debounce(event: Event): Event { return debounceEvent(event, (prev: TextFileModelChangeEvent[], cur: TextFileModelChangeEvent) => { if (!prev) { prev = [cur]; } else { prev.push(cur); } return prev; }, this.debounceDelay()); } protected debounceDelay(): number { return 250; } public get(resource: URI): ITextFileEditorModel { return this.mapResourceToModel.get(resource); } public loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): TPromise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); if (pendingLoad) { return pendingLoad; } let modelPromise: TPromise; // Model exists let model = this.get(resource); if (model) { if (!options || !options.reload) { modelPromise = TPromise.as(model); } else { modelPromise = model.load(); } } // Model does not exist else { model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : void 0); modelPromise = model.load(); // Install state change listener this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => { const event = new TextFileModelChangeEvent(model, state); switch (state) { case StateChange.DIRTY: this._onModelDirty.fire(event); break; case StateChange.SAVE_ERROR: this._onModelSaveError.fire(event); break; case StateChange.SAVED: this._onModelSaved.fire(event); break; case StateChange.REVERTED: this._onModelReverted.fire(event); break; case StateChange.ENCODING: this._onModelEncodingChanged.fire(event); break; case StateChange.ORPHANED_CHANGE: this._onModelOrphanedChanged.fire(event); break; } })); // Install model content change listener this.mapResourceToModelContentChangeListener.set(resource, model.onDidContentChange(e => { this._onModelContentChanged.fire(new TextFileModelChangeEvent(model, e)); })); } // Store pending loads to avoid race conditions this.mapResourceToPendingModelLoaders.set(resource, modelPromise); return modelPromise.then(model => { // Make known to manager (if not already known) this.add(resource, model); // Model can be dirty if a backup was restored, so we make sure to have this event delivered if (model.isDirty()) { this._onModelDirty.fire(new TextFileModelChangeEvent(model, StateChange.DIRTY)); } // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); return model; }, error => { // Free resources of this invalid model model.dispose(); // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); return TPromise.wrapError(error); }); } public getAll(resource?: URI, filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] { if (resource) { const res = this.mapResourceToModel.get(resource); return res ? [res] : []; } const res: ITextFileEditorModel[] = []; this.mapResourceToModel.forEach(model => { if (!filter || filter(model)) { res.push(model); } }); return res; } public add(resource: URI, model: ITextFileEditorModel): void { const knownModel = this.mapResourceToModel.get(resource); if (knownModel === model) { return; // already cached } // dispose any previously stored dispose listener for this resource const disposeListener = this.mapResourceToDisposeListener.get(resource); if (disposeListener) { disposeListener.dispose(); } // store in cache but remove when model gets disposed this.mapResourceToModel.set(resource, model); this.mapResourceToDisposeListener.set(resource, model.onDispose(() => { this.remove(resource); this._onModelDisposed.fire(resource); })); } public remove(resource: URI): void { this.mapResourceToModel.delete(resource); const disposeListener = this.mapResourceToDisposeListener.get(resource); if (disposeListener) { dispose(disposeListener); this.mapResourceToDisposeListener.delete(resource); } const stateChangeListener = this.mapResourceToStateChangeListener.get(resource); if (stateChangeListener) { dispose(stateChangeListener); this.mapResourceToStateChangeListener.delete(resource); } const modelContentChangeListener = this.mapResourceToModelContentChangeListener.get(resource); if (modelContentChangeListener) { dispose(modelContentChangeListener); this.mapResourceToModelContentChangeListener.delete(resource); } } public clear(): void { // model caches this.mapResourceToModel.clear(); this.mapResourceToPendingModelLoaders.clear(); // dispose dispose listeners this.mapResourceToDisposeListener.forEach(l => l.dispose()); this.mapResourceToDisposeListener.clear(); // dispose state change listeners this.mapResourceToStateChangeListener.forEach(l => l.dispose()); this.mapResourceToStateChangeListener.clear(); // dispose model content change listeners this.mapResourceToModelContentChangeListener.forEach(l => l.dispose()); this.mapResourceToModelContentChangeListener.clear(); } public disposeModel(model: TextFileEditorModel): void { if (!model) { return; // we need data! } if (model.isDisposed()) { return; // already disposed } if (this.mapResourceToPendingModelLoaders.has(model.getResource())) { return; // not yet loaded } if (model.isDirty()) { return; // not saved } model.dispose(); } public dispose(): void { this.toUnbind = dispose(this.toUnbind); } }