textFileEditorModel.ts 35.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');
J
Johannes Rieken 已提交
19 20
import { IMode } from 'vs/editor/common/modes';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
21
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, IModelSaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
J
Johannes Rieken 已提交
22 23
import { EncodingMode, EditorModel } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
24
import { IBackupFileService, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
25
import { IFileService, IFileStat, IFileOperationResult, FileOperationResult, IContent, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
26 27 28
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';
29
import { IModelService } from 'vs/editor/common/services/modelService';
30 31
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { anonymize } from 'vs/platform/telemetry/common/telemetryUtils';
32
import { RunOnceScheduler } from 'vs/base/common/async';
A
Alex Dima 已提交
33
import { IRawTextSource } from 'vs/editor/common/model/textSource';
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
	public static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
44

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

	private resource: URI;
	private contentEncoding: string; 			// encoding as reported from disk
	private preferredEncoding: string;			// encoding as chosen by the user
	private dirty: boolean;
	private versionId: number;
	private bufferSavedVersionId: number;
54
	private lastResolvedDiskStat: IFileStat;
55
	private toDispose: IDisposable[];
E
Erich Gamma 已提交
56
	private blockModelContentChange: boolean;
57
	private autoSaveAfterMillies: number;
58
	private autoSaveAfterMilliesEnabled: boolean;
59
	private autoSavePromise: TPromise<void>;
60
	private contentChangeEventScheduler: RunOnceScheduler;
61
	private orphanedChangeEventScheduler: RunOnceScheduler;
62
	private saveSequentializer: SaveSequentializer;
E
Erich Gamma 已提交
63
	private disposed: 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
	private inConflictMode: boolean;
	private inOrphanMode: boolean;
	private inErrorMode: boolean;

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

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

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

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

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

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

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

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

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

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

129 130
	private onFileChanges(e: FileChangesEvent): void {

131
		// Track ADD and DELETES for updates of this model to orphan-mode
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
		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);
				}
			});
157 158 159 160 161 162 163 164 165 166
		}
	}

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

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

177 178 179 180
	private onFilesAssociationChange(): void {
		this.updateTextEditorModelMode();
	}

B
Benjamin Pasero 已提交
181 182 183 184 185 186 187 188 189 190 191
	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);
	}

192
	public get onDidContentChange(): Event<StateChange> {
193 194 195
		return this._onDidContentChange.event;
	}

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

200 201 202 203 204 205 206
	/**
	 * The current version id of the model.
	 */
	public getVersionId(): number {
		return this.versionId;
	}

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

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

231 232
		// Cancel any running auto-save
		this.cancelAutoSavePromise();
E
Erich Gamma 已提交
233 234

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

237 238 239 240 241 242 243 244
		let loadPromise: TPromise<EditorModel>;
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
			loadPromise = this.load(true /* force */);
		}

		return loadPromise.then(() => {
E
Erich Gamma 已提交
245 246

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

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

253
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
		});
	}

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

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
		// 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 */
294
					encoding: this.fileService.getEncoding(this.resource, this.preferredEncoding)
295 296 297 298 299 300 301 302 303 304 305 306
				};

				return this.loadWithContent(content, backup);
			}

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

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

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

		// Resolve Content
316
		return this.textFileService
317
			.resolveTextContent(this.resource, { acceptTextOnly: true, etag, encoding: this.preferredEncoding })
318 319 320 321 322 323 324 325 326
			.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);
327
	}
E
Erich Gamma 已提交
328

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

332 333 334
		// Apply orphaned state based on error code
		this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);

335 336 337
		// 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 已提交
338

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

342 343 344 345 346 347 348
		// 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);
		}

349 350
		// Otherwise bubble up the error
		return TPromise.wrapError(error);
351
	}
E
Erich Gamma 已提交
352

353
	private loadWithContent(content: IRawTextContent | IContent, backup?: URI): TPromise<EditorModel> {
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
		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,
		};
369
		this.updateLastResolvedDiskStat(resolvedStat);
370 371 372 373 374 375 376 377 378 379 380

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

382 383
		// Update Existing Model
		if (this.textEditorModel) {
384
			return this.doUpdateTextModel(content.value);
385
		}
E
Erich Gamma 已提交
386

387 388 389
		// 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 已提交
390

391 392
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
393

394
		// Create New Model
395
		return this.doCreateTextModel(content.resource, content.value, backup);
396
	}
E
Erich Gamma 已提交
397

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

401 402 403 404 405 406 407 408 409 410 411 412
		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 已提交
413
	private doCreateTextModel(resource: URI, value: string | IRawTextSource, backup: URI): TPromise<EditorModel> {
414 415
		diag('load() - created text editor model', this.resource, new Date());

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

432 433 434 435 436
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

437 438
				this.toDispose.push(this.textEditorModel.onDidChangeContent((e) => {
					this.onModelContentChanged();
439
				}));
440 441 442 443 444 445

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

				return TPromise.wrapError(error);
446
			});
447
		});
448

449 450 451
		return this.createTextEditorModelPromise;
	}

