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

6
import { join } from 'vs/base/common/path';
7
import * as nls from 'vs/nls';
M
Matt Bierner 已提交
8
import { Event, Emitter } from 'vs/base/common/event';
J
Johannes Rieken 已提交
9 10
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
11
import { URI } from 'vs/base/common/uri';
12
import { isUndefinedOrNull, withUndefinedAsNull } from 'vs/base/common/types';
13
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
14
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
15
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
B
Benjamin Pasero 已提交
16
import { EncodingMode } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
17
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
18
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
B
Benjamin Pasero 已提交
19
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
20
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
A
Alex Dima 已提交
21
import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService';
22
import { IModelService } from 'vs/editor/common/services/modelService';
23
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
24
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
25
import { ITextBufferFactory } from 'vs/editor/common/model';
26
import { hash } from 'vs/base/common/hash';
27
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
28
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
29
import { isLinux } from 'vs/base/common/platform';
30
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
I
isidor 已提交
31
import { ILogService } from 'vs/platform/log/common/log';
B
Benjamin Pasero 已提交
32
import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources';
B
Benjamin Pasero 已提交
33
import { onUnexpectedError } from 'vs/base/common/errors';
34
import { Schemas } from 'vs/base/common/network';
B
Benjamin Pasero 已提交
35

E
Erich Gamma 已提交
36 37 38
/**
 * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk.
 */
39
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel {
E
Erich Gamma 已提交
40

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

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

49 50
	private static saveParticipant: ISaveParticipant | null;
	static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; }
E
Erich Gamma 已提交
51

B
Benjamin Pasero 已提交
52 53 54 55 56 57
	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 已提交
58
	private resource: URI;
B
Benjamin Pasero 已提交
59

E
Erich Gamma 已提交
60 61
	private contentEncoding: string; 			// encoding as reported from disk
	private preferredEncoding: string;			// encoding as chosen by the user
B
Benjamin Pasero 已提交
62

E
Erich Gamma 已提交
63 64 65
	private versionId: number;
	private bufferSavedVersionId: number;
	private blockModelContentChange: boolean;
B
Benjamin Pasero 已提交
66 67 68 69 70

	private createTextEditorModelPromise: Promise<TextFileEditorModel> | null;

	private lastResolvedDiskStat: IFileStatWithMetadata;

71
	private autoSaveAfterMillies?: number;
72
	private autoSaveAfterMilliesEnabled: boolean;
73
	private autoSaveDisposable?: IDisposable;
B
Benjamin Pasero 已提交
74

75
	private saveSequentializer: SaveSequentializer;
B
Benjamin Pasero 已提交
76
	private lastSaveAttemptTime: number;
B
Benjamin Pasero 已提交
77 78 79 80 81

	private contentChangeEventScheduler: RunOnceScheduler;
	private orphanedChangeEventScheduler: RunOnceScheduler;

	private dirty: boolean;
82 83 84 85
	private inConflictMode: boolean;
	private inOrphanMode: boolean;
	private inErrorMode: boolean;

B
Benjamin Pasero 已提交
86 87
	private disposed: boolean;

E
Erich Gamma 已提交
88 89 90
	constructor(
		resource: URI,
		preferredEncoding: string,
91
		@INotificationService private readonly notificationService: INotificationService,
E
Erich Gamma 已提交
92 93
		@IModeService modeService: IModeService,
		@IModelService modelService: IModelService,
94 95 96 97 98 99 100 101
		@IFileService private readonly fileService: IFileService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@ITextFileService private readonly textFileService: ITextFileService,
		@IBackupFileService private readonly backupFileService: IBackupFileService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@ILogService private readonly logService: ILogService
E
Erich Gamma 已提交
102 103
	) {
		super(modelService, modeService);
B
Benjamin Pasero 已提交
104

E
Erich Gamma 已提交
105 106
		this.resource = resource;
		this.preferredEncoding = preferredEncoding;
107
		this.inOrphanMode = false;
E
Erich Gamma 已提交
108 109
		this.dirty = false;
		this.versionId = 0;
B
Benjamin Pasero 已提交
110
		this.lastSaveAttemptTime = 0;
111
		this.saveSequentializer = new SaveSequentializer();
112

B
Benjamin Pasero 已提交
113 114
		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));
115

116
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
117

118
		this.registerListeners();
E
Erich Gamma 已提交
119 120
	}

121
	private registerListeners(): void {
B
Benjamin Pasero 已提交
122 123 124 125
		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 已提交
126
	}
127

