textFileEditorModel.ts 40.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

T
t-amqi 已提交
7
import * as path from 'vs/base/common/paths';
8
import * as nls from 'vs/nls';
M
Matt Bierner 已提交
9
import { Event, Emitter } from 'vs/base/common/event';
10
import { TPromise, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
J
Johannes Rieken 已提交
11 12 13
import { onUnexpectedError } from 'vs/base/common/errors';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
E
Erich Gamma 已提交
14
import URI from 'vs/base/common/uri';
15
import { isUndefinedOrNull } from 'vs/base/common/types';
J
Johannes Rieken 已提交
16
import { IMode } from 'vs/editor/common/modes';
17
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
18
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
19
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, IRawTextContent, ILoadOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles';
B
Benjamin Pasero 已提交
20
import { EncodingMode } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
21
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
22
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
23
import { IFileService, IFileStat, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
24 25
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModeService } from 'vs/editor/common/services/modeService';
26
import { IModelService } from 'vs/editor/common/services/modelService';
27
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
28
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
29
import { ITextBufferFactory } from 'vs/editor/common/model';
R
Ramya Achutha Rao 已提交
30
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
31
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
32
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
33
import { isLinux } from 'vs/base/common/platform';
34
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
I
isidor 已提交
35
import { ILogService } from 'vs/platform/log/common/log';
36
import { isEqual, isEqualOrParent } from 'vs/base/common/resources';
B
Benjamin Pasero 已提交
37

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

B
Benjamin Pasero 已提交
43 44
	static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
	static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
K
kieferrm 已提交
45
	static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json'];
46
	static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json'];
47

E
Erich Gamma 已提交
48
	private static saveErrorHandler: ISaveErrorHandler;
B
Benjamin Pasero 已提交
49 50
	static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; }

B
Benjamin Pasero 已提交
51
	private static saveParticipant: ISaveParticipant;
B
Benjamin Pasero 已提交
52
	static setSaveParticipant(handler: ISaveParticipant): void { TextFileEditorModel.saveParticipant = handler; }
E
Erich Gamma 已提交
53

B
Benjamin Pasero 已提交
54 55 56 57 58 59
	private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
	get onDidContentChange(): Event<StateChange> { return this._onDidContentChange.event; }

	private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
	get onDidStateChange(): Event<StateChange> { return this._onDidStateChange.event; }

E
Erich Gamma 已提交
60 61 62 63 64 65
	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;
66
	private lastResolvedDiskStat: IFileStat;
E
Erich Gamma 已提交
67
	private blockModelContentChange: boolean;
68
	private autoSaveAfterMillies: number;
69
	private autoSaveAfterMilliesEnabled: boolean;
70
	private autoSaveDisposable: IDisposable;
71
	private contentChangeEventScheduler: RunOnceScheduler;
72
	private orphanedChangeEventScheduler: RunOnceScheduler;
73
	private saveSequentializer: SaveSequentializer;
E
Erich Gamma 已提交
74
	private disposed: boolean;
B
Benjamin Pasero 已提交
75
	private lastSaveAttemptTime: number;
E
Erich Gamma 已提交
76
	private createTextEditorModelPromise: TPromise<TextFileEditorModel>;
77 78 79 80
	private inConflictMode: boolean;
	private inOrphanMode: boolean;
	private inErrorMode: boolean;

E
Erich Gamma 已提交
81 82 83
	constructor(
		resource: URI,
		preferredEncoding: string,
84
		@INotificationService private notificationService: INotificationService,
E
Erich Gamma 已提交
85 86 87 88
		@IModeService modeService: IModeService,
		@IModelService modelService: IModelService,
		@IFileService private fileService: IFileService,
		@IInstantiationService private instantiationService: IInstantiationService,
89
		@ITelemetryService private telemetryService: ITelemetryService,
90
		@ITextFileService private textFileService: ITextFileService,
91
		@IBackupFileService private backupFileService: IBackupFileService,
92
		@IEnvironmentService private environmentService: IEnvironmentService,
T
t-amqi 已提交
93
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
I
isidor 已提交
94 95
		@IHashService private hashService: IHashService,
		@ILogService private logService: ILogService
E
Erich Gamma 已提交
96 97
	) {
		super(modelService, modeService);
B
Benjamin Pasero 已提交
98

E
Erich Gamma 已提交
99 100
		this.resource = resource;
		this.preferredEncoding = preferredEncoding;
101
		this.inOrphanMode = false;
E
Erich Gamma 已提交
102 103
		this.dirty = false;
		this.versionId = 0;
B
Benjamin Pasero 已提交
104
		this.lastSaveAttemptTime = 0;
105
		this.saveSequentializer = new SaveSequentializer();
106

B
Benjamin Pasero 已提交
107 108
		this.contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
		this.orphanedChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY));
