textFileEditorModel.ts 34.4 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
import Event, { Emitter } from 'vs/base/common/event';
9
import { TPromise, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
10 11 12
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 } from 'vs/editor/common/editorCommon';
J
Johannes Rieken 已提交
20 21
import { IMode } from 'vs/editor/common/modes';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
22
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
J
Johannes Rieken 已提交
23 24
import { EncodingMode, EditorModel } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
25
import { IBackupFileService, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
26
import { IFileService, IFileStat, IFileOperationResult, FileOperationResult, IContent, CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
27 28 29
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';
30
import { IModelService } from 'vs/editor/common/services/modelService';
31 32
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { anonymize } from 'vs/platform/telemetry/common/telemetryUtils';
33
import { RunOnceScheduler } from 'vs/base/common/async';
A
Alex Dima 已提交
34
import { IRawTextSource } from 'vs/editor/common/model/textSource';
E
Erich Gamma 已提交
35 36 37 38

/**
 * 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.
 */
39
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel {
E
Erich Gamma 已提交
40 41 42

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

43
	public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
44

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

	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;
54
	private lastResolvedDiskStat: IFileStat;
55
	private toDispose: IDisposable[];
E
Erich Gamma 已提交
56
	private blockModelContentChange: boolean;
57
	private autoSaveAfterMillies: number;
58
	private autoSaveAfterMilliesEnabled: boolean;
59
	private autoSavePromise: TPromise<void>;
60
	private contentChangeEventScheduler: RunOnceScheduler;
61
	private saveSequentializer: SaveSequentializer;
E
Erich Gamma 已提交
62
	private disposed: 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
	private inConflictMode: boolean;
	private inOrphanMode: boolean;
	private inErrorMode: boolean;

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

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

E
Erich Gamma 已提交
89
		this.resource = resource;
B
Benjamin Pasero 已提交
90
		this.toDispose = [];
91
		this._onDidContentChange = new Emitter<StateChange>();
92
		this._onDidStateChange = new Emitter<StateChange>();
93
		this.toDispose.push(this._onDidContentChange);
94
		this.toDispose.push(this._onDidStateChange);
E
Erich Gamma 已提交
95 96 97
		this.preferredEncoding = preferredEncoding;
		this.dirty = false;
		this.versionId = 0;
B
Benjamin Pasero 已提交
98
		this.lastSaveAttemptTime = 0;
99
		this.saveSequentializer = new SaveSequentializer();
100

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

104
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
105

106
		this.registerListeners();
E
Erich Gamma 已提交
107 108
	}

109
	private registerListeners(): void {
110
		this.toDispose.push(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
111
		this.toDispose.push(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
112 113
		this.toDispose.push(this.onDidStateChange(e => {
			if (e === StateChange.REVERTED) {
114

115
				// Cancel any content change event promises as they are no longer valid.
116
				this.contentChangeEventScheduler.cancel();
117 118

				// Refire state change reverted events as content change events
119 120 121
				this._onDidContentChange.fire(StateChange.REVERTED);
			}
		}));
122 123 124
	}

	private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
125
		if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
126
			this.autoSaveAfterMillies = config.autoSaveDelay;
127
			this.autoSaveAfterMilliesEnabled = true;
E
Erich Gamma 已提交
128
		} else {
129
			this.autoSaveAfterMillies = void 0;
130
			this.autoSaveAfterMilliesEnabled = false;
E
Erich Gamma 已提交
131 132 133
		}
	}

134 135 136 137
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

B
Benjamin Pasero 已提交
138 139 140 141 142 143 144 145 146 147 148
	private updateTextEditorModelMode(modeId?: string): void {
		if (!this.textEditorModel) {
			return;
		}

		const firstLineText = this.getFirstLineText(this.textEditorModel.getValue());
		const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText);

		this.modelService.setMode(this.textEditorModel, mode);
	}

149
	public get onDidContentChange(): Event<StateChange> {
150 151 152
		return this._onDidContentChange.event;
	}

153 154 155 156
	public get onDidStateChange(): Event<StateChange> {
		return this._onDidStateChange.event;
	}

157 158 159 160 161 162 163
	/**
	 * The current version id of the model.
	 */
	public getVersionId(): number {
		return this.versionId;
	}

E
Erich Gamma 已提交
164 165 166 167 168 169 170
	/**
	 * 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 已提交
171 172 173 174 175 176 177
	/**
	 * Set a save participant handler to react on models getting saved.
	 */
	public static setSaveParticipant(handler: ISaveParticipant): void {
		TextFileEditorModel.saveParticipant = handler;
	}

E
Erich Gamma 已提交
178 179
	/**
	 * Discards any local changes and replaces the model with the contents of the version on disk.
180 181
	 *
	 * @param if the parameter soft is true, will not attempt to load the contents from disk.
E
Erich Gamma 已提交
182
	 */
183
	public revert(soft?: boolean): TPromise<void> {
E
Erich Gamma 已提交
184 185 186 187
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

188 189
		// Cancel any running auto-save
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
190 191

		// Unset flags
B
Benjamin Pasero 已提交
192
		const undo = this.setDirty(false);
E
Erich Gamma 已提交
193

194 195 196 197 198 199 200 201
		let loadPromise: TPromise<EditorModel>;
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
			loadPromise = this.load(true /* force */);
		}

		return loadPromise.then(() => {
E
Erich Gamma 已提交
202 203

			// Emit file change event
204
			this._onDidStateChange.fire(StateChange.REVERTED);
205
		}, error => {
E
Erich Gamma 已提交
206 207 208

			// FileNotFound means the file got deleted meanwhile, so emit revert event because thats ok
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
209
				this._onDidStateChange.fire(StateChange.REVERTED);
E
Erich Gamma 已提交
210 211
			}

212
			// Set flags back to previous values, we are still dirty if revert failed
E
Erich Gamma 已提交
213 214 215 216
			else {
				undo();
			}

217
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
		});
	}

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

