textFileEditorModel.ts 34.8 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 { EventType as EditorEventType } 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, FileChangesEvent, FileChangeType } 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
	public static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
45

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

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

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

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

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

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

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

106 107 108
		this.orphanedChangeEventScheduler = new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY);
		this.toDispose.push(this.orphanedChangeEventScheduler);

109
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
110

111
		this.registerListeners();
E
Erich Gamma 已提交
112 113
	}

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

121
				// Cancel any content change event promises as they are no longer valid.
122
				this.contentChangeEventScheduler.cancel();
123 124

				// Refire state change reverted events as content change events
125 126 127
				this._onDidContentChange.fire(StateChange.REVERTED);
			}
		}));
128 129
	}

130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
	private onFileChanges(e: FileChangesEvent): void {

		// Handle added if we are in orphan mode
		if (this.inOrphanMode && e.contains(this.resource, FileChangeType.ADDED)) {
			this.setOrphaned(false);
		}

		// Handle deletes
		if (!this.inOrphanMode && e.contains(this.resource, FileChangeType.DELETED)) {
			this.setOrphaned(true);
		}
	}

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

150
	private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
151
		if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
152
			this.autoSaveAfterMillies = config.autoSaveDelay;
153
			this.autoSaveAfterMilliesEnabled = true;
E
Erich Gamma 已提交
154
		} else {
155
			this.autoSaveAfterMillies = void 0;
156
			this.autoSaveAfterMilliesEnabled = false;
E
Erich Gamma 已提交
157 158 159
		}
	}

160 161 162 163
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

B
Benjamin Pasero 已提交
164 165 166 167 168 169 170 171 172 173 174
	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);
	}

175
	public get onDidContentChange(): Event<StateChange> {
176 177 178
		return this._onDidContentChange.event;
	}

179 180 181 182
	public get onDidStateChange(): Event<StateChange> {
		return this._onDidStateChange.event;
	}

183 184 185 186 187 188 189
	/**
	 * The current version id of the model.
	 */
	public getVersionId(): number {
		return this.versionId;
	}

E
Erich Gamma 已提交
190 191 192 193 194 195 196
	/**
	 * 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 已提交
197 198 199 200 201 202 203
	/**
	 * Set a save participant handler to react on models getting saved.
	 */
	public static setSaveParticipant(handler: ISaveParticipant): void {
		TextFileEditorModel.saveParticipant = handler;
	}

E
Erich Gamma 已提交
204 205
	/**
	 * Discards any local changes and replaces the model with the contents of the version on disk.
206 207
	 *
	 * @param if the parameter soft is true, will not attempt to load the contents from disk.
E
Erich Gamma 已提交
208
	 */
209
	public revert(soft?: boolean): TPromise<void> {
E
Erich Gamma 已提交
210 211 212 213
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

214 215
		// Cancel any running auto-save
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
216 217

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

220 221 222 223 224 225 226 227
		let loadPromise: TPromise<EditorModel>;
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
			loadPromise = this.load(true /* force */);
		}

		return loadPromise.then(() => {
E
Erich Gamma 已提交
228 229

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

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

236
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
		});
	}

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

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 285 286 287 288 289
		// 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 已提交
290 291 292
		// Decide on etag
		let etag: string;
		if (force) {
293
			etag = void 0; // bypass cache if force loading is true
294 295
		} else if (this.lastResolvedDiskStat) {
			etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
296 297 298
		}

		// Resolve Content
299
		return this.textFileService
300
			.resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding })
301 302 303 304 305 306 307 308 309
			.then(content => this.handleLoadSuccess(content), error => this.handleLoadError(error));
	}

	private handleLoadSuccess(content: IRawTextContent): TPromise<EditorModel> {

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

		return this.loadWithContent(content);
310
	}
E
Erich Gamma 已提交
311

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

315 316 317
		// Apply orphaned state based on error code
		this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);

318 319 320
		// 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 已提交
321

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

325 326 327 328 329 330 331
		// 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) {
			return TPromise.as<EditorModel>(this);
		}