B
Benjamin Pasero 已提交
128 129
	private onStateChange(e: StateChange): void {
		if (e === StateChange.REVERTED) {
130

B
Benjamin Pasero 已提交
131 132 133 134 135 136
			// 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);
		}
137 138
	}

139
	private onFileChanges(e: FileChangesEvent): void {
140
		let fileEventImpactsModel = false;
141
		let newInOrphanModeGuess: boolean | undefined;
142 143 144 145 146 147 148 149 150

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

152 153 154 155 156 157 158 159
		// Otherwise we check if the model file was deleted
		else {
			const modelFileDeleted = e.contains(this.resource, FileChangeType.DELETED);
			if (modelFileDeleted) {
				newInOrphanModeGuess = true;
				fileEventImpactsModel = true;
			}
		}
160

161
		if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) {
J
Johannes Rieken 已提交
162
			let checkOrphanedPromise: Promise<boolean>;
163 164 165 166 167
			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.
168
				checkOrphanedPromise = timeout(100).then(() => {
169 170
					if (this.disposed) {
						return true;
171
					}
172

B
Benjamin Pasero 已提交
173
					return this.fileService.exists(this.resource).then(exists => !exists);
174
				});
175
			} else {
176
				checkOrphanedPromise = Promise.resolve(false);
177
			}
178

179
			checkOrphanedPromise.then(newInOrphanModeValidated => {
180 181 182 183
				if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
					this.setOrphaned(newInOrphanModeValidated);
				}
			});
184 185 186 187 188 189 190 191 192 193
		}
	}

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

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

B
Benjamin Pasero 已提交
197
		this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled;
R
Rob Lourens 已提交
198
		this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : undefined;
199 200
	}

B
Benjamin Pasero 已提交
201
	private onFilesAssociationChange(): void {
B
Benjamin Pasero 已提交
202 203 204 205
		if (!this.textEditorModel) {
			return;
		}

A
Alex Dima 已提交
206
		const firstLineText = this.getFirstLineText(this.textEditorModel);
R
Rob Lourens 已提交
207
		const languageSelection = this.getOrCreateMode(this.modeService, undefined, firstLineText);
B
Benjamin Pasero 已提交
208

A
Alex Dima 已提交
209
		this.modelService.setMode(this.textEditorModel, languageSelection);
B
Benjamin Pasero 已提交
210 211
	}

B
Benjamin Pasero 已提交
212
	getVersionId(): number {
213 214 215
		return this.versionId;
	}

216
	async revert(soft?: boolean): Promise<void> {
E
Erich Gamma 已提交
217
		if (!this.isResolved()) {
R
Rob Lourens 已提交
218
			return Promise.resolve(undefined);
E
Erich Gamma 已提交
219 220
		}

221
		// Cancel any running auto-save
222
		this.cancelPendingAutoSave();
E
Erich Gamma 已提交
223 224

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

227
		let loadPromise: Promise<unknown>;
228
		if (soft) {
B
Benjamin Pasero 已提交
229
			loadPromise = Promise.resolve();
230
		} else {
231
			loadPromise = this.load({ forceReadFromDisk: true });
232 233
		}

234 235
		try {
			await loadPromise;
E
Erich Gamma 已提交
236 237

			// Emit file change event
238
			this._onDidStateChange.fire(StateChange.REVERTED);
239
		} catch (error) {
E
Erich Gamma 已提交
240

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

B
Benjamin Pasero 已提交
244
			return Promise.reject(error);
245
		}
E
Erich Gamma 已提交
246 247
	}

248
	load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
I
isidor 已提交
249
		this.logService.trace('load() - enter', this.resource);
E
Erich Gamma 已提交
250

B
Benjamin Pasero 已提交
251 252 253
		// 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.
254
		if (this.dirty || this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
255
			this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
E
Erich Gamma 已提交
256

B
Benjamin Pasero 已提交
257
			return Promise.resolve(this);
E
Erich Gamma 已提交
258 259
		}

260 261
		// Only for new models we support to load from backup
		if (!this.textEditorModel && !this.createTextEditorModelPromise) {
B
Benjamin Pasero 已提交
262
			return this.loadFromBackup(options);
263 264 265
		}

		// Otherwise load from file resource
266
		return this.loadFromFile(options);
267 268
	}