233 234 235 236 237 238 239 240 241 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 268 269 270
		// Only for new models we support to load from backup
		if (!this.textEditorModel && !this.createTextEditorModelPromise) {
			return this.loadWithBackup(force);
		}

		// Otherwise load from file resource
		return this.loadFromFile(force);
	}

	private loadWithBackup(force: boolean): TPromise<EditorModel> {
		return this.backupFileService.loadBackupResource(this.resource).then(backup => {

			// Make sure meanwhile someone else did not suceed or start loading
			if (this.createTextEditorModelPromise || this.textEditorModel) {
				return this.createTextEditorModelPromise || TPromise.as(this);
			}

			// If we have a backup, continue loading with it
			if (!!backup) {
				const content: IContent = {
					resource: this.resource,
					name: paths.basename(this.resource.fsPath),
					mtime: Date.now(),
					etag: void 0,
					value: '', /* will be filled later from backup */
					encoding: this.fileService.getEncoding(this.resource)
				};

				return this.loadWithContent(content, backup);
			}

			// Otherwise load from file
			return this.loadFromFile(force);
		});
	}

	private loadFromFile(force: boolean): TPromise<EditorModel> {

E
Erich Gamma 已提交
271 272 273
		// Decide on etag
		let etag: string;
		if (force) {
274
			etag = void 0; // bypass cache if force loading is true
275 276
		} else if (this.lastResolvedDiskStat) {
			etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
277 278 279
		}

		// Resolve Content
280
		return this.textFileService
281
			.resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding })
282 283
			.then(content => this.loadWithContent(content), (error: IFileOperationResult) => this.handleLoadError(error));
	}
E
Erich Gamma 已提交
284

285 286
	private handleLoadError(error: IFileOperationResult): TPromise<EditorModel> {
		const result = error.fileOperationResult;
E
Erich Gamma 已提交
287

288 289 290
		// NotModified status is expected and can be handled gracefully
		if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
			this.setDirty(false); // Ensure we are not tracking a stale state
E
Erich Gamma 已提交
291

292 293 294 295 296
			return TPromise.as<EditorModel>(this);
		}

		// Otherwise bubble up the error
		return TPromise.wrapError(error);
297
	}
E
Erich Gamma 已提交
298