332 333
		// Otherwise bubble up the error
		return TPromise.wrapError(error);
334
	}
E
Erich Gamma 已提交
335

336
	private loadWithContent(content: IRawTextContent | IContent, backup?: URI): TPromise<EditorModel> {
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
		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,
		};
352
		this.updateLastResolvedDiskStat(resolvedStat);
353 354 355 356 357 358 359 360 361 362 363

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

365 366
		// Update Existing Model
		if (this.textEditorModel) {
367
			return this.doUpdateTextModel(content.value);
368
		}
E
Erich Gamma 已提交
369

370 371 372
		// 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 已提交
373

374 375
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
376

377
		// Create New Model
378
		return this.doCreateTextModel(content.resource, content.value, backup);
379
	}
E
Erich Gamma 已提交
380

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

384 385 386 387 388 389 390 391 392 393 394 395
		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 已提交
396
	private doCreateTextModel(resource: URI, value: string | IRawTextSource, backup: URI): TPromise<EditorModel> {
397 398
		diag('load() - created text editor model', this.resource, new Date());

399
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
400 401 402 403 404 405 406 407 408 409 410 411 412
			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);
					}
413 414
				}

415 416 417 418 419
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

420 421 422 423 424 425 426 427 428 429 430 431 432
				this.toDispose.push(this.textEditorModel.addBulkListener((events) => {
					let hasContentChangeEvent = false;
					for (let i = 0, len = events.length; i < len; i++) {
						let eventType = events[i].getType();
						if (eventType === EditorEventType.ModelContentChanged2) {
							hasContentChangeEvent = true;
							break;
						}
					}
					if (hasContentChangeEvent) {
						this.onModelContentChanged();
					}
				}));
433 434 435 436 437 438

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

				return TPromise.wrapError(error);
439
			});
440
		});
441

442 443 444
		return this.createTextEditorModelPromise;
	}

445 446 447 448
	private doLoadBackup(backup: URI): TPromise<string> {
		if (!backup) {
			return TPromise.as(null);
		}
449

450
		return this.textFileService.resolveTextContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => {
451
			return this.backupFileService.parseBackupContent(backup.value);
452
		}, error => null /* ignore errors */);
E
Erich Gamma 已提交
453 454
	}

455
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
456 457 458
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

459 460
	private onModelContentChanged(): void {
		diag(`onModelContentChanged() - enter`, this.resource, new Date());
E
Erich Gamma 已提交
461 462 463

		// 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 已提交
464
		diag(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource, new Date());
E
Erich Gamma 已提交
465 466 467 468 469 470 471 472 473 474

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

			// Clear flags
479
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
480 481 482
			this.setDirty(false);

			// Emit event
483
			if (wasDirty) {
484
				this._onDidStateChange.fire(StateChange.REVERTED);
485
			}
E
Erich Gamma 已提交
486 487 488 489 490 491 492

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
493
		this.makeDirty();
E
Erich Gamma 已提交
494 495

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
496
		if (this.autoSaveAfterMilliesEnabled) {
497
			if (!this.inConflictMode) {
E
Erich Gamma 已提交
498 499 500 501 502
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
503

504 505
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
506 507
	}

B
Benjamin Pasero 已提交
508
	private makeDirty(): void {
E
Erich Gamma 已提交
509 510

		// Track dirty state and version id
B
Benjamin Pasero 已提交
511
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
512 513 514 515
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
516
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
517 518 519 520
		}
	}

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

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

526
		// Create new save promise and keep it
527
		this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
528 529 530

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

535
		return this.autoSavePromise;
E
Erich Gamma 已提交
536 537
	}

