textFileEditorModel.ts 36.3 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');
J
Johannes Rieken 已提交
19
import { IMode } from 'vs/editor/common/modes';
20
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
21
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
23
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
B
Benjamin Pasero 已提交
24
import { EncodingMode } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
25
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
26
import { IBackupFileService, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
27
import { IFileService, IFileStat, IFileOperationResult, FileOperationResult, IContent, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
28 29 30
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';
31
import { IModelService } from 'vs/editor/common/services/modelService';
32 33
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { anonymize } from 'vs/platform/telemetry/common/telemetryUtils';
34
import { RunOnceScheduler } from 'vs/base/common/async';
A
Alex Dima 已提交
35
import { IRawTextSource } from 'vs/editor/common/model/textSource';
E
Erich Gamma 已提交
36 37 38 39

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

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

44
	public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
45
	public static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
46

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

	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;
56
	private lastResolvedDiskStat: IFileStat;
57
	private toDispose: IDisposable[];
E
Erich Gamma 已提交
58
	private blockModelContentChange: boolean;
59
	private autoSaveAfterMillies: number;
60
	private autoSaveAfterMilliesEnabled: boolean;
61
	private autoSavePromise: TPromise<void>;
62
	private contentChangeEventScheduler: RunOnceScheduler;
63
	private orphanedChangeEventScheduler: RunOnceScheduler;
64
	private saveSequentializer: SaveSequentializer;
E
Erich Gamma 已提交
65
	private disposed: boolean;
B
Benjamin Pasero 已提交
66
	private lastSaveAttemptTime: number;
E
Erich Gamma 已提交
67
	private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
68
	private _onDidContentChange: Emitter<StateChange>;
69
	private _onDidStateChange: Emitter<StateChange>;
E
Erich Gamma 已提交
70

71 72 73 74
	private inConflictMode: boolean;
	private inOrphanMode: boolean;
	private inErrorMode: boolean;

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

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

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

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

109 110 111
		this.orphanedChangeEventScheduler = new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY);
		this.toDispose.push(this.orphanedChangeEventScheduler);

112
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
113

114
		this.registerListeners();
E
Erich Gamma 已提交
115 116
	}

117
	private registerListeners(): void {
118
		this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
119
		this.toDispose.push(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
120
		this.toDispose.push(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
121 122
		this.toDispose.push(this.onDidStateChange(e => {
			if (e === StateChange.REVERTED) {
123

124
				// Cancel any content change event promises as they are no longer valid.
125
				this.contentChangeEventScheduler.cancel();
126 127

				// Refire state change reverted events as content change events
128 129 130
				this._onDidContentChange.fire(StateChange.REVERTED);
			}
		}));
131 132
	}

133 134
	private onFileChanges(e: FileChangesEvent): void {

135
		// Track ADD and DELETES for updates of this model to orphan-mode
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
		const newInOrphanModeGuess = e.contains(this.resource, FileChangeType.DELETED) && !e.contains(this.resource, FileChangeType.ADDED);
		if (this.inOrphanMode !== newInOrphanModeGuess) {
			let checkOrphanedPromise: TPromise<boolean>;
			if (newInOrphanModeGuess) {
				// We have received reports of users seeing delete events even though the file still
				// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
				// Since we do not want to mark the model as orphaned, we have to check if the
				// file is really gone and not just a faulty file event (TODO@Ben revisit when we
				// have a more stable file watcher in place for this scenario).
				checkOrphanedPromise = TPromise.timeout(100).then(() => {
					if (this.disposed) {
						return true;
					}

					return this.fileService.existsFile(this.resource).then(exists => !exists);
				});
			} else {
				checkOrphanedPromise = TPromise.as(false);
			}

			checkOrphanedPromise.done(newInOrphanModeValidated => {
				if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
					this.setOrphaned(newInOrphanModeValidated);
				}
			});
161 162 163 164 165 166 167 168 169 170
		}
	}

	private setOrphaned(orphaned: boolean): void {
		if (this.inOrphanMode !== orphaned) {
			this.inOrphanMode = orphaned;
			this.orphanedChangeEventScheduler.schedule();
		}
	}

171
	private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
172
		if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
173
			this.autoSaveAfterMillies = config.autoSaveDelay;
174
			this.autoSaveAfterMilliesEnabled = true;
E
Erich Gamma 已提交
175
		} else {
176
			this.autoSaveAfterMillies = void 0;
177
			this.autoSaveAfterMilliesEnabled = false;
E
Erich Gamma 已提交
178 179 180
		}
	}