299
	private loadWithContent(content: IRawTextContent | IContent, backup?: URI): TPromise<EditorModel> {
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
		diag('load() - resolved content', this.resource, new Date());

		// Telemetry
		this.telemetryService.publicLog('fileGet', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) });

		// Update our resolved disk stat model
		const resolvedStat: IFileStat = {
			resource: this.resource,
			name: content.name,
			mtime: content.mtime,
			etag: content.etag,
			isDirectory: false,
			hasChildren: false,
			children: void 0,
		};
315
		this.updateLastResolvedDiskStat(resolvedStat);
316 317 318 319 320 321 322 323 324 325 326

		// Keep the original encoding to not loose it when saving
		const oldEncoding = this.contentEncoding;
		this.contentEncoding = content.encoding;

		// 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) {
			this._onDidStateChange.fire(StateChange.ENCODING);
		}
E
Erich Gamma 已提交
327

328 329
		// Update Existing Model
		if (this.textEditorModel) {
330
			return this.doUpdateTextModel(content.value);
331
		}
E
Erich Gamma 已提交
332

333 334 335
		// 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());
E
Erich Gamma 已提交
336

337 338
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
339

340
		// Create New Model
341
		return this.doCreateTextModel(content.resource, content.value, backup);
342
	}
E
Erich Gamma 已提交
343

A
Alex Dima 已提交
344
	private doUpdateTextModel(value: string | IRawTextSource): TPromise<EditorModel> {
345
		diag('load() - updated text editor model', this.resource, new Date());
346

347 348 349 350 351 352 353 354 355 356 357 358
		this.setDirty(false); // Ensure we are not tracking a stale state

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

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

A
Alex Dima 已提交
359
	private doCreateTextModel(resource: URI, value: string | IRawTextSource, backup: URI): TPromise<EditorModel> {
360 361
		diag('load() - created text editor model', this.resource, new Date());

362
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
363 364 365 366 367 368 369 370 371 372 373 374 375
			const hasBackupContent = (typeof backupContent === 'string');

			return this.createTextEditorModel(hasBackupContent ? backupContent : value, resource).then(() => {
				this.createTextEditorModelPromise = null;

				// We restored a backup so we have to set the model as being dirty
				// We also want to trigger auto save if it is enabled to simulate the exact same behaviour
				// you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
				if (hasBackupContent) {
					this.makeDirty();
					if (this.autoSaveAfterMilliesEnabled) {
						this.doAutoSave(this.versionId);
					}
376 377
				}

378 379 380 381 382
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

383
				this.toDispose.push(this.textEditorModel.onDidChangeRawContent(e => this.onModelContentChanged(e)));
384 385 386 387 388 389

				return this;
			}, error => {
				this.createTextEditorModelPromise = null;

				return TPromise.wrapError(error);
390
			});
391
		});
392

393 394 395
		return this.createTextEditorModelPromise;
	}

396 397 398 399
	private doLoadBackup(backup: URI): TPromise<string> {
		if (!backup) {
			return TPromise.as(null);
		}
400

401
		return this.textFileService.resolveTextContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => {
402
			return this.backupFileService.parseBackupContent(backup.value);
403
		}, error => null /* ignore errors */);
E
Erich Gamma 已提交
404 405
	}

406
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
407 408 409 410
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

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

		// 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 已提交
415
		diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
416 417 418 419 420 421 422 423 424 425

		// 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.
426 427 428
		// Note: if the model is in orphan mode, we cannot clear the dirty indicator because there
		// is no version on disk after all.
		if (!this.autoSaveAfterMilliesEnabled && !this.inOrphanMode && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
E
Erich Gamma 已提交
429 430 431
			diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());

			// Clear flags
432
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
433 434 435
			this.setDirty(false);

			// Emit event
436
			if (wasDirty) {
437
				this._onDidStateChange.fire(StateChange.REVERTED);
438
			}
E
Erich Gamma 已提交
439 440 441 442 443 444 445

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
446
		this.makeDirty();