452 453 454 455
	private doLoadBackup(backup: URI): TPromise<string> {
		if (!backup) {
			return TPromise.as(null);
		}
456

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

462
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
463 464 465
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

466 467
	private onModelContentChanged(): void {
		diag(`onModelContentChanged() - enter`, this.resource, new Date());
E
Erich Gamma 已提交
468 469 470

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

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

			// Clear flags
486
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
487 488 489
			this.setDirty(false);

			// Emit event
490
			if (wasDirty) {
491
				this._onDidStateChange.fire(StateChange.REVERTED);
492
			}
E
Erich Gamma 已提交
493 494 495 496 497 498 499

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
500
		this.makeDirty();
E
Erich Gamma 已提交
501 502

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

511 512
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
513 514
	}

B
Benjamin Pasero 已提交
515
	private makeDirty(): void {
E
Erich Gamma 已提交
516 517

		// Track dirty state and version id
B
Benjamin Pasero 已提交
518
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
519 520 521 522
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
523
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
524 525 526 527
		}
	}

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

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

533
		// Create new save promise and keep it
534
		this.autoSavePromise = TPromise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
535 536 537

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

542
		return this.autoSavePromise;
E
Erich Gamma 已提交
543 544
	}

545 546 547 548
	private cancelAutoSavePromise(): void {
		if (this.autoSavePromise) {
			this.autoSavePromise.cancel();
			this.autoSavePromise = void 0;
E
Erich Gamma 已提交
549 550 551 552 553 554
		}
	}

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

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

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

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

579
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
580 581
		}

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

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

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

606 607
			// 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 已提交
608 609 610 611
		}

		// 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
612
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
613 614 615
			this.textEditorModel.pushStackElement();
		}

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

626
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
627 628
			};

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

632
				return TextFileEditorModel.saveParticipant.participate(this, { reason });
B
💄  
Benjamin Pasero 已提交
633
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
634 635
		}

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

639 640 641 642
			// 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) {
643
				return this.doTouch();
644 645
			}

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

				// Telemetry
668
				this.telemetryService.publicLog('filePUT', { mimeType: guessMimeTypes(this.resource.fsPath).join(', '), ext: paths.extname(this.lastResolvedDiskStat.resource.fsPath) });
J
Johannes Rieken 已提交
669 670 671 672 673 674 675 676

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

678
				// Updated resolved stat with updated stat
679
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
680

681 682 683
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

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

689
				// Flag as error state in the model
J
Johannes Rieken 已提交
690
				this.inErrorMode = true;
E
Erich Gamma 已提交
691

692 693 694 695 696
				// Look out for a save conflict
				if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
697 698
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
699

J
Johannes Rieken 已提交
700 701
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
702 703
			}));
		}));
E
Erich Gamma 已提交
704 705
	}

706 707 708 709 710 711 712 713
	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 已提交
714
	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
715
		const wasDirty = this.dirty;
716
		const wasInConflictMode = this.inConflictMode;
B
Benjamin Pasero 已提交
717 718
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
719 720 721

		if (!dirty) {
			this.dirty = false;
722
			this.inConflictMode = false;
E
Erich Gamma 已提交
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
			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;
740
			this.inConflictMode = wasInConflictMode;
E
Erich Gamma 已提交
741 742 743 744 745
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

746
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStat): void {
E
Erich Gamma 已提交
747 748

		// First resolve - just take
749 750
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
751 752 753 754 755
		}

		// 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.
756 757
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
		}
	}

	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 已提交
780
	 * Returns the time in millies when this working copy was attempted to be saved.
E
Erich Gamma 已提交
781
	 */
B
Benjamin Pasero 已提交
782 783
	public getLastSaveAttemptTime(): number {
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
784 785
	}

786 787 788 789 790
	/**
	 * 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 已提交
791 792 793
	}

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

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

832
			if (!this.inConflictMode) {
B
Benjamin Pasero 已提交
833
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
			}
		}

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

852
	public updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
853 854 855 856 857 858 859
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
860
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
861 862 863 864 865 866 867 868
	}

	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) {
869
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
870 871 872 873 874 875
		}

		return true;
	}

	public isResolved(): boolean {
876
		return !types.isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
	}

	/**
	 * 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 已提交
893 894 895 896 897 898 899
	/**
	 * Stat accessor only used by tests.
	 */
	public getStat(): IFileStat {
		return this.lastResolvedDiskStat;
	}

E
Erich Gamma 已提交
900 901
	public dispose(): void {
		this.disposed = true;
902 903
		this.inConflictMode = false;
		this.inOrphanMode = false;
E
Erich Gamma 已提交
904 905
		this.inErrorMode = false;

906
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
907 908
		this.createTextEditorModelPromise = null;

909
		this.cancelAutoSavePromise();
D
Daniel Imms 已提交
910

E
Erich Gamma 已提交
911 912
		super.dispose();
	}
913 914
}

915 916 917 918 919
interface IPendingSave {
	versionId: number;
	promise: TPromise<void>;
}

920 921 922 923 924 925 926 927
interface ISaveOperation {
	promise: TPromise<void>;
	promiseValue: TValueCallback<void>;
	promiseError: ErrorCallback;
	run: () => TPromise<void>;
}

export class SaveSequentializer {
928
	private _pendingSave: IPendingSave;
929
	private _nextSave: ISaveOperation;
930 931 932 933 934 935 936 937 938 939 940 941 942

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

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

		return !!this._pendingSave;
	}

943 944 945 946
	public get pendingSave(): TPromise<void> {
		return this._pendingSave ? this._pendingSave.promise : void 0;
	}

947 948 949 950 951 952 953 954 955 956
	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) {
957 958 959 960 961 962

			// 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();
963 964 965
		}
	}

966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002
	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;
1003 1004 1005
	}
}

1006 1007
class DefaultSaveErrorHandler implements ISaveErrorHandler {

J
Johannes Rieken 已提交
1008
	constructor( @IMessageService private messageService: IMessageService) { }
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020

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