109

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

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

115
	private registerListeners(): void {
B
Benjamin Pasero 已提交
116 117 118 119
		this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
		this._register(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
		this._register(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
		this._register(this.onDidStateChange(e => this.onStateChange(e)));
B
Benjamin Pasero 已提交
120
	}
121

B
Benjamin Pasero 已提交
122 123
	private onStateChange(e: StateChange): void {
		if (e === StateChange.REVERTED) {
124

B
Benjamin Pasero 已提交
125 126 127 128 129 130
			// Cancel any content change event promises as they are no longer valid.
			this.contentChangeEventScheduler.cancel();

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

133
	private onFileChanges(e: FileChangesEvent): void {
134 135 136 137 138 139 140 141 142 143 144
		let fileEventImpactsModel = false;
		let newInOrphanModeGuess: boolean;

		// If we are currently orphaned, we check if the model file was added back
		if (this.inOrphanMode) {
			const modelFileAdded = e.contains(this.resource, FileChangeType.ADDED);
			if (modelFileAdded) {
				newInOrphanModeGuess = false;
				fileEventImpactsModel = true;
			}
		}
145

146 147 148 149 150 151 152 153
		// Otherwise we check if the model file was deleted
		else {
			const modelFileDeleted = e.contains(this.resource, FileChangeType.DELETED);
			if (modelFileDeleted) {
				newInOrphanModeGuess = true;
				fileEventImpactsModel = true;
			}
		}
154

155
		if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) {
156
			let checkOrphanedPromise: Thenable<boolean>;
157 158 159 160 161
			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.
162
				checkOrphanedPromise = timeout(100).then(() => {
163 164
					if (this.disposed) {
						return true;
165
					}
166 167

					return this.fileService.existsFile(this.resource).then(exists => !exists);
168
				});
169
			} else {
170
				checkOrphanedPromise = Promise.resolve(false);
171
			}
172

173
			checkOrphanedPromise.then(newInOrphanModeValidated => {
174 175 176 177
				if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
					this.setOrphaned(newInOrphanModeValidated);
				}
			});
178 179 180 181 182 183 184 185 186 187
		}
	}

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

188
	private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
B
Benjamin Pasero 已提交
189
		const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0;
E
Erich Gamma 已提交
190

B
Benjamin Pasero 已提交
191 192
		this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled;
		this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : void 0;
193 194
	}

B
Benjamin Pasero 已提交
195
	private onFilesAssociationChange(): void {
B
Benjamin Pasero 已提交
196 197 198 199
		if (!this.textEditorModel) {
			return;
		}

A
Alex Dima 已提交
200
		const firstLineText = this.getFirstLineText(this.textEditorModel);
B
Benjamin Pasero 已提交
201
		const mode = this.getOrCreateMode(this.modeService, void 0, firstLineText);
B
Benjamin Pasero 已提交
202 203 204 205

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

B
Benjamin Pasero 已提交
206
	getVersionId(): number {
207 208 209
		return this.versionId;
	}

B
Benjamin Pasero 已提交
210
	revert(soft?: boolean): TPromise<void> {
E
Erich Gamma 已提交
211
		if (!this.isResolved()) {
212
			return TPromise.wrap<void>(null);
E
Erich Gamma 已提交
213 214
		}

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

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

B
Benjamin Pasero 已提交
221
		let loadPromise: TPromise<TextFileEditorModel>;
222 223 224
		if (soft) {
			loadPromise = TPromise.as(this);
		} else {
225
			loadPromise = this.load({ forceReadFromDisk: true });
226 227 228
		}

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

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

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

237
			return TPromise.wrapError(error);
E
Erich Gamma 已提交
238 239 240
		});
	}

B
Benjamin Pasero 已提交
241
	load(options?: ILoadOptions): TPromise<TextFileEditorModel> {
I
isidor 已提交
242
		this.logService.trace('load() - enter', this.resource);
E
Erich Gamma 已提交
243

B
Benjamin Pasero 已提交
244 245 246
		// It is very important to not reload the model when the model is dirty.
		// We also only want to reload the model from the disk if no save is pending
		// to avoid data loss.
247
		if (this.dirty || this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
248
			this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
E
Erich Gamma 已提交
249 250 251 252

			return TPromise.as(this);
		}

253 254
		// Only for new models we support to load from backup
		if (!this.textEditorModel && !this.createTextEditorModelPromise) {
B
Benjamin Pasero 已提交
255
			return this.loadFromBackup(options);
256 257 258
		}

		// Otherwise load from file resource
259
		return this.loadFromFile(options);
260 261
	}