269 270 271 272 273 274 275 276 277 278
	private async loadFromBackup(options?: ILoadOptions): Promise<TextFileEditorModel> {
		const backup = await this.backupFileService.loadBackupResource(this.resource);

		// Make sure meanwhile someone else did not suceed or start loading
		if (this.createTextEditorModelPromise || this.textEditorModel) {
			return this.createTextEditorModelPromise || this;
		}

		// If we have a backup, continue loading with it
		if (!!backup) {
279
			const content: ITextFileStreamContent = {
280 281 282 283
				resource: this.resource,
				name: basename(this.resource),
				mtime: Date.now(),
				size: 0,
B
Benjamin Pasero 已提交
284 285
				etag: ETAG_DISABLED, // always allow to save content restored from a backup (see https://github.com/Microsoft/vscode/issues/72343)
				value: createTextBufferFactory(''), // will be filled later from backup
286
				encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
287 288
				isReadonly: false
			};
289

290 291
			return this.loadWithContent(content, options, backup);
		}
292

293 294
		// Otherwise load from file
		return this.loadFromFile(options);
295 296
	}

297
	private async loadFromFile(options?: ILoadOptions): Promise<TextFileEditorModel> {
298 299
		const forceReadFromDisk = options && options.forceReadFromDisk;
		const allowBinary = this.isResolved() /* always allow if we resolved previously */ || (options && options.allowBinary);
300

E
Erich Gamma 已提交
301
		// Decide on etag
302
		let etag: string | undefined;
303
		if (forceReadFromDisk) {
R
Rob Lourens 已提交
304
			etag = undefined; // reset ETag if we enforce to read from disk
305 306
		} else if (this.lastResolvedDiskStat) {
			etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
307 308
		}

B
Benjamin Pasero 已提交
309 310 311 312 313 314 315 316
		// 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 已提交
317
		// Resolve Content
318
		try {
319
			const content = await this.textFileService.readStream(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding });
320

321 322
			// Clear orphaned state when loading was successful
			this.setOrphaned(false);
323

324 325 326 327
			// Guard against the model having changed in the meantime
			if (currentVersionId === this.versionId) {
				return this.loadWithContent(content, options);
			}
E
Erich Gamma 已提交
328

329 330 331
			return this;
		} catch (error) {
			const result = error.fileOperationResult;
E
Erich Gamma 已提交
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) {
E
Erich Gamma 已提交
338

339 340 341
				// Guard against the model having changed in the meantime
				if (currentVersionId === this.versionId) {
					this.setDirty(false); // Ensure we are not tracking a stale state
B
Benjamin Pasero 已提交
342
				}
343

344 345
				return this;
			}
346

347 348 349 350 351 352 353 354 355 356
			// 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 this;
			}

			// Otherwise bubble up the error
			throw error;
		}
357
	}
E
Erich Gamma 已提交
358

359
	private async loadWithContent(content: ITextFileStreamContent, options?: ILoadOptions, backup?: URI): Promise<TextFileEditorModel> {
360
		const model = await this.doLoadWithContent(content, backup);
361

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
		// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
		const settingsType = this.getTypeIfSettings();
		if (settingsType) {
			/* __GDPR__
				"settingsRead" : {
					"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
			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
		} else {
			/* __GDPR__
				"fileGet" : {
					"${include}": [
						"${FileTelemetryData}"
					]
				}
			*/
			this.telemetryService.publicLog('fileGet', this.getTelemetryData(options && options.reason ? options.reason : LoadReason.OTHER));
		}
381

382
		return model;
383 384
	}

385
	private doLoadWithContent(content: ITextFileStreamContent, backup?: URI): Promise<TextFileEditorModel> {
I
isidor 已提交
386
		this.logService.trace('load() - resolved content', this.resource);
387 388

		// Update our resolved disk stat model
B
Benjamin Pasero 已提交
389
		this.updateLastResolvedDiskStat({
390 391 392
			resource: this.resource,
			name: content.name,
			mtime: content.mtime,
393
			size: content.size,
394 395
			etag: content.etag,
			isDirectory: false,
396
			isSymbolicLink: false,
I
isidor 已提交
397
			isReadonly: content.isReadonly
398
		});
399 400 401 402 403 404 405 406 407 408 409

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

411 412
		// Update Existing Model
		if (this.textEditorModel) {
B
Benjamin Pasero 已提交
413 414 415
			this.doUpdateTextModel(content.value);

			return Promise.resolve(this);
416
		}
E
Erich Gamma 已提交
417

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

422 423
			return this.createTextEditorModelPromise;
		}
E
Erich Gamma 已提交
424

425
		// Create New Model
426
		return this.doCreateTextModel(content.resource, content.value, backup);
427
	}