538 539 540 541
	private cancelAutoSavePromise(): void {
		if (this.autoSavePromise) {
			this.autoSavePromise.cancel();
			this.autoSavePromise = void 0;
E
Erich Gamma 已提交
542 543 544 545 546 547
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
B
Benjamin Pasero 已提交
548
	public save(options: IModelSaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
549 550 551 552 553 554 555
		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
556
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
557

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

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

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

572
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
573 574
		}

575
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
576 577 578 579 580 581
		//
		// 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.
		//
582
		if ((!force && !this.dirty) || versionId !== this.versionId) {
B
Benjamin Pasero 已提交
583
			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 已提交
584 585 586 587

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

588
		// Return if currently saving by storing this save request as the next save that should happen.
589
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
590
		//
591
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
592
		//             kicks in.
593 594
		// 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 已提交
595
		//
596
		if (this.saveSequentializer.hasPendingSave()) {
B
Benjamin Pasero 已提交
597
			diag(`doSave(${versionId}) - exit - because busy saving`, this.resource, new Date());
E
Erich Gamma 已提交
598

599 600
			// 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 已提交
601 602 603 604
		}

		// 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
605
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
606 607 608
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
609
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
610 611
		// 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
612 613
		// 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 已提交
614
		let saveParticipantPromise = TPromise.as(versionId);
615
		if (TextFileEditorModel.saveParticipant && !this.lifecycleService.willShutdown) {
B
💄  
Benjamin Pasero 已提交
616
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
617
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
618

619
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
620 621
			};

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

625
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
626
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
627 628
		}

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

632 633 634 635
			// 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) {
636
				return this.doTouch();
637 638
			}

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

				// Telemetry
661
				this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.lastResolvedDiskStat.resource.fsPath) });
J
Johannes Rieken 已提交
662 663 664 665 666 667 668 669

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

671
				// Updated resolved stat with updated stat
672
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
673

674 675 676
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

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

682
				// Flag as error state in the model
J
Johannes Rieken 已提交
683
				this.inErrorMode = true;
E
Erich Gamma 已提交
684

685 686 687 688 689
				// Look out for a save conflict
				if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
690 691
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
692

J
Johannes Rieken 已提交
693 694
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
695 696
			}));
		}));
E
Erich Gamma 已提交
697 698
	}

699 700 701 702 703 704 705 706
	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 已提交
707
	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
708
		const wasDirty = this.dirty;
709
		const wasInConflictMode = this.inConflictMode;
B
Benjamin Pasero 已提交
710 711
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
712 713 714

		if (!dirty) {
			this.dirty = false;
715
			this.inConflictMode = false;
E
Erich Gamma 已提交
716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732
			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;
733
			this.inConflictMode = wasInConflictMode;
E
Erich Gamma 已提交
734 735 736 737 738
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

739
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStat): void {
E
Erich Gamma 已提交
740 741

		// First resolve - just take
742 743
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
744 745 746 747 748
		}

		// 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.
749 750
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
		}
	}

	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 已提交
773
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
774
	 */
B
Benjamin Pasero 已提交
775 776
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
777 778
	}

779 780 781 782 783
	/**
	 * 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 已提交
784 785 786
	}

	/**
787
	 * Answers if this model is in a specific state.
E
Erich Gamma 已提交
788
	 */
789 790 791 792 793 794 795 796 797 798 799 800 801 802
	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 已提交
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
		}
	}

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

825
			if (!this.inConflictMode) {
B
Benjamin Pasero 已提交
826
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
			}
		}

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

845
	public updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
846 847 848 849 850 851 852
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
853
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
854 855 856 857 858 859 860 861
	}

	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) {
862
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
863 864 865 866 867 868
		}

		return true;
	}

	public isResolved(): boolean {
869
		return !types.isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
	}

	/**
	 * 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 已提交
886 887 888 889 890 891 892
	/**
	 * Stat accessor only used by tests.
	 */
	public getStat(): IFileStat {
		return this.lastResolvedDiskStat;
	}

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

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

902
		this.cancelAutoSavePromise();
D
Daniel Imms 已提交
903

E
Erich Gamma 已提交
904 905
		super.dispose();
	}
906 907
}

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

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

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

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

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

		return !!this._pendingSave;
	}

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

940 941 942 943 944 945 946 947 948 949
	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) {
950 951 952 953 954 955

			// 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();
956 957 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
	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;
996 997 998
	}
}

999 1000
class DefaultSaveErrorHandler implements ISaveErrorHandler {

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

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