B
Benjamin Pasero 已提交
262
	private loadFromBackup(options?: ILoadOptions): TPromise<TextFileEditorModel> {
263 264 265 266 267 268 269 270 271
		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) {
272
				const content: IRawTextContent = {
273
					resource: this.resource,
274
					name: path.basename(this.resource.fsPath),
275 276
					mtime: Date.now(),
					etag: void 0,
277
					value: createTextBufferFactory(''), /* will be filled later from backup */
I
isidor 已提交
278 279
					encoding: this.fileService.encoding.getWriteEncoding(this.resource, this.preferredEncoding),
					isReadonly: false
280 281
				};

282
				return this.loadWithContent(content, options, backup);
283 284 285
			}

			// Otherwise load from file
286
			return this.loadFromFile(options);
287 288 289
		});
	}

290 291 292
	private loadFromFile(options?: ILoadOptions): TPromise<TextFileEditorModel> {
		const forceReadFromDisk = options && options.forceReadFromDisk;
		const allowBinary = this.isResolved() /* always allow if we resolved previously */ || (options && options.allowBinary);
293

E
Erich Gamma 已提交
294 295
		// Decide on etag
		let etag: string;
296 297
		if (forceReadFromDisk) {
			etag = void 0; // reset ETag if we enforce to read from disk
298 299
		} else if (this.lastResolvedDiskStat) {
			etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
300 301
		}

B
Benjamin Pasero 已提交
302 303 304 305 306 307 308 309
		// Ensure to track the versionId before doing a long running operation
		// to make sure the model was not changed in the meantime which would
		// indicate that the user or program has made edits. If we would ignore
		// this, we could potentially loose the changes that were made because
		// after resolving the content we update the model and reset the dirty
		// flag.
		const currentVersionId = this.versionId;

T
t-amqi 已提交
310 311
		// Resolve Content
		return this.textFileService
312
			.resolveTextContent(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding })
B
Benjamin Pasero 已提交
313
			.then(content => {
314

B
Benjamin Pasero 已提交
315 316
				// Clear orphaned state when loading was successful
				this.setOrphaned(false);
317

B
Benjamin Pasero 已提交
318 319
				// Guard against the model having changed in the meantime
				if (currentVersionId === this.versionId) {
320
					return this.loadWithContent(content, options);
B
Benjamin Pasero 已提交
321
				}
322

B
Benjamin Pasero 已提交
323 324 325
				return this;
			}, error => {
				const result = error.fileOperationResult;
E
Erich Gamma 已提交
326

B
Benjamin Pasero 已提交
327 328
				// Apply orphaned state based on error code
				this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);
E
Erich Gamma 已提交
329

B
Benjamin Pasero 已提交
330 331
				// NotModified status is expected and can be handled gracefully
				if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
332

B
Benjamin Pasero 已提交
333 334 335 336
					// Guard against the model having changed in the meantime
					if (currentVersionId === this.versionId) {
						this.setDirty(false); // Ensure we are not tracking a stale state
					}
E
Erich Gamma 已提交
337

B
Benjamin Pasero 已提交
338 339
					return TPromise.as<TextFileEditorModel>(this);
				}
340

B
Benjamin Pasero 已提交
341 342 343 344 345 346
				// 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<TextFileEditorModel>(this);
				}
347

B
Benjamin Pasero 已提交
348 349 350
				// Otherwise bubble up the error
				return TPromise.wrapError<TextFileEditorModel>(error);
			});
351
	}
E
Erich Gamma 已提交
352

