textFileEditorModel.ts 27.9 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  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 nls = require('vs/nls');
J
Johannes Rieken 已提交
8 9 10 11 12
import Event, { Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import { onUnexpectedError } from 'vs/base/common/errors';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
E
Erich Gamma 已提交
13
import URI from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
14
import * as assert from 'vs/base/common/assert';
J
Johannes Rieken 已提交
15
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
E
Erich Gamma 已提交
16 17 18
import paths = require('vs/base/common/paths');
import diagnostics = require('vs/base/common/diagnostics');
import types = require('vs/base/common/types');
19
import { IModelContentChangedEvent, IRawText } from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
20 21 22 23 24
import { IMode } from 'vs/editor/common/modes';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode, EditorModel } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
25
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
J
Johannes Rieken 已提交
26 27 28 29 30 31
import { IFileService, IFileStat, IFileOperationResult, FileOperationResult } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ITelemetryService, anonymize } from 'vs/platform/telemetry/common/telemetry';
32
import { RunOnceScheduler } from 'vs/base/common/async';
E
Erich Gamma 已提交
33 34 35 36

/**
 * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
 */
37
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel {
E
Erich Gamma 已提交
38 39 40

	public static ID = 'workbench.editors.files.textFileEditorModel';

41 42
	public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 1000;

E
Erich Gamma 已提交
43
	private static saveErrorHandler: ISaveErrorHandler;
B
Benjamin Pasero 已提交
44
	private static saveParticipant: ISaveParticipant;
E
Erich Gamma 已提交
45 46 47 48 49 50 51 52

	private resource: URI;
	private contentEncoding: string; 			// encoding as reported from disk
	private preferredEncoding: string;			// encoding as chosen by the user
	private dirty: boolean;
	private versionId: number;
	private bufferSavedVersionId: number;
	private versionOnDiskStat: IFileStat;
53
	private toDispose: IDisposable[];
E
Erich Gamma 已提交
54
	private blockModelContentChange: boolean;
55
	private autoSaveAfterMillies: number;
56
	private autoSaveAfterMilliesEnabled: boolean;
E
Erich Gamma 已提交
57
	private autoSavePromises: TPromise<void>[];
58
	private contentChangeEventScheduler: RunOnceScheduler;
E
Erich Gamma 已提交
59 60 61 62
	private mapPendingSaveToVersionId: { [versionId: string]: TPromise<void> };
	private disposed: boolean;
	private inConflictResolutionMode: boolean;
	private inErrorMode: boolean;
B
Benjamin Pasero 已提交
63
	private lastSaveAttemptTime: number;
E
Erich Gamma 已提交
64
	private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
65
	private _onDidContentChange: Emitter<StateChange>;
66
	private _onDidStateChange: Emitter<StateChange>;
E
Erich Gamma 已提交
67 68 69 70 71 72 73 74

	constructor(
		resource: URI,
		preferredEncoding: string,
		@IMessageService private messageService: IMessageService,
		@IModeService modeService: IModeService,
		@IModelService modelService: IModelService,
		@IFileService private fileService: IFileService,
75
		@ILifecycleService private lifecycleService: ILifecycleService,
E
Erich Gamma 已提交
76
		@IInstantiationService private instantiationService: IInstantiationService,
77
		@ITelemetryService private telemetryService: ITelemetryService,
78
		@ITextFileService private textFileService: ITextFileService,
79
		@IBackupFileService private backupFileService: IBackupFileService
E
Erich Gamma 已提交
80 81 82
	) {
		super(modelService, modeService);

B
Benjamin Pasero 已提交
83
		assert.ok(resource.scheme === 'file', 'TextFileEditorModel can only handle file:// resources.');
84

E
Erich Gamma 已提交
85
		this.resource = resource;
B
Benjamin Pasero 已提交
86
		this.toDispose = [];
87
		this._onDidContentChange = new Emitter<StateChange>();
88
		this._onDidStateChange = new Emitter<StateChange>();
89
		this.toDispose.push(this._onDidContentChange);
90
		this.toDispose.push(this._onDidStateChange);
E
Erich Gamma 已提交
91 92 93 94
		this.preferredEncoding = preferredEncoding;
		this.dirty = false;
		this.autoSavePromises = [];
		this.versionId = 0;
B
Benjamin Pasero 已提交
95
		this.lastSaveAttemptTime = 0;
E
Erich Gamma 已提交
96
		this.mapPendingSaveToVersionId = {};
97

98
		this.contentChangeEventScheduler = new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY);
99
		this.toDispose.push(this.contentChangeEventScheduler);
E
Erich Gamma 已提交
100

101
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
102

103
		this.registerListeners();
E
Erich Gamma 已提交
104 105
	}

