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 * as nls from 'vs/nls';
7
import { Emitter } from 'vs/base/common/event';
J
Johannes Rieken 已提交
8 9
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
10
import { URI } from 'vs/base/common/uri';
B
Benjamin Pasero 已提交
11
import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types';
12
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
13
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
14
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
B
Benjamin Pasero 已提交
15
import { EncodingMode } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
16
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
17
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
B
Benjamin Pasero 已提交
18
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
19
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
20
import { IModeService } from 'vs/editor/common/services/modeService';
21
import { IModelService } from 'vs/editor/common/services/modelService';
22
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
23
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
24
import { ITextBufferFactory } from 'vs/editor/common/model';
25
import { hash } from 'vs/base/common/hash';
26
import { INotificationService } from 'vs/platform/notification/common/notification';
27
import { toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
I
isidor 已提交
28
import { ILogService } from 'vs/platform/log/common/log';
S
Sandeep Somavarapu 已提交
29
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
B
Benjamin Pasero 已提交
30
import { onUnexpectedError } from 'vs/base/common/errors';
31
import { Schemas } from 'vs/base/common/network';
32
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
B
Benjamin Pasero 已提交
33

34
export interface IBackupMetaData {
B
Benjamin Pasero 已提交
35
	mtime: number;
36
	ctime: number;
B
Benjamin Pasero 已提交
37 38 39
	size: number;
	etag: string;
	orphaned: boolean;
40 41
}

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
type FileTelemetryDataFragment = {
	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' };
};

type TelemetryData = {
	mimeType: string;
	ext: string;
	path: number;
	reason?: number;
	whitelistedjson?: string;
};

E
Erich Gamma 已提交
58 59 60
/**
 * 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.
 */
61
export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel, IWorkingCopy {
E
Erich Gamma 已提交
62

B
Benjamin Pasero 已提交
63 64
	static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY;
	static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100;
K
kieferrm 已提交
65
	static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json'];
66
	static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json'];
67

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

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

74 75
	private readonly _onDidContentChange = this._register(new Emitter<StateChange>());
	readonly onDidContentChange = this._onDidContentChange.event;
B
Benjamin Pasero 已提交
76

77 78 79 80 81 82 83
	private readonly _onDidStateChange = this._register(new Emitter<StateChange>());
	readonly onDidStateChange = this._onDidStateChange.event;

	private readonly _onDidChangeDirty = this._register(new Emitter<void>());
	readonly onDidChangeDirty = this._onDidChangeDirty.event;

	readonly capabilities = WorkingCopyCapabilities.AutoSave;
B
Benjamin Pasero 已提交
84

B
Benjamin Pasero 已提交
85
	private contentEncoding: string | undefined; // encoding as reported from disk
B
Benjamin Pasero 已提交
86

B
Benjamin Pasero 已提交
87 88 89
	private versionId = 0;
	private bufferSavedVersionId: number | undefined;
	private blockModelContentChange = false;
90

B
Benjamin Pasero 已提交
91
	private lastResolvedFileStat: IFileStatWithMetadata | undefined;
B
Benjamin Pasero 已提交
92

B
Benjamin Pasero 已提交
93 94
	private autoSaveAfterMillies: number | undefined;
	private autoSaveAfterMilliesEnabled: boolean | undefined;
95
	private readonly autoSaveDisposable = this._register(new MutableDisposable());
B
Benjamin Pasero 已提交
96

B
Benjamin Pasero 已提交
97 98
	private readonly saveSequentializer = new SaveSequentializer();
	private lastSaveAttemptTime = 0;
B
Benjamin Pasero 已提交
99

B
Benjamin Pasero 已提交
100 101
	private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY));
	private readonly orphanedChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY));
102

B
Benjamin Pasero 已提交
103 104 105 106 107
	private dirty = false;
	private inConflictMode = false;
	private inOrphanMode = false;
	private inErrorMode = false;
	private disposed = false;
B
Benjamin Pasero 已提交
108