353
	private loadWithContent(content: IRawTextContent, options?: ILoadOptions, backup?: URI): TPromise<TextFileEditorModel> {
354
		return this.doLoadWithContent(content, backup).then(model => {
355
			// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
356 357
			const settingsType = this.getTypeIfSettings();
			if (settingsType) {
358
				/* __GDPR__
359
					"settingsRead" : {
360
						"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
361
					}
362
				*/
363
				this.telemetryService.publicLog('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
364 365 366
			} else {
				/* __GDPR__
					"fileGet" : {
367 368 369
						"${include}": [
							"${FileTelemetryData}"
						]
370 371
					}
				*/
372
				this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
373
			}
374 375 376 377 378

			return model;
		});
	}

379
	private doLoadWithContent(content: IRawTextContent, backup?: URI): TPromise<TextFileEditorModel> {
I
isidor 已提交
380
		this.logService.trace('load() - resolved content', this.resource);
381 382

		// Update our resolved disk stat model
B
Benjamin Pasero 已提交
383
		this.updateLastResolvedDiskStat({
384 385 386 387 388
			resource: this.resource,
			name: content.name,
			mtime: content.mtime,
			etag: content.etag,
			isDirectory: false,
389
			isSymbolicLink: false,
I
isidor 已提交
390 391
			children: void 0,
			isReadonly: content.isReadonly
B
Benjamin Pasero 已提交
392
		} as IFileStat);
393 394 395 396 397 398 399 400 401 402 403

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

405 406
		// Update Existing Model
		if (this.textEditorModel) {
407
			return this.doUpdateTextModel(content.value);
408
		}
E
Erich Gamma 已提交
409

410 411
		// Join an existing request to create the editor model to avoid race conditions
		else if (this.createTextEditorModelPromise) {
I
isidor 已提交
412
			this.logService.trace('load() - join existing text editor model promise', this.resource);
E
Erich Gamma 已提交
413

414 415
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
416

417
		// Create New Model
418
		return this.doCreateTextModel(content.resource, content.value, backup);
419
	}
E
Erich Gamma 已提交
420

421
	private doUpdateTextModel(value: ITextBufferFactory): TPromise<TextFileEditorModel> {
I
isidor 已提交
422
		this.logService.trace('load() - updated text editor model', this.resource);
423

424 425
		// Ensure we are not tracking a stale state
		this.setDirty(false);
426

427
		// Update model value in a block that ignores model content change events
428 429 430 431 432 433 434
		this.blockModelContentChange = true;
		try {
			this.updateTextEditorModel(value);
		} finally {
			this.blockModelContentChange = false;
		}

435 436 437
		// Ensure we track the latest saved version ID given that the contents changed
		this.updateSavedVersionId();

B
Benjamin Pasero 已提交
438
		return TPromise.as<TextFileEditorModel>(this);
439 440
	}

441
	private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI): TPromise<TextFileEditorModel> {
I
isidor 已提交
442
		this.logService.trace('load() - created text editor model', this.resource);
443

444
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
445
			const hasBackupContent = !!backupContent;
446 447 448 449 450 451 452 453 454 455 456 457

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

460 461 462 463 464
				// Ensure we are not tracking a stale state
				else {
					this.setDirty(false);
				}

B
Benjamin Pasero 已提交
465 466
				// Model Listeners
				this.installModelListeners();
467 468 469 470 471

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

472
				return TPromise.wrapError<TextFileEditorModel>(error);
473
			});
474
		});
475

476 477 478
		return this.createTextEditorModelPromise;
	}

B
Benjamin Pasero 已提交
479 480
	private installModelListeners(): void {

481 482 483
		// See https://github.com/Microsoft/vscode/issues/30189
		// This code has been extracted to a different method because it caused a memory leak
		// where `value` was captured in the content change listener closure scope.
B
Benjamin Pasero 已提交
484 485

		// Content Change
B
Benjamin Pasero 已提交
486
		this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
487 488
	}

489
	private doLoadBackup(backup: URI): TPromise<ITextBufferFactory> {
490 491 492
		if (!backup) {
			return TPromise.as(null);
		}
493

494
		return this.backupFileService.resolveBackupContent(backup).then(backupContent => backupContent, error => null /* ignore errors */);
E
Erich Gamma 已提交
495 496
	}

497
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
498 499 500
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

501
	private onModelContentChanged(): void {
I
isidor 已提交
502
		this.logService.trace(`onModelContentChanged() - enter`, this.resource);
E
Erich Gamma 已提交
503 504 505

		// In any case increment the version id because it tracks the textual content state of the model at all times
		this.versionId++;
I
isidor 已提交
506
		this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource);
E
Erich Gamma 已提交
507 508 509 510 511 512 513 514 515 516

		// 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.
517
		if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
I
isidor 已提交
518
			this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
E
Erich Gamma 已提交
519 520

			// Clear flags
521
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
522 523 524
			this.setDirty(false);

			// Emit event
525
			if (wasDirty) {
526
				this._onDidStateChange.fire(StateChange.REVERTED);
527
			}