106
	private registerListeners(): void {
107
		this.toDispose.push(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
108
		this.toDispose.push(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
109 110 111 112 113 114 115 116
		this.toDispose.push(this.onDidStateChange(e => {
			if (e === StateChange.REVERTED) {
				// Refire reverted events as content change events, cancelling any content change
				// promises that are in flight.
				this.contentChangeEventScheduler.cancel();
				this._onDidContentChange.fire(StateChange.REVERTED);
			}
		}));
117 118 119
	}

	private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
120
		if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
121
			this.autoSaveAfterMillies = config.autoSaveDelay;
122
			this.autoSaveAfterMilliesEnabled = true;
E
Erich Gamma 已提交
123
		} else {
124
			this.autoSaveAfterMillies = void 0;
125
			this.autoSaveAfterMilliesEnabled = false;
E
Erich Gamma 已提交
126 127 128
		}
	}

129 130 131 132
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

133
	public get onDidContentChange(): Event<StateChange> {
134 135 136
		return this._onDidContentChange.event;
	}

137 138 139 140
	public get onDidStateChange(): Event<StateChange> {
		return this._onDidStateChange.event;
	}

E
Erich Gamma 已提交
141 142 143 144 145 146 147
	/**
	 * Set a save error handler to install code that executes when save errors occur.
	 */
	public static setSaveErrorHandler(handler: ISaveErrorHandler): void {
		TextFileEditorModel.saveErrorHandler = handler;
	}

B
Benjamin Pasero 已提交
148 149 150 151 152 153 154
	/**
	 * Set a save participant handler to react on models getting saved.
	 */
	public static setSaveParticipant(handler: ISaveParticipant): void {
		TextFileEditorModel.saveParticipant = handler;
	}