E
Erich Gamma 已提交
109
	constructor(
110
		public readonly resource: URI,
B
Benjamin Pasero 已提交
111 112
		private preferredEncoding: string | undefined,	// encoding as chosen by the user
		private preferredMode: string | undefined,		// mode as chosen by the user
113
		@INotificationService private readonly notificationService: INotificationService,
E
Erich Gamma 已提交
114 115
		@IModeService modeService: IModeService,
		@IModelService modelService: IModelService,
116 117 118 119 120 121 122
		@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,
123 124
		@ILogService private readonly logService: ILogService,
		@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
E
Erich Gamma 已提交
125 126
	) {
		super(modelService, modeService);
B
Benjamin Pasero 已提交
127

128
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
B
Benjamin Pasero 已提交
129

130 131 132
		// Make known to working copy service
		this._register(this.workingCopyService.registerWorkingCopy(this));

133
		this.registerListeners();
E
Erich Gamma 已提交
134 135
	}

136
	private registerListeners(): void {
B
Benjamin Pasero 已提交
137 138 139 140
		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 已提交
141
	}
142

B
Benjamin Pasero 已提交
143 144
	private onStateChange(e: StateChange): void {
		if (e === StateChange.REVERTED) {
145

B
Benjamin Pasero 已提交
146 147 148 149 150 151
			// 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);
		}
152 153
	}

154
	private async onFileChanges(e: FileChangesEvent): Promise<void> {
155
		let fileEventImpactsModel = false;
156
		let newInOrphanModeGuess: boolean | undefined;
157 158 159 160 161 162 163 164 165

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

167 168 169 170 171 172 173 174
		// Otherwise we check if the model file was deleted
		else {
			const modelFileDeleted = e.contains(this.resource, FileChangeType.DELETED);
			if (modelFileDeleted) {
				newInOrphanModeGuess = true;
				fileEventImpactsModel = true;
			}
		}
175

176
		if (fileEventImpactsModel && this.inOrphanMode !== newInOrphanModeGuess) {
177
			let newInOrphanModeValidated: boolean = false;
178 179 180 181 182
			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.
183
				await timeout(100);
184

185 186 187 188 189 190
				if (this.disposed) {
					newInOrphanModeValidated = true;
				} else {
					const exists = await this.fileService.exists(this.resource);
					newInOrphanModeValidated = !exists;
				}
191
			}
192

193 194 195
			if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) {
				this.setOrphaned(newInOrphanModeValidated);
			}
196 197 198 199 200 201 202 203 204 205
		}
	}

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

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

B
Benjamin Pasero 已提交
209
		this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled;
R
Rob Lourens 已提交
210
		this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : undefined;
211 212
	}

B
Benjamin Pasero 已提交
213
	private onFilesAssociationChange(): void {
214
		if (!this.isResolved()) {
B
Benjamin Pasero 已提交
215 216 217
			return;
		}

A
Alex Dima 已提交
218
		const firstLineText = this.getFirstLineText(this.textEditorModel);
219
		const languageSelection = this.getOrCreateMode(this.resource, this.modeService, this.preferredMode, firstLineText);
B
Benjamin Pasero 已提交
220

A
Alex Dima 已提交
221
		this.modelService.setMode(this.textEditorModel, languageSelection);
B
Benjamin Pasero 已提交
222 223
	}

224 225 226 227 228 229
	setMode(mode: string): void {
		super.setMode(mode);

		this.preferredMode = mode;
	}

230
	async backup(target = this.resource): Promise<void> {
231 232 233 234
		if (this.isResolved()) {

			// Only fill in model metadata if resource matches
			let meta: IBackupMetaData | undefined = undefined;
235
			if (isEqual(target, this.resource) && this.lastResolvedFileStat) {
236
				meta = {
237
					mtime: this.lastResolvedFileStat.mtime,
238
					ctime: this.lastResolvedFileStat.ctime,
239 240
					size: this.lastResolvedFileStat.size,
					etag: this.lastResolvedFileStat.etag,
241 242 243
					orphaned: this.inOrphanMode
				};
			}
244

245
			return this.backupFileService.backupResource<IBackupMetaData>(target, this.createSnapshot(), this.versionId, meta);
246 247 248
		}
	}

249 250 251 252
	hasBackup(): boolean {
		return this.backupFileService.hasBackupSync(this.resource, this.versionId);
	}