181 182 183 184
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

B
Benjamin Pasero 已提交
185 186 187 188 189 190 191 192 193 194 195
	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);
	}

196
	public get onDidContentChange(): Event<StateChange> {
197 198 199
		return this._onDidContentChange.event;
	}

200 201 202 203
	public get onDidStateChange(): Event<StateChange> {
		return this._onDidStateChange.event;
	}

204 205 206 207 208 209 210
	/**
	 * The current version id of the model.
	 */
	public getVersionId(): number {
		return this.versionId;
	}

E
Erich Gamma 已提交
211 212 213 214 215 216 217
	/**
	 * 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 已提交
218 219 220 221 222 223 224
	/**
	 * Set a save participant handler to react on models getting saved.
	 */
	public static setSaveParticipant(handler: ISaveParticipant): void {
		TextFileEditorModel.saveParticipant = handler;
	}

E
Erich Gamma 已提交
225 226
	/**
	 * Discards any local changes and replaces the model with the contents of the version on disk.
227 228
	 *
	 * @param if the parameter soft is true, will not attempt to load the contents from disk.
E
Erich Gamma 已提交
229
	 */
230
	public revert(soft?: boolean): TPromise<void> {
E
Erich Gamma 已提交
231 232 233 234
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

235 236
		// Cancel any running auto-save
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
237 238

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

B
Benjamin Pasero 已提交
241
		let loadPromise: TPromise<TextFileEditorModel>;
242 243 244 245 246 247 248
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
			loadPromise = this.load(true /* force */);
		}

		return loadPromise.then(() => {
E
Erich Gamma 已提交
249 250

			// Emit file change event
251
			this._onDidStateChange.fire(StateChange.REVERTED);
252
		}, error => {
E
Erich Gamma 已提交
253

254
			// Set flags back to previous values, we are still dirty if revert failed
255
			undo();
E
Erich Gamma 已提交
256

257
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
258 259 260
		});
	}

B
Benjamin Pasero 已提交
261
	public load(force?: boolean /* bypass any caches and really go to disk */): TPromise<TextFileEditorModel> {
E
Erich Gamma 已提交
262 263 264 265 266 267 268 269 270 271 272
		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);
		}

273 274 275 276 277 278 279 280 281
		// 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);
	}

B
Benjamin Pasero 已提交
282
	private loadWithBackup(force: boolean): TPromise<TextFileEditorModel> {
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
		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 */
298
					encoding: this.fileService.getEncoding(this.resource, this.preferredEncoding)
299 300 301 302 303 304 305 306 307 308
				};

				return this.loadWithContent(content, backup);
			}

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

B
Benjamin Pasero 已提交
309
	private loadFromFile(force: boolean): TPromise<TextFileEditorModel> {
310

E
Erich Gamma 已提交
311 312 313
		// Decide on etag
		let etag: string;
		if (force) {
314
			etag = void 0; // bypass cache if force loading is true
315 316
		} else if (this.lastResolvedDiskStat) {
			etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
317 318 319
		}

		// Resolve Content
320
		return this.textFileService
321
			.resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding })
322 323 324
			.then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error));
	}

B
Benjamin Pasero 已提交
325
	private handleLoadSuccess(content: IRawTextContent): TPromise<TextFileEditorModel> {
326 327 328 329 330

		// Clear orphaned state when load was successful
		this.setOrphaned(false);

		return this.loadWithContent(content);
331
	}