E
Erich Gamma 已提交
428

B
Benjamin Pasero 已提交
429
	private doUpdateTextModel(value: ITextBufferFactory): void {
I
isidor 已提交
430
		this.logService.trace('load() - updated text editor model', this.resource);
431

432 433
		// Ensure we are not tracking a stale state
		this.setDirty(false);
434

435
		// Update model value in a block that ignores model content change events
436 437 438 439 440 441 442
		this.blockModelContentChange = true;
		try {
			this.updateTextEditorModel(value);
		} finally {
			this.blockModelContentChange = false;
		}

443 444
		// Ensure we track the latest saved version ID given that the contents changed
		this.updateSavedVersionId();
445 446
	}

M
Matt Bierner 已提交
447
	private doCreateTextModel(resource: URI, value: ITextBufferFactory, backup: URI | undefined): Promise<TextFileEditorModel> {
I
isidor 已提交
448
		this.logService.trace('load() - created text editor model', this.resource);
449

450
		this.createTextEditorModelPromise = this.doLoadBackup(backup).then(backupContent => {
B
Benjamin Pasero 已提交
451
			this.createTextEditorModelPromise = null;
452

B
Benjamin Pasero 已提交
453 454
			// Create model
			const hasBackupContent = !!backupContent;
M
Matt Bierner 已提交
455
			this.createTextEditorModel(backupContent ? backupContent : value, resource);
B
Benjamin Pasero 已提交
456 457 458 459 460 461 462 463

			// 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);
464
				}
B
Benjamin Pasero 已提交
465
			}
466

B
Benjamin Pasero 已提交
467 468 469 470
			// Ensure we are not tracking a stale state
			else {
				this.setDirty(false);
			}
471

B
Benjamin Pasero 已提交
472 473
			// Model Listeners
			this.installModelListeners();
474

B
Benjamin Pasero 已提交
475 476 477
			return this;
		}, error => {
			this.createTextEditorModelPromise = null;
478

B
Benjamin Pasero 已提交
479
			return Promise.reject<TextFileEditorModel>(error);
480
		});
481

482 483 484
		return this.createTextEditorModelPromise;
	}

B
Benjamin Pasero 已提交
485 486
	private installModelListeners(): void {

487 488 489
		// 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 已提交
490 491

		// Content Change
M
Matt Bierner 已提交
492 493 494
		if (this.textEditorModel) {
			this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
		}
495 496
	}

497
	private async doLoadBackup(backup: URI | undefined): Promise<ITextBufferFactory | null> {
498
		if (!backup) {
499
			return null;
500
		}
501

502 503 504 505 506
		try {
			return withUndefinedAsNull(await this.backupFileService.resolveBackupContent(backup));
		} catch (error) {
			return null; // ignore errors
		}
E
Erich Gamma 已提交
507 508
	}

509
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string | undefined, firstLineText?: string): ILanguageSelection {
A
Alex Dima 已提交
510
		return modeService.createByFilepathOrFirstLine(this.resource.fsPath, firstLineText);
E
Erich Gamma 已提交
511 512
	}

513
	private onModelContentChanged(): void {
I
isidor 已提交
514
		this.logService.trace(`onModelContentChanged() - enter`, this.resource);
E
Erich Gamma 已提交
515 516 517

		// In any case increment the version id because it tracks the textual content state of the model at all times
		this.versionId++;
I
isidor 已提交
518
		this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource);
E
Erich Gamma 已提交
519 520 521 522 523 524 525 526 527 528

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

			// Clear flags
533
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
534 535 536
			this.setDirty(false);

			// Emit event
537
			if (wasDirty) {
538
				this._onDidStateChange.fire(StateChange.REVERTED);
539
			}
E
Erich Gamma 已提交
540 541 542 543

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
547
		this.makeDirty();
E
Erich Gamma 已提交
548 549

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
550
		if (this.autoSaveAfterMilliesEnabled) {
551
			if (!this.inConflictMode) {
E
Erich Gamma 已提交
552 553
				this.doAutoSave(this.versionId);
			} else {
I
isidor 已提交
554
				this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource);
E
Erich Gamma 已提交
555 556
			}
		}
557

558 559
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
560 561
	}

B
Benjamin Pasero 已提交
562
	private makeDirty(): void {
E
Erich Gamma 已提交
563 564

		// Track dirty state and version id
B
Benjamin Pasero 已提交
565
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
566 567 568 569
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
570
			this._onDidStateChange.fire(StateChange.DIRTY);
E
Erich Gamma 已提交
571 572 573
		}
	}