253
	async revert(soft?: boolean): Promise<void> {
E
Erich Gamma 已提交
254
		if (!this.isResolved()) {
255
			return;
E
Erich Gamma 已提交
256 257
		}

258
		// Cancel any running auto-save
259
		this.autoSaveDisposable.clear();
E
Erich Gamma 已提交
260 261

		// Unset flags
262
		const wasDirty = this.dirty;
B
Benjamin Pasero 已提交
263
		const undo = this.setDirty(false);
E
Erich Gamma 已提交
264

265 266 267 268 269
		// Force read from disk unless reverting soft
		if (!soft) {
			try {
				await this.load({ forceReadFromDisk: true });
			} catch (error) {
E
Erich Gamma 已提交
270

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

274 275
				throw error;
			}
276
		}
277 278 279

		// Emit file change event
		this._onDidStateChange.fire(StateChange.REVERTED);
280 281 282 283 284

		// Emit dirty change event
		if (wasDirty) {
			this._onDidChangeDirty.fire();
		}
E
Erich Gamma 已提交
285 286
	}

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

B
Benjamin Pasero 已提交
290 291 292
		// 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.
293
		if (this.dirty || this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
294
			this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource);
E
Erich Gamma 已提交
295

296
			return this;
E
Erich Gamma 已提交
297 298
		}

299
		// Only for new models we support to load from backup
300
		if (!this.isResolved()) {
301 302
			const backup = await this.backupFileService.loadBackupResource(this.resource);

303
			if (this.isResolved()) {
304 305 306 307 308
				return this; // Make sure meanwhile someone else did not suceed in loading
			}

			if (backup) {
				try {
B
Benjamin Pasero 已提交
309
					return await this.loadFromBackup(backup, options);
310
				} catch (error) {
B
Benjamin Pasero 已提交
311
					this.logService.error(error); // ignore error and continue to load as file below
312 313
				}
			}
314 315 316
		}

		// Otherwise load from file resource
317
		return this.loadFromFile(options);
318 319
	}

320
	private async loadFromBackup(backup: URI, options?: ILoadOptions): Promise<TextFileEditorModel> {
321

322
		// Resolve actual backup contents
323
		const resolvedBackup = await this.backupFileService.resolveBackupContent<IBackupMetaData>(backup);
324

325
		if (this.isResolved()) {
326
			return this; // Make sure meanwhile someone else did not suceed in loading
327
		}
328

329 330
		// Load with backup
		this.loadFromContent({
331 332
			resource: this.resource,
			name: basename(this.resource),
B
Benjamin Pasero 已提交
333
			mtime: resolvedBackup.meta ? resolvedBackup.meta.mtime : Date.now(),
334
			ctime: resolvedBackup.meta ? resolvedBackup.meta.ctime : Date.now(),
B
Benjamin Pasero 已提交
335 336
			size: resolvedBackup.meta ? resolvedBackup.meta.size : 0,
			etag: resolvedBackup.meta ? resolvedBackup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
337
			value: resolvedBackup.value,
338 339 340
			encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
			isReadonly: false
		}, options, true /* from backup */);
341 342

		// Restore orphaned flag based on state
B
Benjamin Pasero 已提交
343
		if (resolvedBackup.meta && resolvedBackup.meta.orphaned) {
344 345 346 347
			this.setOrphaned(true);
		}

		return this;
348 349
	}

350
	private async loadFromFile(options?: ILoadOptions): Promise<TextFileEditorModel> {
B
Benjamin Pasero 已提交
351 352
		const forceReadFromDisk = options?.forceReadFromDisk;
		const allowBinary = this.isResolved() /* always allow if we resolved previously */ || options?.allowBinary;
353

E
Erich Gamma 已提交
354
		// Decide on etag
355
		let etag: string | undefined;
356
		if (forceReadFromDisk) {
357
			etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk
358 359
		} else if (this.lastResolvedFileStat) {
			etag = this.lastResolvedFileStat.etag; // otherwise respect etag to support caching
E
Erich Gamma 已提交
360 361
		}

B
Benjamin Pasero 已提交
362 363 364 365 366 367 368 369
		// 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 已提交
370
		// Resolve Content
371
		try {
372
			const content = await this.textFileService.readStream(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding });
373

374 375
			// Clear orphaned state when loading was successful
			this.setOrphaned(false);
376

377 378
			if (currentVersionId !== this.versionId) {
				return this; // Make sure meanwhile someone else did not suceed loading
379
			}
E
Erich Gamma 已提交
380

381
			return this.loadFromContent(content, options);
382 383
		} catch (error) {
			const result = error.fileOperationResult;
E
Erich Gamma 已提交
384

385 386
			// Apply orphaned state based on error code
			this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND);