E
Erich Gamma 已提交
332

B
Benjamin Pasero 已提交
333
	private handleLoadError(error: IFileOperationResult): TPromise<TextFileEditorModel> {
334
		const result = error.fileOperationResult;
E
Erich Gamma 已提交
335

336 337 338
		// Apply orphaned state based on error code
		this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);

339 340 341
		// 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 已提交
342

B
Benjamin Pasero 已提交
343
			return TPromise.as<TextFileEditorModel>(this);
344 345
		}

346 347 348 349
		// Ignore when a model has been resolved once and the file was deleted meanwhile. Since
		// we already have the model loaded, we can return to this state and update the orphaned
		// flag to indicate that this model has no version on disk anymore.
		if (this.isResolved() && result === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
350
			return TPromise.as<TextFileEditorModel>(this);
351 352
		}

353
		// Otherwise bubble up the error
B
Benjamin Pasero 已提交
354
		return TPromise.wrapError<TextFileEditorModel>(error);
355
	}
E
Erich Gamma 已提交
356

B
Benjamin Pasero 已提交
357
	private loadWithContent(content: IRawTextContent | IContent, backup?: URI): TPromise<TextFileEditorModel> {
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
		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,
		};
373
		this.updateLastResolvedDiskStat(resolvedStat);
374 375 376 377 378 379 380 381 382 383 384

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

386 387
		// Update Existing Model
		if (this.textEditorModel) {
388
			return this.doUpdateTextModel(content.value);
389
		}
E
Erich Gamma 已提交
390

391 392 393
		// 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 已提交
394

395 396
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
397

398
		// Create New Model
399
		return this.doCreateTextModel(content.resource, content.value, backup);
400
	}
E
Erich Gamma 已提交
401

B
Benjamin Pasero 已提交
402
	private doUpdateTextModel(value: string | IRawTextSource): TPromise<TextFileEditorModel> {
403
		diag('load() - updated text editor model', this.resource, new Date());
404

405 406 407 408 409 410 411 412 413
		this.setDirty(false); // Ensure we are not tracking a stale state

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

B
Benjamin Pasero 已提交
414
		return TPromise.as<TextFileEditorModel>(this);
415 416
	}

B
Benjamin Pasero 已提交
417
	private doCreateTextModel(resource: URI, value: string | IRawTextSource, backup: URI): TPromise<TextFileEditorModel> {
418 419
		diag('load() - created text editor model', this.resource, new Date());

420
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
421 422 423 424 425 426 427 428 429 430 431 432 433
			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);
					}
434 435
				}

436 437 438 439 440
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

441 442
				this.toDispose.push(this.textEditorModel.onDidChangeContent((e) => {
					this.onModelContentChanged();
443
				}));
444 445 446 447 448

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

449
				return TPromise.wrapError<TextFileEditorModel>(error);
450
			});
451
		});
452

453 454 455
		return this.createTextEditorModelPromise;
	}

456 457 458 459
	private doLoadBackup(backup: URI): TPromise<string> {
		if (!backup) {
			return TPromise.as(null);
		}
460

461
		return this.textFileService.resolveTextContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => {
462
			return this.backupFileService.parseBackupContent(backup.value);
463
		}, error => null /* ignore errors */);
E
Erich Gamma 已提交
464 465
	}

466
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
467 468 469
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

470 471
	private onModelContentChanged(): void {
		diag(`onModelContentChanged() - enter`, this.resource, new Date());
E
Erich Gamma 已提交
472 473 474

		// 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 已提交
475
		diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
476 477 478 479 480 481 482 483 484 485

		// 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.
486
		if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
E
Erich Gamma 已提交
487 488 489
			diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());

			// Clear flags
490
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
491 492 493
			this.setDirty(false);

			// Emit event
494
			if (wasDirty) {
495
				this._onDidStateChange.fire(StateChange.REVERTED);
496
			}