E
Erich Gamma 已提交
528 529 530 531

			return;
		}

I
isidor 已提交
532
		this.logService.trace('onModelContentChanged() - model content changed and marked as dirty', this.resource);
E
Erich Gamma 已提交
533 534

		// Mark as dirty
B
Benjamin Pasero 已提交
535
		this.makeDirty();
E
Erich Gamma 已提交
536 537

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
538
		if (this.autoSaveAfterMilliesEnabled) {
539
			if (!this.inConflictMode) {
E
Erich Gamma 已提交
540 541
				this.doAutoSave(this.versionId);
			} else {
I
isidor 已提交
542
				this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource);
E
Erich Gamma 已提交
543 544
			}
		}
545

546 547
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
548 549
	}

B
Benjamin Pasero 已提交
550
	private makeDirty(): void {
E
Erich Gamma 已提交
551 552

		// Track dirty state and version id
B
Benjamin Pasero 已提交
553
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
554 555 556 557
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
558
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
559 560 561
		}
	}

562
	private doAutoSave(versionId: number): void {
I
isidor 已提交
563
		this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
E
Erich Gamma 已提交
564 565

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

568 569
		// Create new save timer and store it for disposal as needed
		const handle = setTimeout(() => {
E
Erich Gamma 已提交
570 571 572

			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
573
				this.doSave(versionId, { reason: 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 已提交
574
			}
575
		}, this.autoSaveAfterMillies);
E
Erich Gamma 已提交
576

577
		this.autoSaveDisposable = toDisposable(() => clearTimeout(handle));
E
Erich Gamma 已提交
578 579
	}

580 581 582 583
	private cancelPendingAutoSave(): void {
		if (this.autoSaveDisposable) {
			this.autoSaveDisposable.dispose();
			this.autoSaveDisposable = void 0;
E
Erich Gamma 已提交
584 585 586
		}
	}

B
Benjamin Pasero 已提交
587
	save(options: ISaveOptions = Object.create(null)): TPromise<void> {
E
Erich Gamma 已提交
588
		if (!this.isResolved()) {
589
			return TPromise.wrap<void>(null);
E
Erich Gamma 已提交
590 591
		}

I
isidor 已提交
592
		this.logService.trace('save() - enter', this.resource);
E
Erich Gamma 已提交
593 594

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

597
		return this.doSave(this.versionId, options);
E
Erich Gamma 已提交
598 599
	}

600
	private doSave(versionId: number, options: ISaveOptions): TPromise<void> {
601
		if (isUndefinedOrNull(options.reason)) {
602 603 604
			options.reason = SaveReason.EXPLICIT;
		}

I
isidor 已提交
605
		this.logService.trace(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource);
E
Erich Gamma 已提交
606 607

		// Lookup any running pending save for this versionId and return it if found
B
Benjamin Pasero 已提交
608 609 610 611
		//
		// Scenario: user invoked the save action multiple times quickly for the same contents
		//           while the save was not yet finished to disk
		//
612
		if (this.saveSequentializer.hasPendingSave(versionId)) {
I
isidor 已提交
613
			this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource);
E
Erich Gamma 已提交
614

615
			return this.saveSequentializer.pendingSave;
E
Erich Gamma 已提交
616 617
		}

618
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
619 620 621 622 623 624
		//
		// 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.
		//
625
		if ((!options.force && !this.dirty) || versionId !== this.versionId) {
I
isidor 已提交
626
			this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource);
E
Erich Gamma 已提交
627

628
			return TPromise.wrap<void>(null);
E
Erich Gamma 已提交
629 630
		}

631
		// Return if currently saving by storing this save request as the next save that should happen.
632
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
633
		//
634
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
635
		//             kicks in.
636 637
		// 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 已提交
638
		//
639
		if (this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
640
			this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource);
E
Erich Gamma 已提交
641

642
			// Register this as the next upcoming save and return
643
			return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options));
E
Erich Gamma 已提交
644 645 646 647
		}

		// 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
648
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
649 650 651
			this.textEditorModel.pushStackElement();
		}

B
Benjamin Pasero 已提交
652
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
653 654
		// 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
655
		// Save participants can also be skipped through API.
J
Johannes Rieken 已提交
656
		let saveParticipantPromise = TPromise.as(versionId);
657
		if (TextFileEditorModel.saveParticipant && !options.skipSaveParticipants) {
B
💄  
Benjamin Pasero 已提交
658
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
659
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
660

661
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
662 663
			};

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

667
				return TextFileEditorModel.saveParticipant.participate(this, { reason: options.reason });