387

388 389
			// NotModified status is expected and can be handled gracefully
			if (result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
E
Erich Gamma 已提交
390

391 392 393
				// 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 已提交
394
				}
395

396 397
				return this;
			}
398

399 400 401 402 403 404 405 406 407 408
			// 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;
		}
409
	}
E
Erich Gamma 已提交
410

411
	private loadFromContent(content: ITextFileStreamContent, options?: ILoadOptions, fromBackup?: boolean): TextFileEditorModel {
I
isidor 已提交
412
		this.logService.trace('load() - resolved content', this.resource);
413 414

		// Update our resolved disk stat model
415
		this.updateLastResolvedFileStat({
416 417 418
			resource: this.resource,
			name: content.name,
			mtime: content.mtime,
419
			ctime: content.ctime,
420
			size: content.size,
421
			etag: content.etag,
422
			isFile: true,
423
			isDirectory: false,
424
			isSymbolicLink: false,
I
isidor 已提交
425
			isReadonly: content.isReadonly
426
		});
427 428 429 430 431 432 433 434 435 436 437

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

439
		// Update Existing Model
440
		if (this.isResolved()) {
B
Benjamin Pasero 已提交
441
			this.doUpdateTextModel(content.value);
442
		}
B
Benjamin Pasero 已提交
443

444 445 446
		// Create New Model
		else {
			this.doCreateTextModel(content.resource, content.value, !!fromBackup);
447
		}
E
Erich Gamma 已提交
448

449 450 451
		// Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype
		const settingsType = this.getTypeIfSettings();
		if (settingsType) {
452 453 454 455 456
			type SettingsReadClassification = {
				settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
			};

			this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data
457
		} else {
458 459
			type FileGetClassification = {} & FileTelemetryDataFragment;

B
Benjamin Pasero 已提交
460
			this.telemetryService.publicLog2<TelemetryData, FileGetClassification>('fileGet', this.getTelemetryData(options?.reason ?? LoadReason.OTHER));
461 462 463 464
		}

		return this;
	}
E
Erich Gamma 已提交
465

466 467 468 469
	private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void {
		this.logService.trace('load() - created text editor model', this.resource);

		// Create model
470
		this.createTextEditorModel(value, resource, this.preferredMode);
471 472 473 474 475

		// 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 (fromBackup) {
B
Benjamin Pasero 已提交
476
			this.doMakeDirty();
477 478 479
			if (this.autoSaveAfterMilliesEnabled) {
				this.doAutoSave(this.versionId);
			}
480
		}
E
Erich Gamma 已提交
481

482 483 484 485 486 487 488
		// Ensure we are not tracking a stale state
		else {
			this.setDirty(false);
		}

		// Model Listeners
		this.installModelListeners();
489
	}
E
Erich Gamma 已提交
490

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

494 495
		// Ensure we are not tracking a stale state
		this.setDirty(false);
496

497
		// Update model value in a block that ignores model content change events
498 499
		this.blockModelContentChange = true;
		try {
500
			this.updateTextEditorModel(value, this.preferredMode);
501 502 503 504
		} finally {
			this.blockModelContentChange = false;
		}

505 506
		// Ensure we track the latest saved version ID given that the contents changed
		this.updateSavedVersionId();
507 508
	}

B
Benjamin Pasero 已提交
509 510
	private installModelListeners(): void {

511 512 513
		// 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 已提交
514 515

		// Content Change
516
		if (this.isResolved()) {
M
Matt Bierner 已提交
517 518
			this._register(this.textEditorModel.onDidChangeContent(() => this.onModelContentChanged()));
		}
519 520
	}

521
	private onModelContentChanged(): void {
I
isidor 已提交
522
		this.logService.trace(`onModelContentChanged() - enter`, this.resource);
E
Erich Gamma 已提交
523 524 525

		// In any case increment the version id because it tracks the textual content state of the model at all times
		this.versionId++;
I
isidor 已提交
526
		this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource);
