textFileEditorModel.ts 33.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, ITextSource2 } 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';
E
Erich Gamma 已提交
34 35 36 37

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

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

42
	public static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
43

E
Erich Gamma 已提交
44
	private static saveErrorHandler: ISaveErrorHandler;
B
Benjamin Pasero 已提交
45
	private static saveParticipant: ISaveParticipant;
E
Erich Gamma 已提交
46 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;
	private versionOnDiskStat: IFileStat;
54
	private toDispose: IDisposable[];
E
Erich Gamma 已提交
55
	private blockModelContentChange: boolean;
56
	private autoSaveAfterMillies: number;
57
	private autoSaveAfterMilliesEnabled: boolean;
58
	private autoSavePromise: TPromise<void>;
59
	private contentChangeEventScheduler: RunOnceScheduler;
60
	private saveSequentializer: SaveSequentializer;
E
Erich Gamma 已提交
61 62 63
	private disposed: boolean;
	private inConflictResolutionMode: boolean;
	private inErrorMode: boolean;
B
Benjamin Pasero 已提交
64
	private lastSaveAttemptTime: number;
E
Erich Gamma 已提交
65
	private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
66
	private _onDidContentChange: Emitter<StateChange>;
67
	private _onDidStateChange: Emitter<StateChange>;
E
Erich Gamma 已提交
68 69 70 71 72 73 74 75

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

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

E
Erich Gamma 已提交
86
		this.resource = resource;
B
Benjamin Pasero 已提交
87
		this.toDispose = [];
88
		this._onDidContentChange = new Emitter<StateChange>();
89
		this._onDidStateChange = new Emitter<StateChange>();
90
		this.toDispose.push(this._onDidContentChange);
91
		this.toDispose.push(this._onDidStateChange);
E
Erich Gamma 已提交
92 93 94
		this.preferredEncoding = preferredEncoding;
		this.dirty = false;
		this.versionId = 0;
B
Benjamin Pasero 已提交
95
		this.lastSaveAttemptTime = 0;
96
		this.saveSequentializer = new SaveSequentializer();
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
		this.toDispose.push(this.onDidStateChange(e => {
			if (e === StateChange.REVERTED) {
111

112
				// Cancel any content change event promises as they are no longer valid.
113
				this.contentChangeEventScheduler.cancel();
114 115

				// Refire state change reverted events as content change events
116 117 118
				this._onDidContentChange.fire(StateChange.REVERTED);
			}
		}));
119 120 121
	}

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

131 132 133 134
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

B
Benjamin Pasero 已提交
135 136 137 138 139 140 141 142 143 144 145
	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);
	}

146
	public get onDidContentChange(): Event<StateChange> {
147 148 149
		return this._onDidContentChange.event;
	}

150 151 152 153
	public get onDidStateChange(): Event<StateChange> {
		return this._onDidStateChange.event;
	}

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

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

E
Erich Gamma 已提交
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
	/**
	 * 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.
194 195
	 *
	 * @param if the parameter soft is true, will not attempt to load the contents from disk.
E
Erich Gamma 已提交
196
	 */
197
	public revert(soft?: boolean): TPromise<void> {
E
Erich Gamma 已提交
198 199 200 201
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

202 203
		// Cancel any running auto-save
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
204 205

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

208 209 210 211 212 213 214 215
		let loadPromise: TPromise<EditorModel>;
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
			loadPromise = this.load(true /* force */);
		}

		return loadPromise.then(() => {
E
Erich Gamma 已提交
216 217

			// Emit file change event
218
			this._onDidStateChange.fire(StateChange.REVERTED);
219
		}, error => {
E
Erich Gamma 已提交
220 221 222

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

226
			// Set flags back to previous values, we are still dirty if revert failed
E
Erich Gamma 已提交
227 228 229 230
			else {
				undo();
			}

231
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
		});
	}

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

247 248 249 250 251 252 253 254 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 284
		// 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 已提交
285 286 287
		// Decide on etag
		let etag: string;
		if (force) {
288
			etag = void 0; // bypass cache if force loading is true
E
Erich Gamma 已提交
289 290 291 292 293
		} else if (this.versionOnDiskStat) {
			etag = this.versionOnDiskStat.etag; // otherwise respect etag to support caching
		}

		// Resolve Content