E
Erich Gamma 已提交
497 498 499 500 501 502 503

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
504
		this.makeDirty();
E
Erich Gamma 已提交
505 506

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
507
		if (this.autoSaveAfterMilliesEnabled) {
508
			if (!this.inConflictMode) {
E
Erich Gamma 已提交
509 510 511 512 513
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
514

515 516
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
517 518
	}

B
Benjamin Pasero 已提交
519
	private makeDirty(): void {
E
Erich Gamma 已提交
520 521

		// Track dirty state and version id
B
Benjamin Pasero 已提交
522
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
523 524 525 526
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
527
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
528 529 530 531
		}
	}

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

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

537
		// Create new save promise and keep it
538
		this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
539 540 541

			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
542
				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 已提交
543 544 545
			}
		});

546
		return this.autoSavePromise;
E
Erich Gamma 已提交
547 548
	}

549 550 551 552
	private cancelAutoSavePromise(): void {
		if (this.autoSavePromise) {
			this.autoSavePromise.cancel();
			this.autoSavePromise = void 0;
E
Erich Gamma 已提交
553 554 555 556 557 558
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
B
Benjamin Pasero 已提交
559
	public save(options: IModelSaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
560 561 562 563 564 565 566
		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
567
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
568

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

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

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

583
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
584 585
		}

586
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
587 588 589 590 591 592
		//
		// 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.
		//
593
		if ((!force && !this.dirty) || versionId !== this.versionId) {
B
Benjamin Pasero 已提交
594
			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 已提交
595 596 597 598

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

599
		// Return if currently saving by storing this save request as the next save that should happen.
600
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
601
		//
602
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
603
		//             kicks in.
604 605
		// 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 已提交
606
		//
607
		if (this.saveSequentializer.hasPendingSave()) {
B
Benjamin Pasero 已提交
608
			diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
E
Erich Gamma 已提交
609

610 611
			// 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 已提交
612 613 614 615
		}

		// 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
616
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
617 618 619
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
620
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
621 622
		// 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
623 624
		// 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 已提交
625
		let saveParticipantPromise = TPromise.as(versionId);
626
		if (TextFileEditorModel.saveParticipant && this.lifecycleService.phase !== LifecyclePhase.ShuttingDown) {
B
💄  
Benjamin Pasero 已提交
627
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
628
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
629

630
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
631 632
			};

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

636
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
637
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
638 639
		}

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

643 644 645 646
			// 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) {
647
				return this.doTouch();
648 649
			}

650
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
651 652 653 654 655 656 657 658 659
			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
660
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
J
Johannes Rieken 已提交
661
			diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date());
662
			return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.getValue(), {
B
💄  
Benjamin Pasero 已提交
663 664
				overwriteReadonly,
				overwriteEncoding,
665
				mtime: this.lastResolvedDiskStat.mtime,
J
Johannes Rieken 已提交
666
				encoding: this.getEncoding(),
667
				etag: this.lastResolvedDiskStat.etag
668
			}).then(stat => {
J
Johannes Rieken 已提交
669 670 671
				diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());

				// Telemetry
672
				if ((this.contextService.hasWorkspace() && paths.isEqualOrParent(this.resource.fsPath, this.contextService.toResource('.vscode').fsPath)) ||
673 674
					this.resource.fsPath === this.environmentService.appSettingsPath) {
					// Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
675 676 677 678
					this.telemetryService.publicLog('settingsWritten');
				} else {
					this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.lastResolvedDiskStat.resource.fsPath) });
				}
J
Johannes Rieken 已提交
679 680 681 682 683 684 685 686

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

688
				// Updated resolved stat with updated stat
689
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
690

691 692 693
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

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

699
				// Flag as error state in the model
J
Johannes Rieken 已提交
700
				this.inErrorMode = true;
E
Erich Gamma 已提交
701

702 703 704 705 706
				// Look out for a save conflict
				if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
707 708
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
709

J
Johannes Rieken 已提交
710 711
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
712 713
			}));
		}));