E
Erich Gamma 已提交
527 528 529 530 531 532 533 534 535 536

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

			// Clear flags
541
			const wasDirty = this.dirty;
E
Erich Gamma 已提交
542 543 544
			this.setDirty(false);

			// Emit event
545
			if (wasDirty) {
546
				this._onDidStateChange.fire(StateChange.REVERTED);
547
				this._onDidChangeDirty.fire();
548
			}
E
Erich Gamma 已提交
549 550 551 552

			return;
		}

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

		// Mark as dirty
B
Benjamin Pasero 已提交
556
		this.doMakeDirty();
E
Erich Gamma 已提交
557 558

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
559
		if (this.autoSaveAfterMilliesEnabled) {
560
			if (!this.inConflictMode) {
E
Erich Gamma 已提交
561 562
				this.doAutoSave(this.versionId);
			} else {
I
isidor 已提交
563
				this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource);
E
Erich Gamma 已提交
564 565
			}
		}
566

567 568
		// Handle content change events
		this.contentChangeEventScheduler.schedule();
E
Erich Gamma 已提交
569 570
	}

B
Benjamin Pasero 已提交
571 572 573 574 575 576 577 578 579
	makeDirty(): void {
		if (!this.isResolved()) {
			return; // only resolved models can be marked dirty
		}

		this.doMakeDirty();
	}

	private doMakeDirty(): void {
E
Erich Gamma 已提交
580 581

		// Track dirty state and version id
B
Benjamin Pasero 已提交
582
		const wasDirty = this.dirty;
E
Erich Gamma 已提交
583 584 585 586
		this.setDirty(true);

		// Emit as Event if we turned dirty
		if (!wasDirty) {
587
			this._onDidStateChange.fire(StateChange.DIRTY);
588
			this._onDidChangeDirty.fire();
E
Erich Gamma 已提交
589 590 591
		}
	}

592
	private doAutoSave(versionId: number): void {
I
isidor 已提交
593
		this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
E
Erich Gamma 已提交
594 595

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

598 599
		// Create new save timer and store it for disposal as needed
		const handle = setTimeout(() => {
E
Erich Gamma 已提交
600

601 602 603
			// Clear the timeout now that we are running
			this.autoSaveDisposable.clear();

E
Erich Gamma 已提交
604 605
			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
606
				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 已提交
607
			}
608
		}, this.autoSaveAfterMillies);
E
Erich Gamma 已提交
609

610
		this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle));
E
Erich Gamma 已提交
611 612
	}

613
	async save(options: ISaveOptions = Object.create(null)): Promise<void> {
E
Erich Gamma 已提交
614
		if (!this.isResolved()) {
615
			return;
E
Erich Gamma 已提交
616 617
		}

I
isidor 已提交
618
		this.logService.trace('save() - enter', this.resource);
E
Erich Gamma 已提交
619 620

		// Cancel any currently running auto saves to make this the one that succeeds
621
		this.autoSaveDisposable.clear();
E
Erich Gamma 已提交
622

623
		return this.doSave(this.versionId, options);
E
Erich Gamma 已提交
624 625
	}

J
Johannes Rieken 已提交
626
	private doSave(versionId: number, options: ISaveOptions): Promise<void> {
627
		if (isUndefinedOrNull(options.reason)) {
628 629 630
			options.reason = SaveReason.EXPLICIT;
		}

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

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

641
			return this.saveSequentializer.pendingSave || Promise.resolve();
E
Erich Gamma 已提交
642 643
		}

644
		// Return early if not dirty (unless forced) or version changed meanwhile
B
Benjamin Pasero 已提交
645 646 647 648 649 650
		//
		// 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.
		//
651
		if ((!options.force && !this.dirty) || versionId !== this.versionId) {
I
isidor 已提交
652
			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 已提交
653

654
			return Promise.resolve();
E
Erich Gamma 已提交
655 656
		}

657
		// Return if currently saving by storing this save request as the next save that should happen.
658
		// Never ever must 2 saves execute at the same time because this can lead to dirty writes and race conditions.
B
Benjamin Pasero 已提交
659
		//
660
		// Scenario A: auto save was triggered and is currently busy saving to disk. this takes long enough that another auto save