E
Erich Gamma 已提交
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
	/**
	 * When set, will disable any saving (including auto save) until the model is loaded again. This allows to resolve save conflicts
	 * without running into subsequent save errors when editing the model.
	 */
	public setConflictResolutionMode(): void {
		diag('setConflictResolutionMode() - enabled conflict resolution mode', this.resource, new Date());

		this.inConflictResolutionMode = true;
	}

	/**
	 * Answers if this model is currently in conflic resolution mode or not.
	 */
	public isInConflictResolutionMode(): boolean {
		return this.inConflictResolutionMode;
	}

	/**
	 * Discards any local changes and replaces the model with the contents of the version on disk.
	 */
	public revert(): TPromise<void> {
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

		// Cancel any running auto-saves
		this.cancelAutoSavePromises();

		// Unset flags
B
Benjamin Pasero 已提交
184
		const undo = this.setDirty(false);
E
Erich Gamma 已提交
185 186 187 188 189

		// Reload
		return this.load(true /* force */).then(() => {

			// Emit file change event
190
			this._onDidStateChange.fire(StateChange.REVERTED);
E
Erich Gamma 已提交
191 192 193 194
		}, (error) => {

			// FileNotFound means the file got deleted meanwhile, so emit revert event because thats ok
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
195
				this._onDidStateChange.fire(StateChange.REVERTED);
E
Erich Gamma 已提交
196 197 198 199 200 201 202
			}

			// Set flags back to previous values, we are still dirty if revert failed and we where
			else {
				undo();
			}

203
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
		});
	}

	public load(force?: boolean /* bypass any caches and really go to disk */): TPromise<EditorModel> {
		diag('load() - enter', this.resource, new Date());

		// It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk
		// if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk
		// meanwhile, but this is a very low risk.
		if (this.dirty) {
			diag('load() - exit - without loading because model is dirty', this.resource, new Date());

			return TPromise.as(this);
		}

		// Decide on etag
		let etag: string;
		if (force) {
			etag = undefined; // bypass cache if force loading is true
		} else if (this.versionOnDiskStat) {
			etag = this.versionOnDiskStat.etag; // otherwise respect etag to support caching
		}

		// Resolve Content
A
Alex Dima 已提交
228
		return this.textFileService.resolveTextContent(this.resource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((content) => {
E
Erich Gamma 已提交
229 230 231
			diag('load() - resolved content', this.resource, new Date());

			// Telemetry
232
			this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) });
E
Erich Gamma 已提交
233 234

			// Update our resolved disk stat model
B
Benjamin Pasero 已提交
235
			const resolvedStat: IFileStat = {
E
Erich Gamma 已提交
236 237 238 239 240 241 242 243 244 245
				resource: this.resource,
				name: content.name,
				mtime: content.mtime,
				etag: content.etag,
				isDirectory: false,
				hasChildren: false,
				children: void 0,
			};
			this.updateVersionOnDiskStat(resolvedStat);

B
Benjamin Pasero 已提交
246
			// Keep the original encoding to not loose it when saving
B
Benjamin Pasero 已提交
247
			const oldEncoding = this.contentEncoding;
B
Benjamin Pasero 已提交
248
			this.contentEncoding = content.encoding;
E
Erich Gamma 已提交
249 250 251 252 253

			// Handle events if encoding changed
			if (this.preferredEncoding) {
				this.updatePreferredEncoding(this.contentEncoding); // make sure to reflect the real encoding of the file (never out of sync)
			} else if (oldEncoding !== this.contentEncoding) {
254
				this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
			}

			// Update Existing Model
			if (this.textEditorModel) {
				diag('load() - updated text editor model', this.resource, new Date());

				this.setDirty(false); // Ensure we are not tracking a stale state

				this.blockModelContentChange = true;
				try {
					this.updateTextEditorModel(content.value);
				} finally {
					this.blockModelContentChange = false;
				}

				return TPromise.as<EditorModel>(this);
			}

			// Join an existing request to create the editor model to avoid race conditions
			else if (this.createTextEditorModelPromise) {
				diag('load() - join existing text editor model promise', this.resource, new Date());

				return this.createTextEditorModelPromise;
			}

			// Create New Model
			else {
				diag('load() - created text editor model', this.resource, new Date());

284
				return this.backupFileService.hasBackup(this.resource).then(backupExists => {
B
Benjamin Pasero 已提交
285 286 287
					let resolveBackupPromise: TPromise<IRawText>;

					// Try get restore content, if there is an issue fallback silently to the original file's content
288
					if (backupExists) {
289
						const restoreResource = this.backupFileService.getBackupResource(this.resource);
290
						const restoreOptions = { acceptTextOnly: true, encoding: 'utf-8' };
B
Benjamin Pasero 已提交
291 292

						resolveBackupPromise = this.textFileService.resolveTextContent(restoreResource, restoreOptions).then(backup => backup.value, error => content.value);
293
					} else {
B
Benjamin Pasero 已提交
294
						resolveBackupPromise = TPromise.as(content.value);
295 296
					}

B
Benjamin Pasero 已提交
297
					this.createTextEditorModelPromise = resolveBackupPromise.then(fileContent => {
298
						return this.createTextEditorModel(fileContent, content.resource).then(() => {
299 300
							this.createTextEditorModelPromise = null;

301
							this.setDirty(backupExists); // Ensure we are not tracking a stale state
302
							this.toDispose.push(this.textEditorModel.onDidChangeRawContent((e: IModelContentChangedEvent) => this.onModelContentChanged(e)));
E
Erich Gamma 已提交
303

304 305 306
							return this;
						}, (error) => {
							this.createTextEditorModelPromise = null;
E
Erich Gamma 已提交
307

308 309 310
							return TPromise.wrapError(error);
						});
					});
E
Erich Gamma 已提交
311

312 313
					return this.createTextEditorModelPromise;
				});
E
Erich Gamma 已提交
314 315 316 317 318 319 320 321 322 323 324
			}
		}, (error) => {

			// NotModified status code is expected and can be handled gracefully
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
				this.setDirty(false); // Ensure we are not tracking a stale state

				return TPromise.as<EditorModel>(this);
			}

			// Otherwise bubble up the error