E
Erich Gamma 已提交
447 448

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
449
		if (this.autoSaveAfterMilliesEnabled) {
450
			if (!this.inConflictMode && !this.inOrphanMode) {
E
Erich Gamma 已提交
451 452 453 454 455
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
456

457 458
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
459 460
	}

B
Benjamin Pasero 已提交
461
	private makeDirty(): void {
E
Erich Gamma 已提交
462 463

		// Track dirty state and version id
B
Benjamin Pasero 已提交
464
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
465 466 467 468
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
469
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
470 471 472 473
		}
	}

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

		// Cancel any currently running auto saves to make this the one that succeeds
477
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
478

479
		// Create new save promise and keep it
480
		this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
481 482 483

			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
484
				this.doSave(versionId, SaveReason.AUTO).done(null, onUnexpectedError); // 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 已提交
485 486 487
			}
		});

488
		return this.autoSavePromise;
E
Erich Gamma 已提交
489 490
	}

491 492 493 494
	private cancelAutoSavePromise(): void {
		if (this.autoSavePromise) {
			this.autoSavePromise.cancel();
			this.autoSavePromise = void 0;
E
Erich Gamma 已提交
495 496 497 498 499 500
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
B
Benjamin Pasero 已提交
501
	public save(options: IModelSaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
502 503 504 505 506 507 508
		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
509
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
510

511
		return this.doSave(this.versionId, types.isUndefinedOrNull(options.reason) ? SaveReason.EXPLICIT : options.reason, options.overwriteReadonly, options.overwriteEncoding, options.force);
E
Erich Gamma 已提交
512 513
	}

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

		// Lookup any running pending save for this versionId and return it if found
B
Benjamin Pasero 已提交
518 519 520 521
		//
		// Scenario: user invoked the save action multiple times quickly for the same contents
		//           while the save was not yet finished to disk
		//
522
		if (this.saveSequentializer.hasPendingSave(versionId)) {
B
Benjamin Pasero 已提交
523
			diag(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
524

525
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
526 527
		}

528
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
529 530 531 532 533 534
		//
		// 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.
		//
535
		if ((!force && !this.dirty) || versionId !== this.versionId) {
B
Benjamin Pasero 已提交
536
			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 已提交
537 538 539 540

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

541
		// Return if currently saving by storing this save request as the next save that should happen.
542
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
543
		//
544
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
545
		//             kicks in.
546 547
		// Scenario B: save is very slow (e.g. network share) and the user manages to change the buffer and trigger another save
		//             while the first save has not returned yet.
B
Benjamin Pasero 已提交
548
		//
549
		if (this.saveSequentializer.hasPendingSave()) {
B
Benjamin Pasero 已提交
550
			diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
E
Erich Gamma 已提交
551

552 553
			// Register this as the next upcoming save and return
			return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, reason, overwriteReadonly, overwriteEncoding));
E
Erich Gamma 已提交
554 555 556 557
		}

		// 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
558
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
559 560 561
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
562
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
563 564
		// 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
565 566
		// 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 已提交
567
		let saveParticipantPromise = TPromise.as(versionId);
568
		if (TextFileEditorModel.saveParticipant && !this.lifecycleService.willShutdown) {
B
💄  
Benjamin Pasero 已提交
569
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
570
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
571

572
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
573 574
			};

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

578
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
579
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
580 581
		}

582
		// mark the save participant as current pending save operation
583
		return this.saveSequentializer.setPending(versionId, saveParticipantPromise.then(newVersionId => {
E
Erich Gamma 已提交
584

585 586 587 588
			// the model was not dirty and no save participant changed the contents, so we do not have
			// to write the contents to disk, as they are already on disk. we still want to trigger
			// a change on the file though so that external file watchers can be notified
			if (force && !this.dirty && reason === SaveReason.EXPLICIT && versionId === newVersionId) {
589 590 591
				return this.fileService.touchFile(this.resource).then(stat => {

					// Updated resolved stat with updated stat since touching it might have changed mtime
592
					this.updateLastResolvedDiskStat(stat);
593
				}, () => void 0 /* gracefully ignore errors if just touching */);
594 595
			}

596
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
597 598 599 600 601 602 603 604 605
			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
606
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
J
Johannes Rieken 已提交
607
			diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date());
608
			return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.getValue(), {
B
💄  
Benjamin Pasero 已提交
609 610
				overwriteReadonly,
				overwriteEncoding,
611
				mtime: this.lastResolvedDiskStat.mtime,
J
Johannes Rieken 已提交
612
				encoding: this.getEncoding(),
613
				etag: this.lastResolvedDiskStat.etag
614
			}).then(stat => {
J
Johannes Rieken 已提交
615 616 617
				diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());

				// Telemetry
618
				this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.lastResolvedDiskStat.resource.fsPath) });