661
		//             kicks in.
662 663
		// 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 已提交
664
		//
665
		if (this.saveSequentializer.hasPendingSave()) {
I
isidor 已提交
666
			this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource);
E
Erich Gamma 已提交
667

668
			// Register this as the next upcoming save and return
669
			return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options));
E
Erich Gamma 已提交
670 671 672 673
		}

		// 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
674
		if (!this.autoSaveAfterMilliesEnabled && this.isResolved()) {
M
Matt Bierner 已提交
675
			this.textEditorModel.pushStackElement();
E
Erich Gamma 已提交
676 677
		}

B
Benjamin Pasero 已提交
678
		// A save participant can still change the model now and since we are so close to saving
E
Erich Gamma 已提交
679 680
		// 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
681
		// Save participants can also be skipped through API.
J
Johannes Rieken 已提交
682
		let saveParticipantPromise: Promise<number> = Promise.resolve(versionId);
683
		if (TextFileEditorModel.saveParticipant && !options.skipSaveParticipants) {
B
💄  
Benjamin Pasero 已提交
684
			const onCompleteOrError = () => {
J
Johannes Rieken 已提交
685
				this.blockModelContentChange = false;
B
💄  
Benjamin Pasero 已提交
686

687
				return this.versionId;
B
💄  
Benjamin Pasero 已提交
688 689
			};

B
Benjamin Pasero 已提交
690
			this.blockModelContentChange = true;
691
			saveParticipantPromise = TextFileEditorModel.saveParticipant.participate(this as IResolvedTextFileEditorModel, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
E
Erich Gamma 已提交
692 693
		}

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

697 698 699 700 701
			// 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).
702
			// To fix this issue, we will not store the contents to disk when we got disposed.
703
			if (this.disposed) {
704 705 706 707 708 709
				return;
			}

			// We require a resolved model from this point on, since we are about to write data to disk.
			if (!this.isResolved()) {
				return;
710 711
			}

B
Benjamin Pasero 已提交
712 713
			// 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.
714 715 716 717 718 719
			// 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 已提交
720
				return this.doTouch(newVersionId);
721 722
			}

723
			// update versionId with its new value (if pre-save changes happened)
J
Johannes Rieken 已提交
724 725 726 727 728 729 730 731 732
			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
733
			// mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering)
B
Benjamin Pasero 已提交
734
			this.logService.trace(`doSave(${versionId}) - before write()`, this.resource);
B
Benjamin Pasero 已提交
735 736
			const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat);
			return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), {
737 738
				overwriteReadonly: options.overwriteReadonly,
				overwriteEncoding: options.overwriteEncoding,
B
Benjamin Pasero 已提交
739
				mtime: lastResolvedFileStat.mtime,
J
Johannes Rieken 已提交
740
				encoding: this.getEncoding(),
B
Benjamin Pasero 已提交
741
				etag: lastResolvedFileStat.etag,
742
				writeElevated: options.writeElevated
743
			}).then(stat => {
B
Benjamin Pasero 已提交
744
				this.logService.trace(`doSave(${versionId}) - after write()`, this.resource);
J
Johannes Rieken 已提交
745 746 747

				// Update dirty state unless model has changed meanwhile
				if (versionId === this.versionId) {
I
isidor 已提交
748
					this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource);
J
Johannes Rieken 已提交
749 750
					this.setDirty(false);
				} else {
I
isidor 已提交
751
					this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource);
J
Johannes Rieken 已提交
752
				}
E
Erich Gamma 已提交
753

754
				// Updated resolved stat with updated stat
755
				this.updateLastResolvedFileStat(stat);
E
Erich Gamma 已提交
756

757 758 759
				// Cancel any content change event promises as they are no longer valid
				this.contentChangeEventScheduler.cancel();

760
				// Emit Events
J
Johannes Rieken 已提交
761
				this._onDidStateChange.fire(StateChange.SAVED);
762
				this._onDidChangeDirty.fire();
763 764 765 766

				// Telemetry
				const settingsType = this.getTypeIfSettings();
				if (settingsType) {
767 768 769 770
					type SettingsWrittenClassification = {
						settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
					};
					this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
771
				} else {
772 773
					type FilePutClassfication = {} & FileTelemetryDataFragment;
					this.telemetryService.publicLog2<TelemetryData, FilePutClassfication>('filePUT', this.getTelemetryData(options.reason));
774
				}
