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

import nls = require('vs/nls');
import {TPromise, Promise} from 'vs/base/common/winjs.base';
import {onUnexpectedError, toErrorMessage, getHttpStatus} from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import diagnostics = require('vs/base/common/diagnostics');
import types = require('vs/base/common/types');
import {IModelContentChangedEvent, EventType as EditorEventType} from 'vs/editor/common/editorCommon';
import {IMode} from 'vs/editor/common/modes';
16
import {EventType as WorkbenchEventType, ResourceEvent} from 'vs/workbench/common/events';
17
import {LocalFileChangeEvent, EventType as FileEventType, TextFileChangeEvent, ITextFileService, IAutoSaveConfiguration} from 'vs/workbench/parts/files/common/files';
E
Erich Gamma 已提交
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
import {EncodingMode, EditorModel, IEncodingSupport} from 'vs/workbench/common/editor';
import {BaseTextEditorModel} from 'vs/workbench/browser/parts/editor/textEditorModel';
import {IFileService, IFileStat, IFileOperationResult, FileOperationResult, IContent} from 'vs/platform/files/common/files';
import {IEventService} from 'vs/platform/event/common/event';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IModeService} from 'vs/editor/common/services/modeService';
import {IModelService} from 'vs/editor/common/services/modelService';
import {ITelemetryService, anonymize} from 'vs/platform/telemetry/common/telemetry';

/**
 * The save error handler can be installed on the text text file editor model to install code that executes when save errors occur.
 */
export interface ISaveErrorHandler {

	/**
	 * Called whenever a save fails.
	 */
	onSaveError(error: any, model: TextFileEditorModel): void;
}

class DefaultSaveErrorHandler implements ISaveErrorHandler {

	constructor( @IMessageService private messageService: IMessageService) { }

	public onSaveError(error: any, model: TextFileEditorModel): void {
		let message: string;

		// 412 Dirty write prevention
		if (getHttpStatus(error) === 412) {
			message = nls.localize('staleSaveError', "Failed to save '{0}': The version on disk is newer. Please open the file and save it again.", paths.basename(model.getResource().fsPath));
		}

		// Any other save error
		else {
			message = nls.localize('genericSaveError', "Failed to save '{0}': {1}", paths.basename(model.getResource().fsPath), toErrorMessage(error, false));
		}

		this.messageService.show(Severity.Error, message);
	}
}

// Diagnostics support
let diag: (...args: any[]) => void;
if (!diag) {
	diag = diagnostics.register('TextFileEditorModelDiagnostics', function(...args: any[]) {
		console.log(args[1] + ' - ' + args[0] + ' (time: ' + args[2].getTime() + ' [' + args[2].toUTCString() + '])');
	});
}

/**
 * States the text text file editor model can be in.
 */
export enum State {
	SAVED,
	DIRTY,
	PENDING_SAVE,
	CONFLICT,
	ERROR
}

/**
 * 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.
 */
export class TextFileEditorModel extends BaseTextEditorModel implements IEncodingSupport {

	public static ID = 'workbench.editors.files.textFileEditorModel';

	private static DEFAULT_AUTO_SAVE_DELAY = 1000;
	private static saveErrorHandler: ISaveErrorHandler;

	private resource: URI;
	private contentEncoding: string; 			// encoding as reported from disk
	private preferredEncoding: string;			// encoding as chosen by the user
	private textModelChangeListener: () => void;
	private dirty: boolean;
	private versionId: number;
	private bufferSavedVersionId: number;
	private versionOnDiskStat: IFileStat;
	private blockModelContentChange: boolean;
98
	private autoSaveAfterMillies: number;
99
	private autoSaveAfterMilliesEnabled: boolean;
E
Erich Gamma 已提交
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
	private autoSavePromises: TPromise<void>[];
	private mapPendingSaveToVersionId: { [versionId: string]: TPromise<void> };
	private disposed: boolean;
	private inConflictResolutionMode: boolean;
	private inErrorMode: boolean;
	private lastDirtyTime: number;
	private createTextEditorModelPromise: TPromise<TextFileEditorModel>;