J
Johannes Rieken 已提交
619 620 621 622 623 624 625 626

				// 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 已提交
627

628
				// Updated resolved stat with updated stat
629
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
630

631 632 633
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

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

639
				// Flag as error state in the model
J
Johannes Rieken 已提交
640
				this.inErrorMode = true;
E
Erich Gamma 已提交
641

642 643 644 645 646
				// Look out for a save conflict
				if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
647 648
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
649

J
Johannes Rieken 已提交
650 651
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
652 653
			}));
		}));
E
Erich Gamma 已提交
654 655 656
	}

	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
657
		const wasDirty = this.dirty;
658 659
		const wasInConflictMode = this.inConflictMode;
		const wasInOrphanMode = this.inOrphanMode;
B
Benjamin Pasero 已提交
660 661
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
662 663 664

		if (!dirty) {
			this.dirty = false;
665 666
			this.inConflictMode = false;
			this.inOrphanMode = false;
E
Erich Gamma 已提交
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
			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;
684 685
			this.inConflictMode = wasInConflictMode;
			this.inOrphanMode = wasInOrphanMode;
E
Erich Gamma 已提交
686 687 688 689 690
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

691
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStat): void {
E
Erich Gamma 已提交
692 693

		// First resolve - just take
694 695
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
696 697 698 699 700
		}

		// 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.
701 702
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
		}
	}

	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 已提交
725
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
726
	 */
B
Benjamin Pasero 已提交
727 728
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
729 730
	}

731 732 733 734 735
	/**
	 * Returns the time in millies when this working copy was last modified by the user or some other program.
	 */
	public getETag(): string {
		return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null;
E
Erich Gamma 已提交
736 737 738 739 740
	}

	/**
	 * Returns the state this text text file editor model is in with regards to changes and saving.
	 */
B
Benjamin Pasero 已提交
741
	public getState(): ModelState {
742
		if (this.inConflictMode) {
B
Benjamin Pasero 已提交
743
			return ModelState.CONFLICT;
E
Erich Gamma 已提交
744 745
		}

746 747 748 749
		if (this.inOrphanMode) {
			return ModelState.ORPHAN;
		}

E
Erich Gamma 已提交
750
		if (this.inErrorMode) {
B
Benjamin Pasero 已提交
751
			return ModelState.ERROR;
E
Erich Gamma 已提交
752 753 754
		}

		if (!this.dirty) {
B
Benjamin Pasero 已提交
755
			return ModelState.SAVED;
E
Erich Gamma 已提交
756 757
		}

758
		if (this.saveSequentializer.hasPendingSave()) {
B
Benjamin Pasero 已提交
759
			return ModelState.PENDING_SAVE;
E
Erich Gamma 已提交
760 761 762
		}

		if (this.dirty) {
B
Benjamin Pasero 已提交
763
			return ModelState.DIRTY;
E
Erich Gamma 已提交
764
		}
765
		return undefined;
E
Erich Gamma 已提交
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
	}

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

787
			if (!this.inConflictMode) {
B
Benjamin Pasero 已提交
788
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
			}
		}

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

807
	public updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
808 809 810 811 812 813 814
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
815
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
816 817 818 819 820 821 822 823
	}

	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) {
824
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
825 826 827 828 829 830
		}

		return true;
	}

	public isResolved(): boolean {
831
		return !types.isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
	}

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

B
Benjamin Pasero 已提交
848 849 850 851 852 853 854
	/**
	 * Stat accessor only used by tests.
	 */
	public getStat(): IFileStat {
		return this.lastResolvedDiskStat;
	}