775
			}, error => {
I
isidor 已提交
776
				this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource);
E
Erich Gamma 已提交
777

778
				// Flag as error state in the model
J
Johannes Rieken 已提交
779
				this.inErrorMode = true;
E
Erich Gamma 已提交
780

781
				// Look out for a save conflict
782
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
783 784 785
					this.inConflictMode = true;
				}

J
Johannes Rieken 已提交
786 787
				// Show to user
				this.onSaveError(error);
E
Erich Gamma 已提交
788

J
Johannes Rieken 已提交
789 790
				// Emit as event
				this._onDidStateChange.fire(StateChange.SAVE_ERROR);
791 792
			}));
		}));
E
Erich Gamma 已提交
793 794
	}

795
	private getTypeIfSettings(): string {
B
Benjamin Pasero 已提交
796
		if (extname(this.resource) !== '.json') {
797
			return '';
798
		}
799 800

		// Check for global settings file
801
		if (isEqual(this.resource, this.environmentService.settingsResource)) {
802 803 804 805
			return 'global-settings';
		}

		// Check for keybindings file
806
		if (isEqual(this.resource, this.environmentService.keybindingsResource)) {
807 808 809 810
			return 'keybindings';
		}

		// Check for snippets
811
		if (isEqualOrParent(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) {
812
			return 'snippets';
813 814 815
		}

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

		return '';
827 828
	}

829
	private getTelemetryData(reason: number | undefined): TelemetryData {
830 831
		const ext = extname(this.resource);
		const fileName = basename(this.resource);
832
		const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path;
833
		const telemetryData = {
B
Benjamin Pasero 已提交
834
			mimeType: guessMimeTypes(this.resource).join(', '),
835
			ext,
836
			path: hash(path),
M
Matt Bierner 已提交
837 838
			reason,
			whitelistedjson: undefined as string | undefined
839 840 841 842 843 844 845
		};

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

		return telemetryData;
846 847
	}

J
Johannes Rieken 已提交
848
	private doTouch(versionId: number): Promise<void> {
849 850
		if (!this.isResolved()) {
			return Promise.resolve();
M
Matt Bierner 已提交
851
		}
852

B
Benjamin Pasero 已提交
853 854 855
		const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat);
		return this.saveSequentializer.setPending(versionId, this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), {
			mtime: lastResolvedFileStat.mtime,
B
Benjamin Pasero 已提交
856
			encoding: this.getEncoding(),
B
Benjamin Pasero 已提交
857
			etag: lastResolvedFileStat.etag
B
Benjamin Pasero 已提交
858
		}).then(stat => {
859 860

			// Updated resolved stat with updated stat since touching it might have changed mtime
861
			this.updateLastResolvedFileStat(stat);
B
Benjamin Pasero 已提交
862 863 864 865

			// Emit File Saved Event
			this._onDidStateChange.fire(StateChange.SAVED);

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
	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)
899
		if (this.isResolved()) {
900 901 902 903
			this.bufferSavedVersionId = this.textEditorModel.getAlternativeVersionId();
		}
	}

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

		// First resolve - just take
907 908
		if (!this.lastResolvedFileStat) {
			this.lastResolvedFileStat = newFileStat;
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.lastResolvedFileStat.mtime <= newFileStat.mtime) {
			this.lastResolvedFileStat = newFileStat;
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(): this is IResolvedTextFileEditorModel {
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
	}

B
Benjamin Pasero 已提交
938
	hasState(state: ModelState): boolean {
939 940 941 942 943 944 945 946 947 948 949
		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();
950 951
			case ModelState.PENDING_AUTO_SAVE:
				return !!this.autoSaveDisposable.value;
952 953
			case ModelState.SAVED:
				return !this.dirty;
E
Erich Gamma 已提交
954 955 956
		}
	}

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

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

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

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

				return;
			}

			this.updatePreferredEncoding(encoding);

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

998
	updatePreferredEncoding(encoding: string | undefined): void {
E
Erich Gamma 已提交
999 1000 1001 1002 1003 1004 1005
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
1006
		this._onDidStateChange.fire(StateChange.ENCODING);
E
Erich Gamma 已提交
1007 1008
	}

