textFileEditorModelManager.ts 12.3 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

J
Johannes Rieken 已提交
7 8
import Event, { Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
9
import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
10 11 12 13 14 15 16 17
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { ModelState, ITextFileEditorModel, LocalFileChangeEvent, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles';
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';
18 19

export class TextFileEditorModelManager implements ITextFileEditorModelManager {
20

21 22 23 24
	// Delay in ms that we wait at minimum before we update a model from a file change event.
	// This reduces the chance that a save from the client triggers an update of the editor.
	private static FILE_CHANGE_UPDATE_DELAY = 2000;

25 26
	private toUnbind: IDisposable[];

27
	private _onModelDisposed: Emitter<URI>;
28
	private _onModelContentChanged: Emitter<TextFileModelChangeEvent>;
29 30 31 32
	private _onModelDirty: Emitter<TextFileModelChangeEvent>;
	private _onModelSaveError: Emitter<TextFileModelChangeEvent>;
	private _onModelSaved: Emitter<TextFileModelChangeEvent>;
	private _onModelReverted: Emitter<TextFileModelChangeEvent>;
33
	private _onModelEncodingChanged: Emitter<TextFileModelChangeEvent>;
34

35
	private mapResourceToDisposeListener: { [resource: string]: IDisposable; };
36
	private mapResourceToStateChangeListener: { [resource: string]: IDisposable; };
37
	private mapResourceToModelContentChangeListener: { [resource: string]: IDisposable; };
38 39
	private mapResourceToModel: { [resource: string]: ITextFileEditorModel; };
	private mapResourceToPendingModelLoaders: { [resource: string]: TPromise<ITextFileEditorModel> };
40

41
	constructor(
42 43
		@ILifecycleService private lifecycleService: ILifecycleService,
		@IEventService private eventService: IEventService,
44
		@IInstantiationService private instantiationService: IInstantiationService,
45 46 47 48
		@IEditorGroupService private editorGroupService: IEditorGroupService
	) {
		this.toUnbind = [];

49
		this._onModelDisposed = new Emitter<URI>();
50
		this._onModelContentChanged = new Emitter<TextFileModelChangeEvent>();
51 52 53 54
		this._onModelDirty = new Emitter<TextFileModelChangeEvent>();
		this._onModelSaveError = new Emitter<TextFileModelChangeEvent>();
		this._onModelSaved = new Emitter<TextFileModelChangeEvent>();
		this._onModelReverted = new Emitter<TextFileModelChangeEvent>();
55
		this._onModelEncodingChanged = new Emitter<TextFileModelChangeEvent>();
56

57
		this.toUnbind.push(this._onModelDisposed);
58
		this.toUnbind.push(this._onModelContentChanged);
59 60 61 62
		this.toUnbind.push(this._onModelDirty);
		this.toUnbind.push(this._onModelSaveError);
		this.toUnbind.push(this._onModelSaved);
		this.toUnbind.push(this._onModelReverted);
63
		this.toUnbind.push(this._onModelEncodingChanged);
64

65
		this.mapResourceToModel = Object.create(null);
66
		this.mapResourceToDisposeListener = Object.create(null);
67
		this.mapResourceToStateChangeListener = Object.create(null);
68
		this.mapResourceToModelContentChangeListener = Object.create(null);
69
		this.mapResourceToPendingModelLoaders = Object.create(null);
70 71 72 73 74

		this.registerListeners();
	}

	private registerListeners(): void {
75

B
Benjamin Pasero 已提交
76
		// Editors changing/closing
77
		this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
B
Benjamin Pasero 已提交
78
		this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(() => this.onEditorClosed()));
79

80 81 82 83
		// File changes
		this.toUnbind.push(this.eventService.addListener2('files.internal:fileChanged', (e: LocalFileChangeEvent) => this.onLocalFileChange(e)));
		this.toUnbind.push(this.eventService.addListener2(CommonFileEventType.FILE_CHANGES, (e: FileChangesEvent) => this.onFileChanges(e)));

84 85 86 87
		// Lifecycle
		this.lifecycleService.onShutdown(this.dispose, this);
	}

88 89 90 91
	private onEditorsChanged(): void {
		this.disposeUnusedModels();
	}

B
Benjamin Pasero 已提交
92 93 94 95
	private onEditorClosed(): void {
		this.disposeUnusedModels();
	}

96 97 98 99 100 101 102
	private disposeModelIfPossible(resource: URI): void {
		const model = this.get(resource);
		if (this.canDispose(model)) {
			model.dispose();
		}
	}

103 104
	private onLocalFileChange(e: LocalFileChangeEvent): void {
		if (e.gotMoved() || e.gotDeleted()) {
105
			this.disposeModelIfPossible(e.getBefore().resource); // dispose models of moved or deleted files
106 107 108 109 110 111 112
		}
	}

	private onFileChanges(e: FileChangesEvent): void {

		// Dispose inputs that got deleted
		e.getDeleted().forEach(deleted => {
113
			this.disposeModelIfPossible(deleted.resource);
114 115 116 117 118 119 120
		});

		// Dispose models that got changed and are not visible. We do this because otherwise
		// cached file models will be stale from the contents on disk.
		e.getUpdated()
			.map(u => this.get(u.resource))
			.filter(model => {
121
				if (!model) {
122 123 124 125 126 127 128 129 130
					return false;
				}

				if (Date.now() - model.getLastSaveAttemptTime() < TextFileEditorModelManager.FILE_CHANGE_UPDATE_DELAY) {
					return false; // this is a weak check to see if the change came from outside the editor or not
				}

				return true; // ok boss
			})
131
			.forEach(model => this.disposeModelIfPossible(model.getResource()));
132 133
	}

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
	private canDispose(textModel: ITextFileEditorModel): boolean {
		if (!textModel) {
			return false; // we need data!
		}

		if (textModel.isDisposed()) {
			return false; // already disposed
		}

		if (textModel.textEditorModel && textModel.textEditorModel.isAttachedToEditor()) {
			return false; // never dispose when attached to editor
		}

		if (textModel.getState() !== ModelState.SAVED) {
			return false; // never dispose unsaved models
		}

151 152 153 154
		if (this.mapResourceToPendingModelLoaders[textModel.getResource().toString()]) {
			return false; // never dispose models that we are about to load at the same time
		}

155 156 157
		return true;
	}

158 159 160 161
	public get onModelDisposed(): Event<URI> {
		return this._onModelDisposed.event;
	}

162 163 164 165
	public get onModelContentChanged(): Event<TextFileModelChangeEvent> {
		return this._onModelContentChanged.event;
	}

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
	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;
	}

182 183 184 185
	public get onModelEncodingChanged(): Event<TextFileModelChangeEvent> {
		return this._onModelEncodingChanged.event;
	}

186
	public get(resource: URI): ITextFileEditorModel {
187
		return this.mapResourceToModel[resource.toString()];
188 189
	}

190
	public loadOrCreate(resource: URI, encoding: string, refresh?: boolean): TPromise<ITextFileEditorModel> {
191 192 193 194 195 196 197

		// Return early if model is currently being loaded
		const pendingLoad = this.mapResourceToPendingModelLoaders[resource.toString()];
		if (pendingLoad) {
			return pendingLoad;
		}

198
		let modelPromise: TPromise<ITextFileEditorModel>;
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

		// 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();
214 215 216 217

			// Install state change listener
			this.mapResourceToStateChangeListener[resource.toString()] = model.onDidStateChange(state => {
				const event = new TextFileModelChangeEvent(model, state);
B
Benjamin Pasero 已提交
218
				switch (state) {
219 220
					case StateChange.DIRTY:
						this._onModelDirty.fire(event);
B
Benjamin Pasero 已提交
221
						break;
222 223
					case StateChange.SAVE_ERROR:
						this._onModelSaveError.fire(event);
B
Benjamin Pasero 已提交
224
						break;
225 226
					case StateChange.SAVED:
						this._onModelSaved.fire(event);
B
Benjamin Pasero 已提交
227
						break;
228 229
					case StateChange.REVERTED:
						this._onModelReverted.fire(event);
B
Benjamin Pasero 已提交
230
						break;
231 232 233
					case StateChange.ENCODING:
						this._onModelEncodingChanged.fire(event);
						break;
234 235
				}
			});
236 237 238 239 240 241

			// Install model content change listener
			this.mapResourceToModelContentChangeListener[resource.toString()] = model.onDidContentChange(() => {
				const newEvent = new TextFileModelChangeEvent(model, StateChange.CONTENT_CHANGE);
				this._onModelContentChanged.fire(newEvent);
			});
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
		}

		// 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);
		});
	}