574
	private doAutoSave(versionId: number): void {
I
isidor 已提交
575
		this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
E
Erich Gamma 已提交
576 577

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

580 581
		// Create new save timer and store it for disposal as needed
		const handle = setTimeout(() => {
E
Erich Gamma 已提交
582 583 584

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

589
		this.autoSaveDisposable = toDisposable(() => clearTimeout(handle));
E
Erich Gamma 已提交
590 591
	}

592 593 594
	private cancelPendingAutoSave(): void {
		if (this.autoSaveDisposable) {
			this.autoSaveDisposable.dispose();
R
Rob Lourens 已提交
595
			this.autoSaveDisposable = undefined;
E
Erich Gamma 已提交
596 597 598
		}
	}

J
Johannes Rieken 已提交
599
	save(options: ISaveOptions = Object.create(null)): Promise<void> {
E
Erich Gamma 已提交
600
		if (!this.isResolved()) {
R
Rob Lourens 已提交
601
			return Promise.resolve(undefined);
E
Erich Gamma 已提交
602 603
		}

I
isidor 已提交
604
		this.logService.trace('save() - enter', this.resource);
E
Erich Gamma 已提交
605 606

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

609
		return this.doSave(this.versionId, options);
E
Erich Gamma 已提交
610 611
	}

J
Johannes Rieken 已提交
612
	private doSave(versionId: number, options: ISaveOptions): Promise<void> {
613
		if (isUndefinedOrNull(options.reason)) {
614 615 616
			options.reason = SaveReason.EXPLICIT;
		}

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

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

M
Matt Bierner 已提交
627
			return this.saveSequentializer.pendingSave || Promise.resolve(undefined);
E
Erich Gamma 已提交
628 629
		}

630
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
631 632 633 634 635 636
		//
		// 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.
		//
637
		if ((!options.force && !this.dirty) || versionId !== this.versionId) {
I
isidor 已提交
638
			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 已提交
639

R
Rob Lourens 已提交
640
			return Promise.resolve(undefined);
E
Erich Gamma 已提交
641 642
		}

643
		// Return if currently saving by storing this save request as the next save that should happen.
644
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
645
		//
646
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
647
		//             kicks in.
648 649
		// 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 已提交
650
		//
651
		if (this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
652
			this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource);
E
Erich Gamma 已提交
653

654
			// Register this as the next upcoming save and return
655
			return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options));
E
Erich Gamma 已提交
656 657 658 659
		}

		// 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
M
Matt Bierner 已提交
660 661
		if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel) {
			this.textEditorModel.pushStackElement();
E
Erich Gamma 已提交
662 663
		}

B
Benjamin Pasero 已提交
664
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
665 666
		// 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
667
		// Save participants can also be skipped through API.
J
Johannes Rieken 已提交
668
		let saveParticipantPromise: Promise<number> = Promise.resolve(versionId);
669
		if (TextFileEditorModel.saveParticipant && !options.skipSaveParticipants) {
B
💄  
Benjamin Pasero 已提交
670
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
671
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
672

673
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
674 675
			};

B
Benjamin Pasero 已提交
676
			this.blockModelContentChange = true;
677
			saveParticipantPromise = TextFileEditorModel.saveParticipant.participate(this as IResolvedTextFileEditorModel, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
678 679
		}

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

683 684 685 686 687
			// 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).
688
			// To fix this issue, we will not store the contents to disk when we got disposed.
689
			if (this.disposed) {
R
Rob Lourens 已提交
690
				return undefined;
691 692
			}

B
Benjamin Pasero 已提交
693 694
			// 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.
695 696 697 698 699 700
			// 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 已提交
701
				return this.doTouch(newVersionId);
702 703
			}

704
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
705 706 707 708 709 710 711 712 713
			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
714
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
B
Benjamin Pasero 已提交
715
			this.logService.trace(`doSave(${versionId}) - before write()`, this.resource);
M
Matt Bierner 已提交
716 717 718 719
			const snapshot = this.createSnapshot();
			if (!snapshot) {
				throw new Error('Invalid snapshot');
			}
B
Benjamin Pasero 已提交
720
			return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
721 722
				overwriteReadonly: options.overwriteReadonly,
				overwriteEncoding: options.overwriteEncoding,
723
				mtime: this.lastResolvedDiskStat.mtime,
J
Johannes Rieken 已提交
724
				encoding: this.getEncoding(),
725 726
				etag: this.lastResolvedDiskStat.etag,
				writeElevated: options.writeElevated