1009
	private isNewEncoding(encoding: string | undefined): boolean {
E
Erich Gamma 已提交
1010 1011 1012 1013 1014
		if (this.preferredEncoding === encoding) {
			return false; // return early if the encoding is already the same
		}

		if (!this.preferredEncoding && this.contentEncoding === encoding) {
1015
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
1016 1017 1018 1019 1020
		}

		return true;
	}

1021 1022
	isResolved(): this is IResolvedTextFileEditorModel {
		return !!this.textEditorModel;
E
Erich Gamma 已提交
1023 1024
	}

B
Benjamin Pasero 已提交
1025
	isReadonly(): boolean {
1026
		return !!(this.lastResolvedFileStat && this.lastResolvedFileStat.isReadonly);
1027 1028
	}

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

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

B
Benjamin Pasero 已提交
1037
	getStat(): IFileStatWithMetadata | undefined {
1038
		return this.lastResolvedFileStat;
B
Benjamin Pasero 已提交
1039 1040
	}

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

		super.dispose();
	}
1049 1050
}

1051 1052
interface IPendingSave {
	versionId: number;
J
Johannes Rieken 已提交
1053
	promise: Promise<void>;
1054 1055
}

1056
interface ISaveOperation {
J
Johannes Rieken 已提交
1057
	promise: Promise<void>;
B
Benjamin Pasero 已提交
1058 1059
	promiseResolve: () => void;
	promiseReject: (error: Error) => void;
J
Johannes Rieken 已提交
1060
	run: () => Promise<void>;
1061 1062 1063
}

export class SaveSequentializer {
1064 1065
	private _pendingSave?: IPendingSave;
	private _nextSave?: ISaveOperation;
1066

B
Benjamin Pasero 已提交
1067
	hasPendingSave(versionId?: number): boolean {
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
		if (!this._pendingSave) {
			return false;
		}

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

		return !!this._pendingSave;
	}

1079
	get pendingSave(): Promise<void> | undefined {
R
Rob Lourens 已提交
1080
		return this._pendingSave ? this._pendingSave.promise : undefined;
1081 1082
	}

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

1086
		promise.then(() => this.donePending(versionId), () => this.donePending(versionId));
1087 1088 1089 1090 1091 1092

		return promise;
	}

	private donePending(versionId: number): void {
		if (this._pendingSave && versionId === this._pendingSave.versionId) {
1093 1094

			// only set pending to done if the promise finished that is associated with that versionId
R
Rob Lourens 已提交
1095
			this._pendingSave = undefined;
1096 1097 1098

			// schedule the next save now that we are free if we have any
			this.triggerNextSave();
1099 1100 1101
		}
	}

1102 1103 1104
	private triggerNextSave(): void {
		if (this._nextSave) {
			const saveOperation = this._nextSave;
R
Rob Lourens 已提交
1105
			this._nextSave = undefined;
1106 1107

			// Run next save and complete on the associated promise
B
Benjamin Pasero 已提交
1108
			saveOperation.run().then(saveOperation.promiseResolve, saveOperation.promiseReject);
1109 1110 1111
		}
	}

J
Johannes Rieken 已提交
1112
	setNext(run: () => Promise<void>): Promise<void> {
1113 1114 1115 1116 1117

		// 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 已提交
1118 1119
			let promiseResolve: () => void;
			let promiseReject: (error: Error) => void;
B
Benjamin Pasero 已提交
1120 1121 1122
			const promise = new Promise<void>((resolve, reject) => {
				promiseResolve = resolve;
				promiseReject = reject;
1123 1124 1125 1126 1127
			});

			this._nextSave = {
				run,
				promise,
M
Matt Bierner 已提交
1128 1129
				promiseResolve: promiseResolve!,
				promiseReject: promiseReject!
1130 1131 1132 1133 1134 1135 1136 1137
			};
		}

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

M
Matt Bierner 已提交
1138
		return this._nextSave.promise;
1139 1140 1141
	}
}

1142 1143
class DefaultSaveErrorHandler implements ISaveErrorHandler {

1144
	constructor(@INotificationService private readonly notificationService: INotificationService) { }
1145

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