	constructor(
		resource: URI,
		preferredEncoding: string,
		@IMessageService private messageService: IMessageService,
		@IModeService modeService: IModeService,
		@IModelService modelService: IModelService,
		@IEventService private eventService: IEventService,
		@IFileService private fileService: IFileService,
		@IInstantiationService private instantiationService: IInstantiationService,
117 118
		@ITelemetryService private telemetryService: ITelemetryService,
		@ITextFileService private textFileService: ITextFileService
E
Erich Gamma 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
	) {
		super(modelService, modeService);

		this.resource = resource;
		if (this.resource.scheme !== 'file') {
			throw new Error('TextFileEditorModel can only handle file:// resources.');
		}

		this.preferredEncoding = preferredEncoding;
		this.textModelChangeListener = null;
		this.dirty = false;
		this.autoSavePromises = [];
		this.versionId = 0;
		this.lastDirtyTime = 0;
		this.mapPendingSaveToVersionId = {};

135
		this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
E
Erich Gamma 已提交
136 137
	}

138
	public updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
139 140
		if (typeof config.autoSaveDelay === 'number' && config.autoSaveDelay > 0) {
			this.autoSaveAfterMillies = config.autoSaveDelay * 1000;
141
			this.autoSaveAfterMilliesEnabled = true;
E
Erich Gamma 已提交
142
		} else {
143
			this.autoSaveAfterMillies = void 0;
144
			this.autoSaveAfterMilliesEnabled = false;
E
Erich Gamma 已提交
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
		}
	}

	/**
	 * Set a save error handler to install code that executes when save errors occur.
	 */
	public static setSaveErrorHandler(handler: ISaveErrorHandler): void {
		TextFileEditorModel.saveErrorHandler = handler;
	}

	/**
	 * When set, will disable any saving (including auto save) until the model is loaded again. This allows to resolve save conflicts
	 * without running into subsequent save errors when editing the model.
	 */
	public setConflictResolutionMode(): void {
		diag('setConflictResolutionMode() - enabled conflict resolution mode', this.resource, new Date());

		this.inConflictResolutionMode = true;
	}

	/**
	 * Answers if this model is currently in conflic resolution mode or not.
	 */
	public isInConflictResolutionMode(): boolean {
		return this.inConflictResolutionMode;
	}

	/**
	 * Discards any local changes and replaces the model with the contents of the version on disk.
	 */
	public revert(): TPromise<void> {
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

		// Cancel any running auto-saves
		this.cancelAutoSavePromises();

		// Be prepared to send out a file change event in case reverting changes anything
		let oldStat = this.cloneStat(this.versionOnDiskStat);

		// Unset flags
		let undo = this.setDirty(false);

		// Reload
		return this.load(true /* force */).then(() => {

			// Emit file change event
			this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.textEditorModel, oldStat, this.versionOnDiskStat));
		}, (error) => {

			// FileNotFound means the file got deleted meanwhile, so emit revert event because thats ok
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
				this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.textEditorModel, oldStat, this.versionOnDiskStat));
			}

			// Set flags back to previous values, we are still dirty if revert failed and we where
			else {
				undo();
			}

			return Promise.wrapError(error);
		});
	}

	public load(force?: boolean /* bypass any caches and really go to disk */): TPromise<EditorModel> {
		diag('load() - enter', this.resource, new Date());

		// It is very important to not reload the model when the model is dirty. We only want to reload the model from the disk
		// if no save is pending to avoid data loss. This might cause a save conflict in case the file has been modified on the disk
		// meanwhile, but this is a very low risk.
		if (this.dirty) {
			diag('load() - exit - without loading because model is dirty', this.resource, new Date());

			return TPromise.as(this);
		}

		// Decide on etag
		let etag: string;
		if (force) {
			etag = undefined; // bypass cache if force loading is true
		} else if (this.versionOnDiskStat) {
			etag = this.versionOnDiskStat.etag; // otherwise respect etag to support caching
		}

		// Resolve Content
		return this.fileService.resolveContent(this.resource, { acceptTextOnly: true, etag: etag, encoding: this.preferredEncoding }).then((content: IContent) => {
			diag('load() - resolved content', this.resource, new Date());

			// Telemetry
			this.telemetryService.publicLog('fileGet', { mimeType: content.mime, ext: paths.extname(this.resource.fsPath), path: anonymize(this.resource.fsPath) });

			// Update our resolved disk stat model
			let resolvedStat: IFileStat = {
				resource: this.resource,
				name: content.name,
				mtime: content.mtime,
				etag: content.etag,
				mime: content.mime,
				isDirectory: false,
				hasChildren: false,
				children: void 0,
			};
			this.updateVersionOnDiskStat(resolvedStat);

			// Keep the original charset to not loose it when saving
			let oldEncoding = this.contentEncoding;
			this.contentEncoding = content.charset;

			// 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.eventService.emit(WorkbenchEventType.RESOURCE_ENCODING_CHANGED, new ResourceEvent(this.resource));
			}

			// Update Existing Model
			if (this.textEditorModel) {
				diag('load() - updated text editor model', this.resource, new Date());

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

				this.blockModelContentChange = true;
				try {
					this.updateTextEditorModel(content.value);
				} finally {
					this.blockModelContentChange = false;
				}

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

			// Join an existing request to create the editor model to avoid race conditions
			else if (this.createTextEditorModelPromise) {
				diag('load() - join existing text editor model promise', this.resource, new Date());

				return this.createTextEditorModelPromise;
			}

			// Create New Model
			else {
				diag('load() - created text editor model', this.resource, new Date());

B
Benjamin Pasero 已提交
288
				this.createTextEditorModelPromise = this.createTextEditorModel(content.value, content.resource).then(() => {
E
Erich Gamma 已提交
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
					this.createTextEditorModelPromise = null;

					this.setDirty(false); // Ensure we are not tracking a stale state
					this.textModelChangeListener = this.textEditorModel.addListener(EditorEventType.ModelContentChanged, (e: IModelContentChangedEvent) => this.onModelContentChanged(e));

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

					return Promise.wrapError(error);
				});

				return this.createTextEditorModelPromise;
			}
		}, (error) => {

			// NotModified status code is expected and can be handled gracefully
			if ((<IFileOperationResult>error).fileOperationResult === FileOperationResult.FILE_NOT_MODIFIED_SINCE) {
				this.setDirty(false); // Ensure we are not tracking a stale state

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

			// Otherwise bubble up the error
			return Promise.wrapError(error);
		});
	}

317
	protected getOrCreateMode(modeService: IModeService, preferredModeIds: string, firstLineText?: string): TPromise<IMode> {
E
Erich Gamma 已提交
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
		return modeService.getOrCreateModeByFilenameOrFirstLine(this.resource.fsPath, firstLineText);
	}

	private onModelContentChanged(e: IModelContentChangedEvent): void {
		diag('onModelContentChanged(' + e.changeType + ') - enter', this.resource, new Date());

		// In any case increment the version id because it tracks the textual content state of the model at all times
		this.versionId++;
		diag('onModelContentChanged() - new versionId ' + this.versionId, this.resource, new Date());

		// 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.
337
		if (!this.autoSaveAfterMilliesEnabled && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
E
Erich Gamma 已提交
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
			diag('onModelContentChanged() - model content changed back to last saved version', this.resource, new Date());

			// Clear flags
			this.setDirty(false);

			// Emit event
			this.emitEvent(FileEventType.FILE_REVERTED, new TextFileChangeEvent(this.textEditorModel, this.versionOnDiskStat));

			return;
		}

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

		// Mark as dirty
		this.makeDirty(e);

		// Start auto save process unless we are in conflict resolution mode and unless it is disabled
355
		if (this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
			if (!this.inConflictResolutionMode) {
				this.doAutoSave(this.versionId);
			} else {
				diag('makeDirty() - prevented save because we are in conflict resolution mode', this.resource, new Date());
			}
		}
	}

	private makeDirty(e?: IModelContentChangedEvent): void {

		// Track dirty state and version id
		let wasDirty = this.dirty;
		this.setDirty(true);
		this.lastDirtyTime = new Date().getTime();

		// Emit as Event if we turned dirty
		if (!wasDirty) {
			let stat = this.cloneStat(this.versionOnDiskStat);
			this.emitEvent(FileEventType.FILE_DIRTY, new TextFileChangeEvent(this.textEditorModel, stat, stat, <any>e));
		}
	}

	private doAutoSave(versionId: number): TPromise<void> {
		diag('doAutoSave() - enter for versionId ' + versionId, this.resource, new Date());

		// Cancel any currently running auto saves to make this the one that succeeds
		this.cancelAutoSavePromises();

384 385
		// Create new save promise and keep it
		let promise: TPromise<void> = Promise.timeout(this.autoSaveAfterMillies).then(() => {
E
Erich Gamma 已提交
386 387 388

			// Only trigger save if the version id has not changed meanwhile
			if (versionId === this.versionId) {
389
				this.doSave(versionId, true); // 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 已提交
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
			}
		});

		this.autoSavePromises.push(promise);

		return promise;
	}

	private cancelAutoSavePromises(): void {
		while (this.autoSavePromises.length) {
			this.autoSavePromises.pop().cancel();
		}
	}

	/**
	 * Saves the current versionId of this editor model if it is dirty.
	 */
	public save(overwriteReadonly?: boolean): TPromise<void> {
		if (!this.isResolved()) {
			return TPromise.as<void>(null);
		}

		diag('save() - enter', this.resource, new Date());

		// Cancel any currently running auto saves to make this the one that succeeds
		this.cancelAutoSavePromises();

417
		return this.doSave(this.versionId, false, overwriteReadonly);
E
Erich Gamma 已提交
418 419
	}

420
	private doSave(versionId: number, isAutoSave: boolean, overwriteReadonly?: boolean): TPromise<void> {
E
Erich Gamma 已提交
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
		diag('doSave(' + versionId + ') - enter with versionId ' + versionId, this.resource, new Date());

		// Lookup any running pending save for this versionId and return it if found
		let pendingSave = this.mapPendingSaveToVersionId[versionId];
		if (pendingSave) {
			diag('doSave(' + versionId + ') - exit - found a pending save for versionId ' + versionId, this.resource, new Date());

			return pendingSave;
		}

		// Return early if not dirty or version changed meanwhile
		if (!this.dirty || versionId !== this.versionId) {
			diag('doSave(' + versionId + ') - exit - because not dirty and/or versionId is different (this.isDirty: ' + this.dirty + ', this.versionId: ' + this.versionId + ')', this.resource, new Date());

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

		// Return if currently saving by scheduling another auto save. Never ever must 2 saves execute at the same time because
		// this can lead to dirty writes and race conditions
		if (this.isBusySaving()) {
			diag('doSave(' + versionId + ') - exit - because busy saving', this.resource, new Date());

443
			// Avoid endless loop here and guard if auto save is disabled
444
			if (this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
445 446 447 448 449 450
				return this.doAutoSave(versionId);
			}
		}

		// 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
451
		if (!this.autoSaveAfterMilliesEnabled) {
E
Erich Gamma 已提交
452 453 454 455 456 457 458 459 460
			this.textEditorModel.pushStackElement();
		}

		// Emit file saving event: Listeners can still change the model now and since we are so close to saving
		// 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
		let versionOnDiskStatClone = this.cloneStat(this.versionOnDiskStat);
		this.blockModelContentChange = true;
		try {
461 462 463
			const saveEvent = new TextFileChangeEvent(this.textEditorModel, versionOnDiskStatClone);
			saveEvent.setAutoSaved(isAutoSave);
			this.emitEvent(FileEventType.FILE_SAVING, saveEvent);
E
Erich Gamma 已提交
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
		} finally {
			this.blockModelContentChange = false;
		}
		versionId = this.versionId;

		// Clear error flag since we are trying to save again
		this.inErrorMode = false;

		// Save to Disk
		diag('doSave(' + versionId + ') - before updateContent()', this.resource, new Date());
		this.mapPendingSaveToVersionId[versionId] = this.fileService.updateContent(this.versionOnDiskStat.resource, this.getValue(), {
			overwriteReadonly: overwriteReadonly,
			mtime: this.versionOnDiskStat.mtime,
			charset: this.getEncoding(),
			etag: this.versionOnDiskStat.etag
		}).then((stat: IFileStat) => {
			diag('doSave(' + versionId + ') - after updateContent()', this.resource, new Date());

			// Telemetry
			this.telemetryService.publicLog('filePUT', { mimeType: stat.mime, ext: paths.extname(this.versionOnDiskStat.resource.fsPath) });

			// Remove from pending saves
			delete this.mapPendingSaveToVersionId[versionId];

			// Update dirty state unless model has changed meanwhile
			if (versionId === this.versionId) {
				diag('doSave(' + versionId + ') - setting dirty to false because versionId did not change', this.resource, new Date());
				this.setDirty(false);
			} else {
				diag('doSave(' + versionId + ') - not setting dirty to false because versionId did change meanwhile', this.resource, new Date());
			}

			// Updated resolved stat with updated stat, and keep old for event
			let oldStat = this.versionOnDiskStat;
			this.updateVersionOnDiskStat(stat);

			// Emit File Change Event
			let oldValue = this.cloneStat(oldStat);
			let newValue = this.cloneStat(this.versionOnDiskStat);
			this.emitEvent('files.internal:fileChanged', new TextFileChangeEvent(this.textEditorModel, oldValue, newValue));

			// Emit File Saved Event
			this.emitEvent(FileEventType.FILE_SAVED, new TextFileChangeEvent(this.textEditorModel, oldValue, newValue));
		}, (error) => {
			diag('doSave(' + versionId + ') - exit - resulted in a save error: ' + error.toString(), this.resource, new Date());

			// Remove from pending saves
			delete this.mapPendingSaveToVersionId[versionId];

			// Flag as error state
			this.inErrorMode = true;

			// Show to user
			this.onSaveError(error);

			// Emit as event
			this.emitEvent(FileEventType.FILE_SAVE_ERROR, new TextFileChangeEvent(this.textEditorModel, versionOnDiskStatClone));
		});

		return this.mapPendingSaveToVersionId[versionId];
	}

	private setDirty(dirty: boolean): () => void {
		let wasDirty = this.dirty;
		let wasInConflictResolutionMode = this.inConflictResolutionMode;
		let wasInErrorMode = this.inErrorMode;
		let oldBufferSavedVersionId = this.bufferSavedVersionId;

		if (!dirty) {
			this.dirty = false;
			this.inConflictResolutionMode = false;
			this.inErrorMode = false;

			// we remember the models alternate version id to remember when the version
			// of the model matches with the saved version on disk. we need to keep this
			// in order to find out if the model changed back to a saved version (e.g.
			// when undoing long enough to reach to a version that is saved and then to
			// clear the dirty flag)
			if (this.textEditorModel) {
				this.bufferSavedVersionId = this.textEditorModel.getAlternativeVersionId();
			}
		} else {
			this.dirty = true;
		}

		// Return function to revert this call
		return () => {
			this.dirty = wasDirty;
			this.inConflictResolutionMode = wasInConflictResolutionMode;
			this.inErrorMode = wasInErrorMode;
			this.bufferSavedVersionId = oldBufferSavedVersionId;
		};
	}

	private updateVersionOnDiskStat(newVersionOnDiskStat: IFileStat): void {

		// First resolve - just take
		if (!this.versionOnDiskStat) {
			this.versionOnDiskStat = newVersionOnDiskStat;
		}

		// Subsequent resolve - make sure that we only assign it if the mtime is equal or has advanced.
		// This is essential a If-Modified-Since check on the client ot prevent race conditions from loading
		// and saving. If a save comes in late after a revert was called, the mtime could be out of sync.
		else if (this.versionOnDiskStat.mtime <= newVersionOnDiskStat.mtime) {
			this.versionOnDiskStat = newVersionOnDiskStat;
		}
	}

	private onSaveError(error: any): void {

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

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

	private emitEvent(type: string, event: LocalFileChangeEvent): void {
		try {
			this.eventService.emit(type, event);
		} catch (e) {
			e.friendlyMessage = nls.localize('unexpectedEventError', "An unexpected error was thrown from a file change listener of type: {0}", type);
			onUnexpectedError(e);
		}
	}

	private isBusySaving(): boolean {
		return !types.isEmptyObject(this.mapPendingSaveToVersionId);
	}

	/**
	 * Returns true if the content of this model has changes that are not yet saved back to the disk.
	 */
	public isDirty(): boolean {
		return this.dirty;
	}

	/**
	 * Returns the time in millies when this working copy was edited by the user.
	 */
	public getLastDirtyTime(): number {
		return this.lastDirtyTime;
	}

	/**
	 * Returns the time in millies when this working copy was last modified by the user or some other program.
	 */
	public getLastModifiedTime(): number {
		return this.versionOnDiskStat ? this.versionOnDiskStat.mtime : -1;
	}

	/**
	 * Returns the state this text text file editor model is in with regards to changes and saving.
	 */
	public getState(): State {
		if (this.inConflictResolutionMode) {
			return State.CONFLICT;
		}

		if (this.inErrorMode) {
			return State.ERROR;
		}

		if (!this.dirty) {
			return State.SAVED;
		}

		if (this.isBusySaving()) {
			return State.PENDING_SAVE;
		}

		if (this.dirty) {
			return State.DIRTY;
		}
	}

	public getEncoding(): string {
		return this.preferredEncoding || this.contentEncoding;
	}

	public setEncoding(encoding: string, mode: EncodingMode): void {
		if (!this.isNewEncoding(encoding)) {
			return; // return early if the encoding is already the same
		}

		// Encode: Save with encoding
		if (mode === EncodingMode.Encode) {
			this.updatePreferredEncoding(encoding);

			// Save
			if (!this.isDirty()) {
				this.versionId++; // needs to increment because we change the model potentially
				this.makeDirty();
			}

			if (!this.inConflictResolutionMode) {
				this.save().done(null, onUnexpectedError);
			}
		}

		// Decode: Load with encoding
		else {
			if (this.isDirty()) {
				this.messageService.show(Severity.Info, nls.localize('saveFileFirst', "The file is dirty. Please save it first before reopening it with another encoding."));

				return;
			}

			this.updatePreferredEncoding(encoding);

			// Load
			this.load(true /* force because encoding has changed */).done(null, onUnexpectedError);
		}
	}

	private updatePreferredEncoding(encoding: string): void {
		if (!this.isNewEncoding(encoding)) {
			return;
		}

		this.preferredEncoding = encoding;

		// Emit
		this.eventService.emit(WorkbenchEventType.RESOURCE_ENCODING_CHANGED, new ResourceEvent(this.resource));
	}

	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) {
699
			return false; // also return if we don't have a preferred encoding but the content encoding is already the same
E
Erich Gamma 已提交
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
		}

		return true;
	}

	public isResolved(): boolean {
		return !types.isUndefinedOrNull(this.versionOnDiskStat);
	}

	/**
	 * Returns true if the dispose() method of this model has been called.
	 */
	public isDisposed(): boolean {
		return this.disposed;
	}

	/**
	 * Returns the full resource URI of the file this text file editor model is about.
	 */
	public getResource(): URI {
		return this.resource;
	}

	public dispose(): void {
		this.disposed = true;
		this.inConflictResolutionMode = false;
		this.inErrorMode = false;

		this.createTextEditorModelPromise = null;

		if (this.textModelChangeListener) {
			this.textModelChangeListener();
			this.textModelChangeListener = null;
		}

		this.cancelAutoSavePromises();

		CACHE.remove(this.resource);

		super.dispose();
	}

	private cloneStat(stat: IFileStat): IFileStat {
		return {
			resource: URI.parse(stat.resource.toString()),
			name: stat.name,
			mtime: stat.mtime,
			etag: stat.etag,
			mime: stat.mime,
			isDirectory: stat.isDirectory,
			hasChildren: stat.hasChildren,
			children: stat.children
		}
	}
}

export class TextFileEditorModelCache {
	private mapResourcePathToModel: { [resource: string]: TextFileEditorModel; };

	constructor() {
		this.mapResourcePathToModel = Object.create(null);
	}

	public dispose(resource: URI): void {
		let model = this.get(resource);
		if (model) {
			if (model.isDirty()) {
				return; // we never dispose dirty models to avoid data loss
			}

			model.dispose();
		}
	}

	public get(resource: URI): TextFileEditorModel {
		return this.mapResourcePathToModel[resource.toString()];
	}

	public getAll(resource?: URI): TextFileEditorModel[] {
		return Object.keys(this.mapResourcePathToModel)
			.filter((r) => !resource || resource.toString() === r)
			.map((r) => this.mapResourcePathToModel[r]);
	}

	public add(resource: URI, model: TextFileEditorModel): void {
		this.mapResourcePathToModel[resource.toString()] = model;
	}

	// Clients should not call this method
	public clear(): void {
		this.mapResourcePathToModel = Object.create(null);
	}

	// Clients should not call this method
	public remove(resource: URI): void {
		delete this.mapResourcePathToModel[resource.toString()];
	}
}

export const CACHE = new TextFileEditorModelCache();