268
	public getAll(resource?: URI): ITextFileEditorModel[] {
269
		return Object.keys(this.mapResourceToModel)
270
			.filter(r => !resource || resource.toString() === r)
271
			.map(r => this.mapResourceToModel[r]);
272 273
	}

274
	public add(resource: URI, model: ITextFileEditorModel): void {
275
		const knownModel = this.mapResourceToModel[resource.toString()];
276 277 278 279 280 281 282 283 284 285 286
		if (knownModel === model) {
			return; // already cached
		}

		// dispose any previously stored dispose listener for this resource
		const disposeListener = this.mapResourceToDisposeListener[resource.toString()];
		if (disposeListener) {
			disposeListener.dispose();
		}

		// store in cache but remove when model gets disposed
287
		this.mapResourceToModel[resource.toString()] = model;
288 289 290 291
		this.mapResourceToDisposeListener[resource.toString()] = model.onDispose(() => {
			this.remove(resource);
			this._onModelDisposed.fire(resource);
		});
292 293 294
	}

	public remove(resource: URI): void {
295
		delete this.mapResourceToModel[resource.toString()];
296 297 298 299 300 301

		const disposeListener = this.mapResourceToDisposeListener[resource.toString()];
		if (disposeListener) {
			dispose(disposeListener);
			delete this.mapResourceToDisposeListener[resource.toString()];
		}
302 303 304 305 306 307

		const stateChangeListener = this.mapResourceToStateChangeListener[resource.toString()];
		if (stateChangeListener) {
			dispose(stateChangeListener);
			delete this.mapResourceToStateChangeListener[resource.toString()];
		}
308 309 310 311 312 313

		const modelContentCHangeListener = this.mapResourceToModelContentChangeListener[resource.toString()];
		if (modelContentCHangeListener) {
			dispose(modelContentCHangeListener);
			delete this.mapResourceToModelContentChangeListener[resource.toString()];
		}
314 315 316 317 318
	}

	public clear(): void {

		// model cache
319
		this.mapResourceToModel = Object.create(null);
320

321 322
		// dispose dispose listeners
		let keys = Object.keys(this.mapResourceToDisposeListener);
323 324
		dispose(keys.map(k => this.mapResourceToDisposeListener[k]));
		this.mapResourceToDisposeListener = Object.create(null);
325 326 327 328 329

		// dispose state change listeners
		keys = Object.keys(this.mapResourceToStateChangeListener);
		dispose(keys.map(k => this.mapResourceToStateChangeListener[k]));
		this.mapResourceToStateChangeListener = Object.create(null);
330 331 332 333 334

		// dispose model content change listeners
		keys = Object.keys(this.mapResourceToModelContentChangeListener);
		dispose(keys.map(k => this.mapResourceToModelContentChangeListener[k]));
		this.mapResourceToModelContentChangeListener = Object.create(null);
335
	}
336 337 338 339 340 341 342 343 344

	private disposeUnusedModels(): void {

		// To not grow our text file model cache infinitly, we dispose models that
		// are not showing up in any opened editor.

		// Get all cached file models
		this.getAll()

345 346
			// Only models that are not open inside the editor area
			.filter(model => !this.editorGroupService.getStacksModel().isOpen(model.getResource()))
347 348

			// Dispose
349
			.forEach(model => this.disposeModelIfPossible(model.getResource()));
350 351 352 353 354
	}

	public dispose(): void {
		this.toUnbind = dispose(this.toUnbind);
	}
355
}