325
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
326 327 328
		});
	}

329
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
330 331 332 333
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

	private onModelContentChanged(e: IModelContentChangedEvent): void {
B
Benjamin Pasero 已提交
334
		diag(`onModelContentChanged(${e.changeType}) - enter`, this.resource, new Date());
E
Erich Gamma 已提交
335 336 337

		// In any case increment the version id because it tracks the textual content state of the model at all times
		this.versionId++;
B
Benjamin Pasero 已提交
338
		diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
339 340 341 342 343 344 345 346 347 348

		// Ignore if blocking model changes
		if (this.blockModelContentChange) {
			return;
		}

		// The contents changed as a matter of Undo and the version reached matches the saved one
		// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
		// Note: we currently only do this check when auto-save is turned off because there you see
		// a dirty indicator that you want to get rid of when undoing to the saved version.
349
		if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
E
Erich Gamma 已提交
350 351 352
			diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());

			// Clear flags
353
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
354 355 356
			this.setDirty(false);

			// Emit event
357
			if (wasDirty) {
358
				this._onDidStateChange.fire(StateChange.REVERTED);
359
			}
E
Erich Gamma 已提交
360 361 362 363 364 365 366

			return;
		}

		diag('onModelContentChanged() - model content changed and marked as dirty', this.resource, new Date());

		// Mark as dirty
B
Benjamin Pasero 已提交
367
		this.makeDirty();