855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
	/**
	 * Makes this model an orphan, indicating that the file on disk no longer exists. Returns
	 * a function to undo this and go back to the previous state.
	 */
	public setOrphaned(): () => void {

		// Only saved models can turn into orphans
		if (this.getState() !== ModelState.SAVED) {
			return () => { };
		}

		// Mark as dirty
		const undo = this.setDirty(true);

		// Mark as oprhaned
		this.inOrphanMode = true;

		// Emit as Event if we turned dirty
		this._onDidStateChange.fire(StateChange.DIRTY);

		// Return undo function
		const currentVersionId = this.versionId;
		return () => {

			// Leave orphan mode
			this.inOrphanMode = false;

			// Undo is only valid if version is the one we left with
			if (this.versionId === currentVersionId) {

				// Revert
				undo();

				// Events
				this._onDidStateChange.fire(StateChange.SAVED);
			}
		};
	}

E
Erich Gamma 已提交
894 895
	public dispose(): void {
		this.disposed = true;
896 897
		this.inConflictMode = false;
		this.inOrphanMode = false;
E
Erich Gamma 已提交
898 899
		this.inErrorMode = false;

900
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
901 902
		this.createTextEditorModelPromise = null;

903
		this.cancelAutoSavePromise();
904
		this.contentChangeEventScheduler.cancel();
D
Daniel Imms 已提交
905

E
Erich Gamma 已提交
906 907
		super.dispose();
	}
908 909
}

910 911 912 913 914
interface IPendingSave {
	versionId: number;
	promise: TPromise<void>;
}

915 916 917 918 919 920 921 922
interface ISaveOperation {
	promise: TPromise<void>;
	promiseValue: TValueCallback<void>;
	promiseError: ErrorCallback;
	run: () => TPromise<void>;
}

export class SaveSequentializer {
923
	private _pendingSave: IPendingSave;
924
	private _nextSave: ISaveOperation;
925 926 927 928 929 930 931 932 933 934 935 936 937

	public hasPendingSave(versionId?: number): boolean {
		if (!this._pendingSave) {
			return false;
		}

		if (typeof versionId === 'number') {
			return this._pendingSave.versionId === versionId;
		}

		return !!this._pendingSave;
	}

938 939 940 941
	public get pendingSave(): TPromise<void> {
		return this._pendingSave ? this._pendingSave.promise : void 0;
	}

942 943 944 945 946 947 948 949 950 951
	public setPending(versionId: number, promise: TPromise<void>): TPromise<void> {
		this._pendingSave = { versionId, promise };

		promise.done(() => this.donePending(versionId), () => this.donePending(versionId));

		return promise;
	}

	private donePending(versionId: number): void {
		if (this._pendingSave && versionId === this._pendingSave.versionId) {
952 953 954 955 956 957

			// only set pending to done if the promise finished that is associated with that versionId
			this._pendingSave = void 0;

			// schedule the next save now that we are free if we have any
			this.triggerNextSave();
958 959 960
		}
	}

961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
	private triggerNextSave(): void {
		if (this._nextSave) {
			const saveOperation = this._nextSave;
			this._nextSave = void 0;

			// Run next save and complete on the associated promise
			saveOperation.run().done(saveOperation.promiseValue, saveOperation.promiseError);
		}
	}

	public setNext(run: () => TPromise<void>): TPromise<void> {

		// this is our first next save, so we create associated promise with it
		// so that we can return a promise that completes when the save operation
		// has completed.
		if (!this._nextSave) {
			let promiseValue: TValueCallback<void>;
			let promiseError: ErrorCallback;
			const promise = new TPromise<void>((c, e) => {
				promiseValue = c;
				promiseError = e;
			});

			this._nextSave = {
				run,
				promise,
				promiseValue,
				promiseError
			};
		}

		// we have a previous next save, just overwrite it
		else {
			this._nextSave.run = run;
		}

		return this._nextSave.promise;
998 999 1000
	}
}

1001 1002
class DefaultSaveErrorHandler implements ISaveErrorHandler {

J
Johannes Rieken 已提交
1003
	constructor( @IMessageService private messageService: IMessageService) { }
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

	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() + '])');
	});
1016
}