294
		return this.textFileService
295
			.resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding })
296 297
			.then(content => this.loadWithContent(content), (error: IFileOperationResult) => this.handleLoadError(error));
	}
E
Erich Gamma 已提交
298

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

302 303 304
		// 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 已提交
305

306 307 308 309 310
			return TPromise.as<EditorModel>(this);
		}

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

313
	private loadWithContent(content: IRawTextContent | IContent, backup?: URI): TPromise<EditorModel> {
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
		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,
		};
		this.updateVersionOnDiskStat(resolvedStat);

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

342 343
		// Update Existing Model
		if (this.textEditorModel) {
344
			return this.doUpdateTextModel(content.value);
345
		}
E
Erich Gamma 已提交
346

347 348 349
		// 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 已提交
350

351 352
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
353

354
		// Create New Model
355
		return this.doCreateTextModel(content.resource, content.value, backup);
356
	}
E
Erich Gamma 已提交
357

358
	private doUpdateTextModel(value: string | ITextSource2): TPromise<EditorModel> {
359
		diag('load() - updated text editor model', this.resource, new Date());
360

361 362 363 364 365 366 367 368 369 370 371 372
		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);
	}

373
	private doCreateTextModel(resource: URI, value: string | ITextSource2, backup: URI): TPromise<EditorModel> {
374 375
		diag('load() - created text editor model', this.resource, new Date());

376
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
377 378 379 380 381 382 383 384 385 386 387 388 389
			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);
					}
390 391
				}

392 393 394 395 396
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

397
				this.toDispose.push(this.textEditorModel.onDidChangeRawContent(e => this.onModelContentChanged(e)));
398 399 400 401 402 403

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

				return TPromise.wrapError(error);
404
			});
405
		});
406

407 408 409
		return this.createTextEditorModelPromise;
	}

410 411 412 413
	private doLoadBackup(backup: URI): TPromise<string> {
		if (!backup) {
			return TPromise.as(null);
		}
414

415
		return this.textFileService.resolveTextContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => {
416
			return this.backupFileService.parseBackupContent(backup.value);
417
		}, error => null /* ignore errors */);
E
Erich Gamma 已提交
418 419
	}

420
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
421 422 423 424
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

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

		// 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 已提交
429
		diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
430 431 432 433 434 435 436 437 438 439

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

			// Clear flags
444
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
445 446 447
			this.setDirty(false);

			// Emit event
448
			if (wasDirty) {
449
				this._onDidStateChange.fire(StateChange.REVERTED);
450
			}
E
Erich Gamma 已提交
451 452 453 454 455 456 457

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
458
		this.makeDirty();
E
Erich Gamma 已提交
459 460

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
461
		if (this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
462 463 464 465 466 467
			if (!this.inConflictResolutionMode) {
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
468

469 470
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
471 472
	}

B
Benjamin Pasero 已提交
473
	private makeDirty(): void {
E
Erich Gamma 已提交
474 475

		// Track dirty state and version id
B
Benjamin Pasero 已提交
476
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
477 478 479 480
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
481
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
482 483 484 485
		}
	}

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

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

491
		// Create new save promise and keep it
492
		this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
493 494 495

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

500
		return this.autoSavePromise;
E
Erich Gamma 已提交
501 502
	}

