Files
vscode/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
T

348 lines
11 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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<URI>;
private readonly _onModelContentChanged: Emitter<TextFileModelChangeEvent>;
private readonly _onModelDirty: Emitter<TextFileModelChangeEvent>;
private readonly _onModelSaveError: Emitter<TextFileModelChangeEvent>;
private readonly _onModelSaved: Emitter<TextFileModelChangeEvent>;
private readonly _onModelReverted: Emitter<TextFileModelChangeEvent>;
private readonly _onModelEncodingChanged: Emitter<TextFileModelChangeEvent>;
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent>;
private _onModelsDirtyEvent: Event<TextFileModelChangeEvent[]>;
private _onModelsSaveError: Event<TextFileModelChangeEvent[]>;
private _onModelsSaved: Event<TextFileModelChangeEvent[]>;
private _onModelsReverted: Event<TextFileModelChangeEvent[]>;
private mapResourceToDisposeListener: ResourceMap<IDisposable>;
private mapResourceToStateChangeListener: ResourceMap<IDisposable>;
private mapResourceToModelContentChangeListener: ResourceMap<IDisposable>;
private mapResourceToModel: ResourceMap<ITextFileEditorModel>;
private mapResourceToPendingModelLoaders: ResourceMap<TPromise<ITextFileEditorModel>>;
constructor(
@ILifecycleService private lifecycleService: ILifecycleService,
@IInstantiationService private instantiationService: IInstantiationService
) {
this.toUnbind = [];
this._onModelDisposed = new Emitter<URI>();
this._onModelContentChanged = new Emitter<TextFileModelChangeEvent>();
this._onModelDirty = new Emitter<TextFileModelChangeEvent>();
this._onModelSaveError = new Emitter<TextFileModelChangeEvent>();
this._onModelSaved = new Emitter<TextFileModelChangeEvent>();
this._onModelReverted = new Emitter<TextFileModelChangeEvent>();
this._onModelEncodingChanged = new Emitter<TextFileModelChangeEvent>();
this._onModelOrphanedChanged = new Emitter<TextFileModelChangeEvent>();
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<ITextFileEditorModel>();
this.mapResourceToDisposeListener = new ResourceMap<IDisposable>();
this.mapResourceToStateChangeListener = new ResourceMap<IDisposable>();
this.mapResourceToModelContentChangeListener = new ResourceMap<IDisposable>();
this.mapResourceToPendingModelLoaders = new ResourceMap<TPromise<ITextFileEditorModel>>();
this.registerListeners();
}
private registerListeners(): void {
// Lifecycle
this.lifecycleService.onShutdown(this.dispose, this);
}
public get onModelDisposed(): Event<URI> {
return this._onModelDisposed.event;
}
public get onModelContentChanged(): Event<TextFileModelChangeEvent> {
return this._onModelContentChanged.event;
}
public get onModelDirty(): Event<TextFileModelChangeEvent> {
return this._onModelDirty.event;
}
public get onModelSaveError(): Event<TextFileModelChangeEvent> {
return this._onModelSaveError.event;
}
public get onModelSaved(): Event<TextFileModelChangeEvent> {
return this._onModelSaved.event;
}
public get onModelReverted(): Event<TextFileModelChangeEvent> {
return this._onModelReverted.event;
}
public get onModelEncodingChanged(): Event<TextFileModelChangeEvent> {
return this._onModelEncodingChanged.event;
}
public get onModelOrphanedChanged(): Event<TextFileModelChangeEvent> {
return this._onModelOrphanedChanged.event;
}
public get onModelsDirty(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsDirtyEvent) {
this._onModelsDirtyEvent = this.debounce(this.onModelDirty);
}
return this._onModelsDirtyEvent;
}
public get onModelsSaveError(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsSaveError) {
this._onModelsSaveError = this.debounce(this.onModelSaveError);
}
return this._onModelsSaveError;
}
public get onModelsSaved(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsSaved) {
this._onModelsSaved = this.debounce(this.onModelSaved);
}
return this._onModelsSaved;
}
public get onModelsReverted(): Event<TextFileModelChangeEvent[]> {
if (!this._onModelsReverted) {
this._onModelsReverted = this.debounce(this.onModelReverted);
}
return this._onModelsReverted;
}
private debounce(event: Event<TextFileModelChangeEvent>): Event<TextFileModelChangeEvent[]> {
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<ITextFileEditorModel> {
// Return early if model is currently being loaded
const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource);
if (pendingLoad) {
return pendingLoad;
}
let modelPromise: TPromise<ITextFileEditorModel>;
// 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<ITextFileEditorModel>(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);
}
}