727
			}).then(stat => {
B
Benjamin Pasero 已提交
728
				this.logService.trace(`doSave(${versionId}) - after write()`, this.resource);
J
Johannes Rieken 已提交
729 730 731

				// Update dirty state unless model has changed meanwhile
				if (versionId === this.versionId) {
I
isidor 已提交
732
					this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource);
J
Johannes Rieken 已提交
733 734
					this.setDirty(false);
				} else {
I
isidor 已提交
735
					this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource);
J
Johannes Rieken 已提交
736
				}
E
Erich Gamma 已提交
737

738
				// Updated resolved stat with updated stat
739
				this.updateLastResolvedDiskStat(stat);
E
Erich Gamma 已提交
740

741 742 743
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

J
Johannes Rieken 已提交
744 745
				// Emit File Saved Event
				this._onDidStateChange.fire(StateChange.SAVED);
746 747 748 749 750 751 752 753 754 755 756

				// Telemetry
				const settingsType = this.getTypeIfSettings();
				if (settingsType) {
					/* __GDPR__
						"settingsWritten" : {
							"settingsType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
						}
					*/
					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
				} else {
757
					/* __GDPR__
758 759 760 761 762 763
							"filePUT" : {
								"${include}": [
									"${FileTelemetryData}"
								]
							}
						*/
764
					this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
765
				}
766
			}, error => {
I
isidor 已提交
767
				this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
E
Erich Gamma 已提交
768

769
				// Flag as error state in the model
J
Johannes Rieken 已提交
770
				this.inErrorMode = true;
E
Erich Gamma 已提交
771

772
				// Look out for a save conflict
773
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
774 775 776
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
777 778
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
779

J
Johannes Rieken 已提交
780 781
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
782 783
			}));
		}));
E
Erich Gamma 已提交
784 785
	}

786
	private getTypeIfSettings(): string {
B
Benjamin Pasero 已提交
787
		if (extname(this.resource) !== '.json') {
788
			return '';
789
		}
790 791

		// Check for global settings file
S
Sandeep Somavarapu 已提交
792
		if (isEqual(this.resource, URI.file(this.environmentService.appSettingsPath), !isLinux)) {
793 794 795 796 797 798 799 800 801
			return 'global-settings';
		}

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

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

		// Check for snippets
807
		if (isEqualOrParent(this.resource, URI.file(join(this.environmentService.appSettingsHome, 'snippets')))) {
808
			return 'snippets';
809 810 811
		}

		// Check for workspace settings file
812
		const folders = this.contextService.getWorkspace().folders;
813 814
		for (const folder of folders) {
			if (isEqualOrParent(this.resource, folder.toResource('.vscode'))) {
B
Benjamin Pasero 已提交
815
				const filename = basename(this.resource);
816 817 818 819 820 821 822
				if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) {
					return `.vscode/${filename}`;
				}
			}
		}

		return '';
823 824
	}

825 826 827
	private getTelemetryData(reason: number | undefined): object {
		const ext = extname(this.resource);
		const fileName = basename(this.resource);
828
		const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path;
829
		const telemetryData = {
830
			mimeType: guessMimeTypes(path).join(', '),
831
			ext,
832
			path: hash(path),
833 834 835 836 837 838 839 840 841 842 843 844 845 846
			reason
		};

		if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
			telemetryData['whitelistedjson'] = fileName;
		}

		/* __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" }
847
			}
848 849
		*/
		return telemetryData;
850 851
	}

J
Johannes Rieken 已提交
852
	private doTouch(versionId: number): Promise<void> {
M
Matt Bierner 已提交
853 854 855 856
		const snapshot = this.createSnapshot();
		if (!snapshot) {
			throw new Error('invalid snapshot');
		}
857

B
Benjamin Pasero 已提交
858
		return this.saveSequentializer.setPending(versionId, this.textFileService.write(this.lastResolvedDiskStat.resource, snapshot, {
B
Benjamin Pasero 已提交
859 860 861 862
			mtime: this.lastResolvedDiskStat.mtime,
			encoding: this.getEncoding(),
			etag: this.lastResolvedDiskStat.etag
		}).then(stat => {
863 864 865

			// Updated resolved stat with updated stat since touching it might have changed mtime
			this.updateLastResolvedDiskStat(stat);
B
Benjamin Pasero 已提交
866
		}, error => onUnexpectedError(error) /* just log any error but do not notify the user since the file was not dirty */));
867 868
	}