B
💄  
Benjamin Pasero 已提交
668
			}).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
669 670
		}

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

674 675 676 677 678
			// We have to protect against being disposed at this point. It could be that the save() operation
			// was triggerd followed by a dispose() operation right after without waiting. Typically we cannot
			// be disposed if we are dirty, but if we are not dirty, save() and dispose() can still be triggered
			// one after the other without waiting for the save() to complete. If we are disposed(), we risk
			// saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942).
679
			// To fix this issue, we will not store the contents to disk when we got disposed.
680 681 682 683
			if (this.disposed) {
				return void 0;
			}

B
Benjamin Pasero 已提交
684 685
			// Under certain conditions we do a short-cut of flushing contents to disk when we can assume that
			// the file has not changed and as such was not dirty before.
686 687 688 689 690 691
			// The conditions are all of:
			// - a forced, explicit save (Ctrl+S)
			// - the model is not dirty (otherwise we know there are changed which needs to go to the file)
			// - the model is not in orphan mode (because in that case we know the file does not exist on disk)
			// - the model version did not change due to save participants running
			if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === newVersionId) {
B
Benjamin Pasero 已提交
692
				return this.doTouch(newVersionId);
693 694
			}

695
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
696 697 698 699 700 701 702 703 704
			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
705
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
I
isidor 已提交
706
			this.logService.trace(`doSave(${versionId}) - before updateContent()`, this.resource);
707
			return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
708 709
				overwriteReadonly: options.overwriteReadonly,
				overwriteEncoding: options.overwriteEncoding,
710
				mtime: this.lastResolvedDiskStat.mtime,
J
Johannes Rieken 已提交
711
				encoding: this.getEncoding(),
712 713
				etag: this.lastResolvedDiskStat.etag,
				writeElevated: options.writeElevated
714
			}).then(stat => {
I
isidor 已提交
715
				this.logService.trace(`doSave(${versionId}) - after updateContent()`, this.resource);
J
Johannes Rieken 已提交
716 717

				// Telemetry
718 719
				const settingsType = this.getTypeIfSettings();
				if (settingsType) {
K
kieferrm 已提交
720
					/* __GDPR__
721
						"settingsWritten" : {
722
							"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
723
						}
K
kieferrm 已提交
724
					*/
725
					this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
726
				} else {
K
kieferrm 已提交
727
					/* __GDPR__
K
kieferrm 已提交
728
						"filePUT" : {
729 730 731
							"${include}": [
								"${FileTelemetryData}"
							]
K
kieferrm 已提交
732 733
						}
					*/
734
					this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
735
				}
J
Johannes Rieken 已提交
736 737 738

				// Update dirty state unless model has changed meanwhile
				if (versionId === this.versionId) {
I
isidor 已提交
739
					this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource);
J
Johannes Rieken 已提交
740 741
					this.setDirty(false);
				} else {
I
isidor 已提交
742
					this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource);
J
Johannes Rieken 已提交
743
				}
E
Erich Gamma 已提交
744

745
				// Updated resolved stat with updated stat
746
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
747

748 749 750
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

J
Johannes Rieken 已提交
751 752
				// Emit File Saved Event
				this._onDidStateChange.fire(StateChange.SAVED);
753
			}, error => {
754 755
				if (!error) {
					error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051)
M
Martin Aeschlimann 已提交
756 757
				}

I
isidor 已提交
758
				this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
E
Erich Gamma 已提交
759

760
				// Flag as error state in the model
J
Johannes Rieken 已提交
761
				this.inErrorMode = true;
E
Erich Gamma 已提交
762

763
				// Look out for a save conflict
764
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
765 766 767
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
768 769
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
770

J
Johannes Rieken 已提交
771 772
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
773 774
			}));
		}));
E
Erich Gamma 已提交
775 776
	}

777
	private getTypeIfSettings(): string {
778
		if (path.extname(this.resource.fsPath) !== '.json') {
779
			return '';
780
		}
781 782

		// Check for global settings file
S
Sandeep Somavarapu 已提交
783
		if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) {
784 785 786 787 788 789 790 791 792 793 794 795 796 797
			return 'global-settings';
		}

		// Check for keybindings file
		if (isEqual(this.resource, URI.file(this.environmentService.appKeybindingsPath), !isLinux)) {
			return 'keybindings';
		}

		// Check for locale file
		if (isEqual(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'locale.json')), !isLinux)) {
			return 'locale';
		}

		// Check for snippets