E
Erich Gamma 已提交
368 369

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
370
		if (this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
371 372 373 374 375 376
			if (!this.inConflictResolutionMode) {
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
377

378 379
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
380 381
	}

B
Benjamin Pasero 已提交
382
	private makeDirty(): void {
E
Erich Gamma 已提交
383 384

		// Track dirty state and version id
B
Benjamin Pasero 已提交
385
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
386 387 388 389
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
390
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
391 392 393 394
		}
	}

	private doAutoSave(versionId: number): TPromise<void> {
B
Benjamin Pasero 已提交
395
		diag(`doAutoSave() - enter for versionId ${versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
396 397 398 399

		// Cancel any currently running auto saves to make this the one that succeeds
		this.cancelAutoSavePromises();

400
		// Create new save promise and keep it
B
Benjamin Pasero 已提交
401
		const promise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
402 403 404

			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
B
Benjamin Pasero 已提交
405
				this.doSave(versionId, SaveReason.AUTO); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change
E
Erich Gamma 已提交
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
			}
		});

		this.autoSavePromises.push(promise);

		return promise;
	}

	private cancelAutoSavePromises(): void {
		while (this.autoSavePromises.length) {
			this.autoSavePromises.pop().cancel();
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
B
Benjamin Pasero 已提交
423
	public save(options: IModelSaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
424 425 426 427 428 429 430 431 432
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

		diag('save() - enter', this.resource, new Date());

		// Cancel any currently running auto saves to make this the one that succeeds
		this.cancelAutoSavePromises();

B
Benjamin Pasero 已提交
433
		return this.doSave(this.versionId, types.isUndefinedOrNull(options.reason) ? SaveReason.EXPLICIT : options.reason, options.overwriteReadonly, options.overwriteEncoding);
E
Erich Gamma 已提交
434 435
	}

B
Benjamin Pasero 已提交
436
	private doSave(versionId: number, reason: SaveReason, overwriteReadonly?: boolean, overwriteEncoding?: boolean): TPromise<void> {
B
Benjamin Pasero 已提交
437
		diag(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource, new Date());
E
Erich Gamma 已提交
438 439

		// Lookup any running pending save for this versionId and return it if found
B
Benjamin Pasero 已提交
440 441 442 443
		//
		// Scenario: user invoked the save action multiple times quickly for the same contents
		//           while the save was not yet finished to disk
		//
B
Benjamin Pasero 已提交
444
		const pendingSave = this.mapPendingSaveToVersionId[versionId];
E
Erich Gamma 已提交
445
		if (pendingSave) {
B
Benjamin Pasero 已提交
446
			diag(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
447 448 449 450 451

			return pendingSave;
		}

		// Return early if not dirty or version changed meanwhile
B
Benjamin Pasero 已提交
452 453 454 455 456 457
		//
		// Scenario A: user invoked save action even though the model is not dirty
		// Scenario B: auto save was triggered for a certain change by the user but meanwhile the user changed
		//             the contents and the version for which auto save was started is no longer the latest.
		//             Thus we avoid spawning multiple auto saves and only take the latest.
		//
E
Erich Gamma 已提交
458
		if (!this.dirty || versionId !== this.versionId) {
B
Benjamin Pasero 已提交
459
			diag(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource, new Date());
E
Erich Gamma 已提交
460 461 462 463 464 465

			return TPromise.as<void>(null);
		}

		// Return if currently saving by scheduling another auto save. Never ever must 2 saves execute at the same time because
		// this can lead to dirty writes and race conditions
B
Benjamin Pasero 已提交
466 467 468 469 470
		//
		// Scenario: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
		//           kicks in. since we never want to trigger 2 saves at the same time, we push out this auto save for the
		//           configured auto save delay assuming that it can proceed next time it triggers.
		//
E
Erich Gamma 已提交
471
		if (this.isBusySaving()) {
B
Benjamin Pasero 已提交
472
			diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
E
Erich Gamma 已提交
473

474
			// Avoid endless loop here and guard if auto save is disabled
475
			if (this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
476 477 478 479 480 481
				return this.doAutoSave(versionId);
			}
		}

		// Push all edit operations to the undo stack so that the user has a chance to
		// Ctrl+Z back to the saved version. We only do this when auto-save is turned off
482
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
483 484 485
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
486
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
487 488
		// we do not want to trigger another auto save or similar, so we block this
		// In addition we update our version right after in case it changed because of a model change
489 490
		// We DO NOT run any save participant if we are in the shutdown phase and files are being
		// saved as a result of that.
J
Johannes Rieken 已提交
491
		let saveParticipantPromise = TPromise.as(versionId);
492
		if (TextFileEditorModel.saveParticipant && !this.lifecycleService.willShutdown) {
B
💄  
Benjamin Pasero 已提交
493
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
494
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
495

496
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
497 498
			};

J
Johannes Rieken 已提交
499 500
			saveParticipantPromise = TPromise.as(undefined).then(() => {
				this.blockModelContentChange = true;
B
💄  
Benjamin Pasero 已提交
501

502
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
503
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
504 505
		}

J
Johannes Rieken 已提交
506
		this.mapPendingSaveToVersionId[versionId] = saveParticipantPromise.then(newVersionId => {
E
Erich Gamma 已提交
507

B
💄  
Benjamin Pasero 已提交
508
			// remove save participant promise from pending saves and update versionId with
J
Johannes Rieken 已提交
509
			// its new value (if pre-save changes happened)
E
Erich Gamma 已提交
510
			delete this.mapPendingSaveToVersionId[versionId];
J
Johannes Rieken 已提交
511 512 513 514 515 516 517 518 519 520 521
			versionId = newVersionId;

			// Clear error flag since we are trying to save again
			this.inErrorMode = false;

			// Remember when this model was saved last
			this.lastSaveAttemptTime = Date.now();

			// Save to Disk
			diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date());
			this.mapPendingSaveToVersionId[versionId] = this.fileService.updateContent(this.versionOnDiskStat.resource, this.getValue(), {
B
💄  
Benjamin Pasero 已提交
522 523
				overwriteReadonly,
				overwriteEncoding,
J
Johannes Rieken 已提交
524 525 526 527 528 529 530 531 532 533
				mtime: this.versionOnDiskStat.mtime,
				encoding: this.getEncoding(),
				etag: this.versionOnDiskStat.etag
			}).then((stat: IFileStat) => {
				diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());

				// Remove from pending saves
				delete this.mapPendingSaveToVersionId[versionId];

				// Telemetry
534
				this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.versionOnDiskStat.resource.fsPath) });
J
Johannes Rieken 已提交
535 536 537 538 539 540 541 542

				// Update dirty state unless model has changed meanwhile
				if (versionId === this.versionId) {
					diag(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource, new Date());
					this.setDirty(false);
				} else {
					diag(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource, new Date());
				}
E
Erich Gamma 已提交
543

J
Johannes Rieken 已提交
544 545
				// Updated resolved stat with updated stat, and keep old for event
				this.updateVersionOnDiskStat(stat);
E
Erich Gamma 已提交
546

J
Johannes Rieken 已提交
547 548 549 550
				// Emit File Saved Event
				this._onDidStateChange.fire(StateChange.SAVED);
			}, (error) => {
				diag(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource, new Date());
E
Erich Gamma 已提交
551

J
Johannes Rieken 已提交
552 553
				// Remove from pending saves
				delete this.mapPendingSaveToVersionId[versionId];
E
Erich Gamma 已提交
554

J
Johannes Rieken 已提交
555 556
				// Flag as error state
				this.inErrorMode = true;
E
Erich Gamma 已提交
557

J
Johannes Rieken 已提交
558 559
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
560

J
Johannes Rieken 已提交
561 562 563
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
			});
E
Erich Gamma 已提交
564

J
Johannes Rieken 已提交
565
			return this.mapPendingSaveToVersionId[versionId];
E
Erich Gamma 已提交
566 567 568 569 570 571
		});

		return this.mapPendingSaveToVersionId[versionId];
	}

	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
572 573 574 575
		const wasDirty = this.dirty;
		const wasInConflictResolutionMode = this.inConflictResolutionMode;
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636

		if (!dirty) {
			this.dirty = false;
			this.inConflictResolutionMode = false;
			this.inErrorMode = false;

			// we remember the models alternate version id to remember when the version
			// of the model matches with the saved version on disk. we need to keep this
			// in order to find out if the model changed back to a saved version (e.g.
			// when undoing long enough to reach to a version that is saved and then to
			// clear the dirty flag)
			if (this.textEditorModel) {
				this.bufferSavedVersionId = this.textEditorModel.getAlternativeVersionId();
			}
		} else {
			this.dirty = true;
		}

		// Return function to revert this call
		return () => {
			this.dirty = wasDirty;
			this.inConflictResolutionMode = wasInConflictResolutionMode;
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

	private updateVersionOnDiskStat(newVersionOnDiskStat: IFileStat): void {

		// First resolve - just take
		if (!this.versionOnDiskStat) {
			this.versionOnDiskStat = newVersionOnDiskStat;
		}

		// Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced.
		// This is essential a If-Modified-Since check on the client ot prevent race conditions from loading
		// and saving. If a save comes in late after a revert was called, the mtime could be out of sync.
		else if (this.versionOnDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.versionOnDiskStat = newVersionOnDiskStat;
		}
	}

	private onSaveError(error: any): void {

		// Prepare handler
		if (!TextFileEditorModel.saveErrorHandler) {
			TextFileEditorModel.setSaveErrorHandler(this.instantiationService.createInstance(DefaultSaveErrorHandler));
		}

		// Handle
		TextFileEditorModel.saveErrorHandler.onSaveError(error, this);
	}

	/**
	 * Returns true if the content of this model has changes that are not yet saved back to the disk.
	 */
	public isDirty(): boolean {
		return this.dirty;
	}

	/**
B
Benjamin Pasero 已提交
637
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
638
	 */
B
Benjamin Pasero 已提交
639 640
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
641 642 643 644 645 646 647 648 649 650 651 652
	}

	/**
	 * Returns the time in millies when this working copy was last modified by the user or some other program.
	 */
	public getLastModifiedTime(): number {
		return this.versionOnDiskStat ? this.versionOnDiskStat.mtime : -1;
	}

	/**
	 * Returns the state this text text file editor model is in with regards to changes and saving.
	 */
B
Benjamin Pasero 已提交
653
	public getState(): ModelState {
E
Erich Gamma 已提交
654
		if (this.inConflictResolutionMode) {
B
Benjamin Pasero 已提交
655
			return ModelState.CONFLICT;
E
Erich Gamma 已提交
656 657 658
		}

		if (this.inErrorMode) {
B
Benjamin Pasero 已提交
659
			return ModelState.ERROR;
E
Erich Gamma 已提交
660 661 662
		}

		if (!this.dirty) {
B
Benjamin Pasero 已提交
663
			return ModelState.SAVED;
E
Erich Gamma 已提交
664 665 666
		}

		if (this.isBusySaving()) {
B
Benjamin Pasero 已提交
667
			return ModelState.PENDING_SAVE;
E
Erich Gamma 已提交
668 669 670
		}

		if (this.dirty) {
B
Benjamin Pasero 已提交
671
			return ModelState.DIRTY;
E
Erich Gamma 已提交
672 673 674
		}
	}

B
Benjamin Pasero 已提交
675 676 677 678
	private isBusySaving(): boolean {
		return !types.isEmptyObject(this.mapPendingSaveToVersionId);
	}

E
Erich Gamma 已提交
679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
	public getEncoding(): string {
		return this.preferredEncoding || this.contentEncoding;
	}

	public setEncoding(encoding: string, mode: EncodingMode): void {
		if (!this.isNewEncoding(encoding)) {
			return; // return early if the encoding is already the same
		}

		// Encode: Save with encoding
		if (mode === EncodingMode.Encode) {
			this.updatePreferredEncoding(encoding);

			// Save
			if (!this.isDirty()) {
				this.versionId++; // needs to increment because we change the model potentially
				this.makeDirty();
			}

			if (!this.inConflictResolutionMode) {
B
Benjamin Pasero 已提交
699
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
			}
		}

		// Decode: Load with encoding
		else {
			if (this.isDirty()) {
				this.messageService.show(Severity.Info, nls.localize('saveFileFirst', "The file is dirty. Please save it first before reopening it with another encoding."));

				return;
			}

			this.updatePreferredEncoding(encoding);

			// Load
			this.load(true /* force because encoding has changed */).done(null, onUnexpectedError);
		}
	}

718
	public updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
719 720 721 722 723 724 725
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
726
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
727 728 729 730 731 732 733 734
	}

	private isNewEncoding(encoding: string): boolean {
		if (this.preferredEncoding === encoding) {
			return false; // return early if the encoding is already the same
		}

		if (!this.preferredEncoding && this.contentEncoding === encoding) {
735
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
		}

		return true;
	}

	public isResolved(): boolean {
		return !types.isUndefinedOrNull(this.versionOnDiskStat);
	}

	/**
	 * Returns true if the dispose() method of this model has been called.
	 */
	public isDisposed(): boolean {
		return this.disposed;
	}

	/**
	 * Returns the full resource URI of the file this text file editor model is about.
	 */
	public getResource(): URI {
		return this.resource;
	}

	public dispose(): void {
		this.disposed = true;
		this.inConflictResolutionMode = false;
		this.inErrorMode = false;

764
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
765 766 767
		this.createTextEditorModelPromise = null;

		this.cancelAutoSavePromises();
D
Daniel Imms 已提交
768

E
Erich Gamma 已提交
769 770
		super.dispose();
	}
771 772 773 774
}

class DefaultSaveErrorHandler implements ISaveErrorHandler {

J
Johannes Rieken 已提交
775
	constructor( @IMessageService private messageService: IMessageService) { }
776 777 778 779 780 781 782 783 784 785 786 787

	public onSaveError(error: any, model: TextFileEditorModel): void {
		this.messageService.show(Severity.Error, nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(model.getResource().fsPath), toErrorMessage(error, false)));
	}
}

// Diagnostics support
let diag: (...args: any[]) => void;
if (!diag) {
	diag = diagnostics.register('TextFileEditorModelDiagnostics', function (...args: any[]) {
		console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])');
	});
788
}