E
Erich Gamma 已提交
869
	private setDirty(dirty: boolean): () => void {
B
Benjamin Pasero 已提交
870
		const wasDirty = this.dirty;
871
		const wasInConflictMode = this.inConflictMode;
B
Benjamin Pasero 已提交
872 873
		const wasInErrorMode = this.inErrorMode;
		const oldBufferSavedVersionId = this.bufferSavedVersionId;
E
Erich Gamma 已提交
874 875 876

		if (!dirty) {
			this.dirty = false;
877
			this.inConflictMode = false;
E
Erich Gamma 已提交
878
			this.inErrorMode = false;
879
			this.updateSavedVersionId();
E
Erich Gamma 已提交
880 881 882 883 884 885 886
		} else {
			this.dirty = true;
		}

		// Return function to revert this call
		return () => {
			this.dirty = wasDirty;
887
			this.inConflictMode = wasInConflictMode;
E
Erich Gamma 已提交
888 889 890 891 892
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

893 894 895 896 897 898 899 900 901 902 903
	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();
		}
	}

904
	private updateLastResolvedDiskStat(newVersionOnDiskStat: IFileStatWithMetadata): void {
E
Erich Gamma 已提交
905 906

		// First resolve - just take
907 908
		if (!this.lastResolvedDiskStat) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
909 910 911
		}

		// Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced.
B
Benjamin Pasero 已提交
912 913
		// 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.
914 915
		else if (this.lastResolvedDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.lastResolvedDiskStat = newVersionOnDiskStat;
E
Erich Gamma 已提交
916 917 918
		}
	}

919
	private onSaveError(error: Error): void {
E
Erich Gamma 已提交
920 921 922 923 924 925 926 927 928 929

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

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

B
Benjamin Pasero 已提交
930
	isDirty(): boolean {
E
Erich Gamma 已提交
931 932 933
		return this.dirty;
	}

B
Benjamin Pasero 已提交
934
	getLastSaveAttemptTime(): number {
B
Benjamin Pasero 已提交
935
		return this.lastSaveAttemptTime;
E
Erich Gamma 已提交
936 937
	}

938
	getETag(): string | null {
M
Matt Bierner 已提交
939
		return this.lastResolvedDiskStat ? this.lastResolvedDiskStat.etag || null : null;
E
Erich Gamma 已提交
940 941
	}

B
Benjamin Pasero 已提交
942
	hasState(state: ModelState): boolean {
943 944 945 946 947 948 949 950 951 952 953 954 955
		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 已提交
956 957 958
		}
	}

B
Benjamin Pasero 已提交
959
	getEncoding(): string {
E
Erich Gamma 已提交
960 961 962
		return this.preferredEncoding || this.contentEncoding;
	}

B
Benjamin Pasero 已提交
963
	setEncoding(encoding: string, mode: EncodingMode): void {
E
Erich Gamma 已提交
964 965 966 967 968 969 970 971 972 973 974 975 976 977
		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();
			}

978
			if (!this.inConflictMode) {
979
				this.save({ overwriteEncoding: true });
E
Erich Gamma 已提交
980 981 982 983 984 985
			}
		}

		// Decode: Load with encoding
		else {
			if (this.isDirty()) {
986
				this.notificationService.info(nls.localize('saveFileFirst', "The file is dirty. Please save it first before reopening it with another encoding."));
E
Erich Gamma 已提交
987 988 989 990 991 992 993

				return;
			}

			this.updatePreferredEncoding(encoding);

			// Load
994 995
			this.load({
				forceReadFromDisk: true	// because encoding has changed
996
			});
E
Erich Gamma 已提交
997 998 999
		}
	}

B
Benjamin Pasero 已提交
1000
	updatePreferredEncoding(encoding: string): void {
E
Erich Gamma 已提交
1001 1002 1003 1004 1005 1006 1007
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
1008
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
1009 1010 1011 1012 1013 1014 1015 1016
	}

	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) {
1017
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
1018 1019 1020 1021 1022
		}

		return true;
	}

B
Benjamin Pasero 已提交
1023
	isResolved(): boolean {
1024
		return !isUndefinedOrNull(this.lastResolvedDiskStat);
E
Erich Gamma 已提交
1025 1026
	}

B
Benjamin Pasero 已提交
1027
	isReadonly(): boolean {
1028
		return !!(this.lastResolvedDiskStat && this.lastResolvedDiskStat.isReadonly);
1029 1030
	}

B
Benjamin Pasero 已提交
1031
	isDisposed(): boolean {
E
Erich Gamma 已提交
1032 1033 1034
		return this.disposed;
	}