798
		if (isEqualOrParent(this.resource, URI.file(path.join(this.environmentService.appSettingsHome, 'snippets')))) {
799
			return 'snippets';
800 801 802
		}

		// Check for workspace settings file
803 804
		const folders = this.contextService.getWorkspace().folders;
		for (let i = 0; i < folders.length; i++) {
805
			if (isEqualOrParent(this.resource, folders[i].toResource('.vscode'))) {
806 807 808 809 810 811 812 813
				const filename = path.basename(this.resource.fsPath);
				if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) {
					return `.vscode/${filename}`;
				}
			}
		}

		return '';
814 815
	}

816
	private getTelemetryData(reason: number): Object {
R
Ramya Achutha Rao 已提交
817 818
		const ext = path.extname(this.resource.fsPath);
		const fileName = path.basename(this.resource.fsPath);
819 820
		const telemetryData = {
			mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
R
Ramya Achutha Rao 已提交
821
			ext,
822 823 824 825
			path: this.hashService.createSHA1(this.resource.fsPath),
			reason
		};

R
Ramya Achutha Rao 已提交
826 827
		if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
			telemetryData['whitelistedjson'] = fileName;
828 829 830 831 832 833 834 835 836 837 838 839 840 841
		}

		/* __GDPR__FRAGMENT__
			"FileTelemetryData" : {
				"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
				"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
				"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
				"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
				"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
			}
		*/
		return telemetryData;
	}

B
Benjamin Pasero 已提交
842 843 844 845 846 847
	private doTouch(versionId: number): TPromise<void> {
		return this.saveSequentializer.setPending(versionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), {
			mtime: this.lastResolvedDiskStat.mtime,
			encoding: this.getEncoding(),
			etag: this.lastResolvedDiskStat.etag
		}).then(stat => {
848 849 850

			// Updated resolved stat with updated stat since touching it might have changed mtime
			this.updateLastResolvedDiskStat(stat);
B
Benjamin Pasero 已提交
851
		}, () => void 0 /* gracefully ignore errors if just touching */));
852 853
	}

E
Erich Gamma 已提交
854
	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
855
		const wasDirty = this.dirty;
856
		const wasInConflictMode = this.inConflictMode;
B
Benjamin Pasero 已提交
857 858
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
859 860 861

		if (!dirty) {
			this.dirty = false;
862
			this.inConflictMode = false;
E
Erich Gamma 已提交
863
			this.inErrorMode = false;
864
			this.updateSavedVersionId();
E
Erich Gamma 已提交
865 866 867 868 869 870 871
		} else {
			this.dirty = true;
		}

		// Return function to revert this call
		return () => {
			this.dirty = wasDirty;
872
			this.inConflictMode = wasInConflictMode;
E
Erich Gamma 已提交
873 874 875 876 877
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

878 879 880 881 882 883 884 885 886 887 888
	private updateSavedVersionId(): void {
		// 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();
		}
	}

889
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStat): void {
E
Erich Gamma 已提交
890 891

		// First resolve - just take
892 893
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
894 895 896
		}

		// Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced.
B
Benjamin Pasero 已提交
897 898
		// This prevents race conditions from loading and saving. If a save comes in late after a revert
		// was called, the mtime could be out of sync.
899 900
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
901 902 903 904 905 906 907 908 909 910 911 912 913 914
		}
	}

	private onSaveError(error: any): void {

		// Prepare handler
		if (!TextFileEditorModel.saveErrorHandler) {
			TextFileEditorModel.setSaveErrorHandler(this.instantiationService.createInstance(DefaultSaveErrorHandler));
		}

		// Handle
		TextFileEditorModel.saveErrorHandler.onSaveError(error, this);
	}

B
Benjamin Pasero 已提交
915
	isDirty(): boolean {
E
Erich Gamma 已提交
916 917 918
		return this.dirty;
	}

B
Benjamin Pasero 已提交
919
	getLastSaveAttemptTime(): number {
B
Benjamin Pasero 已提交
920
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
921 922
	}

B
Benjamin Pasero 已提交
923
	getETag(): string {
924
		return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag : null;
E
Erich Gamma 已提交
925 926
	}

B
Benjamin Pasero 已提交
927
	hasState(state: ModelState): boolean {
928 929 930 931 932 933 934 935 936 937 938 939 940
		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 已提交
941 942 943
		}
	}

B
Benjamin Pasero 已提交
944
	getEncoding(): string {
E
Erich Gamma 已提交
945 946 947
		return this.preferredEncoding || this.contentEncoding;
	}