E
Erich Gamma 已提交
714 715
	}

716 717 718 719 720 721 722 723
	private doTouch(): TPromise<void> {
		return this.fileService.touchFile(this.resource).then(stat => {

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

E
Erich Gamma 已提交
724
	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
725
		const wasDirty = this.dirty;
726
		const wasInConflictMode = this.inConflictMode;
B
Benjamin Pasero 已提交
727 728
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
729 730 731

		if (!dirty) {
			this.dirty = false;
732
			this.inConflictMode = false;
E
Erich Gamma 已提交
733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
			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;
750
			this.inConflictMode = wasInConflictMode;
E
Erich Gamma 已提交
751 752 753 754 755
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

756
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStat): void {
E
Erich Gamma 已提交
757 758

		// First resolve - just take
759 760
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
761 762 763 764 765
		}

		// 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.
766 767
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
		}
	}

	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 已提交
790
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
791
	 */
B
Benjamin Pasero 已提交
792 793
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
794 795
	}

796 797 798 799 800
	/**
	 * 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 已提交
801 802 803
	}

	/**
804
	 * Answers if this model is in a specific state.
E
Erich Gamma 已提交
805
	 */
806 807 808 809 810 811 812 813 814 815 816 817 818 819
	public hasState(state: ModelState): boolean {
		switch (state) {
			case ModelState.CONFLICT:
				return this.inConflictMode;
			case ModelState.DIRTY:
				return this.dirty;
			case ModelState.ERROR:
				return this.inErrorMode;
			case ModelState.ORPHAN:
				return this.inOrphanMode;
			case ModelState.PENDING_SAVE:
				return this.saveSequentializer.hasPendingSave();
			case ModelState.SAVED:
				return !this.dirty;
E
Erich Gamma 已提交
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841
		}
	}

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

842
			if (!this.inConflictMode) {
B
Benjamin Pasero 已提交
843
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
			}
		}

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

862
	public updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
863 864 865 866 867 868 869
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
870
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
871 872 873 874 875 876 877 878
	}

	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) {
879
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
880 881 882 883 884 885
		}

		return true;
	}

	public isResolved(): boolean {
886
		return !types.isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902
	}

	/**
	 * 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 已提交
903 904 905 906 907 908 909
	/**
	 * Stat accessor only used by tests.
	 */
	public getStat(): IFileStat {
		return this.lastResolvedDiskStat;
	}

E
Erich Gamma 已提交
910 911
	public dispose(): void {
		this.disposed = true;
912 913
		this.inConflictMode = false;
		this.inOrphanMode = false;
E
Erich Gamma 已提交
914 915
		this.inErrorMode = false;

916
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
917 918
		this.createTextEditorModelPromise = null;

919
		this.cancelAutoSavePromise();
D
Daniel Imms 已提交
920

E
Erich Gamma 已提交
921 922
		super.dispose();
	}
923 924
}

925 926 927 928 929
interface IPendingSave {
	versionId: number;
	promise: TPromise<void>;
}

930 931 932 933 934 935 936 937
interface ISaveOperation {
	promise: TPromise<void>;
	promiseValue: TValueCallback<void>;
	promiseError: ErrorCallback;
	run: () => TPromise<void>;
}

export class SaveSequentializer {
938
	private _pendingSave: IPendingSave;
939
	private _nextSave: ISaveOperation;
940 941 942 943 944 945 946 947 948 949 950 951 952

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

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

		return !!this._pendingSave;
	}

953 954 955 956
	public get pendingSave(): TPromise<void> {
		return this._pendingSave ? this._pendingSave.promise : void 0;
	}

957 958 959 960 961 962 963 964 965 966
	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) {
967 968 969 970 971 972

			// 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();
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 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
	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;
1013 1014 1015
	}
}

1016 1017
class DefaultSaveErrorHandler implements ISaveErrorHandler {

J
Johannes Rieken 已提交
1018
	constructor( @IMessageService private messageService: IMessageService) { }
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030

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