B
Benjamin Pasero 已提交
1035
	getResource(): URI {
E
Erich Gamma 已提交
1036 1037 1038
		return this.resource;
	}

1039
	getStat(): IFileStatWithMetadata {
B
Benjamin Pasero 已提交
1040 1041 1042
		return this.lastResolvedDiskStat;
	}

B
Benjamin Pasero 已提交
1043
	dispose(): void {
E
Erich Gamma 已提交
1044
		this.disposed = true;
1045 1046
		this.inConflictMode = false;
		this.inOrphanMode = false;
E
Erich Gamma 已提交
1047 1048 1049 1050
		this.inErrorMode = false;

		this.createTextEditorModelPromise = null;

1051
		this.cancelPendingAutoSave();
D
Daniel Imms 已提交
1052

E
Erich Gamma 已提交
1053 1054
		super.dispose();
	}
1055 1056
}

1057 1058
interface IPendingSave {
	versionId: number;
J
Johannes Rieken 已提交
1059
	promise: Promise<void>;
1060 1061
}

1062
interface ISaveOperation {
J
Johannes Rieken 已提交
1063
	promise: Promise<void>;
B
Benjamin Pasero 已提交
1064 1065
	promiseResolve: () => void;
	promiseReject: (error: Error) => void;
J
Johannes Rieken 已提交
1066
	run: () => Promise<void>;
1067 1068 1069
}

export class SaveSequentializer {
1070 1071
	private _pendingSave?: IPendingSave;
	private _nextSave?: ISaveOperation;
1072

B
Benjamin Pasero 已提交
1073
	hasPendingSave(versionId?: number): boolean {
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084
		if (!this._pendingSave) {
			return false;
		}

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

		return !!this._pendingSave;
	}

1085
	get pendingSave(): Promise<void> | undefined {
R
Rob Lourens 已提交
1086
		return this._pendingSave ? this._pendingSave.promise : undefined;
1087 1088
	}

J
Johannes Rieken 已提交
1089
	setPending(versionId: number, promise: Promise<void>): Promise<void> {
1090 1091
		this._pendingSave = { versionId, promise };

1092
		promise.then(() => this.donePending(versionId), () => this.donePending(versionId));
1093 1094 1095 1096 1097 1098

		return promise;
	}

	private donePending(versionId: number): void {
		if (this._pendingSave && versionId === this._pendingSave.versionId) {
1099 1100

			// only set pending to done if the promise finished that is associated with that versionId
R
Rob Lourens 已提交
1101
			this._pendingSave = undefined;
1102 1103 1104

			// schedule the next save now that we are free if we have any
			this.triggerNextSave();
1105 1106 1107
		}
	}

1108 1109 1110
	private triggerNextSave(): void {
		if (this._nextSave) {
			const saveOperation = this._nextSave;
R
Rob Lourens 已提交
1111
			this._nextSave = undefined;
1112 1113

			// Run next save and complete on the associated promise
B
Benjamin Pasero 已提交
1114
			saveOperation.run().then(saveOperation.promiseResolve, saveOperation.promiseReject);
1115 1116 1117
		}
	}

J
Johannes Rieken 已提交
1118
	setNext(run: () => Promise<void>): Promise<void> {
1119 1120 1121 1122 1123

		// 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) {
M
Matt Bierner 已提交
1124 1125
			let promiseResolve: () => void;
			let promiseReject: (error: Error) => void;
B
Benjamin Pasero 已提交
1126 1127 1128
			const promise = new Promise<void>((resolve, reject) => {
				promiseResolve = resolve;
				promiseReject = reject;
1129 1130 1131 1132 1133
			});

			this._nextSave = {
				run,
				promise,
M
Matt Bierner 已提交
1134 1135
				promiseResolve: promiseResolve!,
				promiseReject: promiseReject!
1136 1137 1138 1139 1140 1141 1142 1143
			};
		}

		// we have a previous next save, just overwrite it
		else {
			this._nextSave.run = run;
		}

M
Matt Bierner 已提交
1144
		return this._nextSave.promise;
1145 1146 1147
	}
}

1148 1149
class DefaultSaveErrorHandler implements ISaveErrorHandler {

1150
	constructor(@INotificationService private readonly notificationService: INotificationService) { }
1151

1152
	onSaveError(error: Error, model: TextFileEditorModel): void {
B
Benjamin Pasero 已提交
1153
		this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.getResource()), toErrorMessage(error, false)));
1154 1155
	}
}