B
Benjamin Pasero 已提交
948
	setEncoding(encoding: string, mode: EncodingMode): void {
E
Erich Gamma 已提交
949 950 951 952 953 954 955 956 957 958 959 960 961 962
		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();
			}

963
			if (!this.inConflictMode) {
B
Benjamin Pasero 已提交
964
				this.save({ overwriteEncoding: true }).done(null, onUnexpectedError);
E
Erich Gamma 已提交
965 966 967 968 969 970
			}
		}

		// Decode: Load with encoding
		else {
			if (this.isDirty()) {
971
				this.notificationService.info(nls.localize('saveFileFirst', "The file is dirty. Please save it first before reopening it with another encoding."));
E
Erich Gamma 已提交
972 973 974 975 976 977 978

				return;
			}

			this.updatePreferredEncoding(encoding);

			// Load
979 980 981
			this.load({
				forceReadFromDisk: true	// because encoding has changed
			}).done(null, onUnexpectedError);
E
Erich Gamma 已提交
982 983 984
		}
	}

B
Benjamin Pasero 已提交
985
	updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
986 987 988 989 990 991 992
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
993
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
994 995 996 997 998 999 1000 1001
	}

	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) {
1002
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
1003 1004 1005 1006 1007
		}

		return true;
	}

B
Benjamin Pasero 已提交
1008
	isResolved(): boolean {
1009
		return !isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
1010 1011
	}

B
Benjamin Pasero 已提交
1012
	isReadonly(): boolean {
1013
		return this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly;
1014 1015
	}

B
Benjamin Pasero 已提交
1016
	isDisposed(): boolean {
E
Erich Gamma 已提交
1017 1018 1019
		return this.disposed;
	}

B
Benjamin Pasero 已提交
1020
	getResource(): URI {
E
Erich Gamma 已提交
1021 1022 1023
		return this.resource;
	}

B
Benjamin Pasero 已提交
1024
	getStat(): IFileStat {
B
Benjamin Pasero 已提交
1025 1026 1027
		return this.lastResolvedDiskStat;
	}

B
Benjamin Pasero 已提交
1028
	dispose(): void {
E
Erich Gamma 已提交
1029
		this.disposed = true;
1030 1031
		this.inConflictMode = false;
		this.inOrphanMode = false;
E
Erich Gamma 已提交
1032 1033 1034 1035
		this.inErrorMode = false;

		this.createTextEditorModelPromise = null;

1036
		this.cancelPendingAutoSave();
D
Daniel Imms 已提交
1037

E
Erich Gamma 已提交
1038 1039
		super.dispose();
	}
1040 1041
}

1042 1043 1044 1045 1046
interface IPendingSave {
	versionId: number;
	promise: TPromise<void>;
}

1047 1048 1049 1050 1051 1052 1053 1054
interface ISaveOperation {
	promise: TPromise<void>;
	promiseValue: TValueCallback<void>;
	promiseError: ErrorCallback;
	run: () => TPromise<void>;
}

export class SaveSequentializer {
1055
	private _pendingSave: IPendingSave;
1056
	private _nextSave: ISaveOperation;
1057

B
Benjamin Pasero 已提交
1058
	hasPendingSave(versionId?: number): boolean {
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
		if (!this._pendingSave) {
			return false;
		}

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

		return !!this._pendingSave;
	}

B
Benjamin Pasero 已提交
1070
	get pendingSave(): TPromise<void> {
1071 1072 1073
		return this._pendingSave ? this._pendingSave.promise : void 0;
	}

B
Benjamin Pasero 已提交
1074
	setPending(versionId: number, promise: TPromise<void>): TPromise<void> {
1075 1076 1077 1078 1079 1080 1081 1082 1083
		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) {
1084 1085 1086 1087 1088 1089

			// 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();
1090 1091 1092
		}
	}

1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
	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);
		}
	}

B
Benjamin Pasero 已提交
1103
	setNext(run: () => TPromise<void>): TPromise<void> {
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129

		// 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;
1130 1131 1132
	}
}

1133 1134
class DefaultSaveErrorHandler implements ISaveErrorHandler {

M
Matt Bierner 已提交
1135
	constructor(@INotificationService private notificationService: INotificationService) { }
1136

B
Benjamin Pasero 已提交
1137
	onSaveError(error: any, model: TextFileEditorModel): void {
1138
		this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", path.basename(model.getResource().fsPath), toErrorMessage(error, false)));
1139 1140
	}
}