503 504 505 506
	private cancelAutoSavePromise(): void {
		if (this.autoSavePromise) {
			this.autoSavePromise.cancel();
			this.autoSavePromise = void 0;
E
Erich Gamma 已提交
507 508 509 510 511 512
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
B
Benjamin Pasero 已提交
513
	public save(options: IModelSaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
514 515 516 517 518 519 520
		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
521
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
522

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

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

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

537
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
538 539
		}

540
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
541 542 543 544 545 546
		//
		// 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.
		//
547
		if ((!force && !this.dirty) || versionId !== this.versionId) {
B
Benjamin Pasero 已提交
548
			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 已提交
549 550 551 552

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

553
		// Return if currently saving by storing this save request as the next save that should happen.
554
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
555
		//
556
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
557
		//             kicks in.
558 559
		// 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 已提交
560
		//
561
		if (this.saveSequentializer.hasPendingSave()) {
B
Benjamin Pasero 已提交
562
			diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
E
Erich Gamma 已提交
563

564 565
			// 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 已提交
566 567 568 569
		}

		// 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
570
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
571 572 573
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
574
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
575 576
		// 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
577 578
		// 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 已提交
579
		let saveParticipantPromise = TPromise.as(versionId);
580
		if (TextFileEditorModel.saveParticipant && !this.lifecycleService.willShutdown) {
B
💄  
Benjamin Pasero 已提交
581
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
582
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
583

584
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
585 586
			};

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

590
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
591
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
592 593
		}

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

597 598 599 600
			// 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) {
601 602 603 604 605
				return this.fileService.touchFile(this.resource).then(stat => {

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

608
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
609 610 611 612 613 614 615 616 617
			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
618
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
J
Johannes Rieken 已提交
619
			diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date());
620
			return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.versionOnDiskStat.resource, this.getValue(), {
B
💄  
Benjamin Pasero 已提交
621 622
				overwriteReadonly,
				overwriteEncoding,
J
Johannes Rieken 已提交
623 624 625
				mtime: this.versionOnDiskStat.mtime,
				encoding: this.getEncoding(),
				etag: this.versionOnDiskStat.etag
626
			}).then(stat => {
J
Johannes Rieken 已提交
627 628 629
				diag(`doSave(${versionId}) - after updateContent()`, this.resource, new Date());

				// Telemetry
630
				this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.versionOnDiskStat.resource.fsPath) });
J
Johannes Rieken 已提交
631 632 633 634 635 636 637 638

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

640
				// Updated resolved stat with updated stat
J
Johannes Rieken 已提交
641
				this.updateVersionOnDiskStat(stat);
E
Erich Gamma 已提交
642

643 644 645
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

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

J
Johannes Rieken 已提交
651 652
				// Flag as error state
				this.inErrorMode = true;
E
Erich Gamma 已提交
653

J
Johannes Rieken 已提交
654 655
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
656

J
Johannes Rieken 已提交
657 658
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
659 660
			}));
		}));
E
Erich Gamma 已提交
661 662 663
	}

	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
664 665 666 667
		const wasDirty = this.dirty;
		const wasInConflictResolutionMode = this.inConflictResolutionMode;
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728

		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 已提交
729
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
730
	 */
B
Benjamin Pasero 已提交
731 732
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
733 734 735 736 737 738 739 740 741 742 743 744
	}

	/**
	 * 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 已提交
745
	public getState(): ModelState {
E
Erich Gamma 已提交
746
		if (this.inConflictResolutionMode) {
B
Benjamin Pasero 已提交
747
			return ModelState.CONFLICT;
E
Erich Gamma 已提交
748 749 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 787
	}

	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 已提交
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 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
		}

		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;

853
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
854 855
		this.createTextEditorModelPromise = null;

856
		this.cancelAutoSavePromise();
857
		this.contentChangeEventScheduler.cancel();
D
Daniel Imms 已提交
858

E
Erich Gamma 已提交
859 860
		super.dispose();
	}
861 862
}

863 864 865 866 867
interface IPendingSave {
	versionId: number;
	promise: TPromise<void>;
}

868 869 870 871 872 873 874 875
interface ISaveOperation {
	promise: TPromise<void>;
	promiseValue: TValueCallback<void>;
	promiseError: ErrorCallback;
	run: () => TPromise<void>;
}

export class SaveSequentializer {
876
	private _pendingSave: IPendingSave;
877
	private _nextSave: ISaveOperation;
878 879 880 881 882 883 884 885 886 887 888 889 890

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

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

		return !!this._pendingSave;
	}

891 892 893 894
	public get pendingSave(): TPromise<void> {
		return this._pendingSave ? this._pendingSave.promise : void 0;
	}

895 896 897 898 899 900 901 902 903 904
	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) {
905 906 907 908 909 910

			// 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();
911 912 913
		}
	}

914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950
	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;
951 952 953
	}
}

954 955
class DefaultSaveErrorHandler implements ISaveErrorHandler {

J
Johannes Rieken 已提交
956
	constructor( @IMessageService private messageService: IMessageService) { }
957 958 959 960 961 962 963 964 965 966 967 968

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