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

B
Benjamin Pasero 已提交
7
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
8
import { TPromise } from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
9
import URI from 'vs/base/common/uri';
10
import paths = require('vs/base/common/paths');
11
import errors = require('vs/base/common/errors');
12
import objects = require('vs/base/common/objects');
J
Johannes Rieken 已提交
13
import Event, { Emitter } from 'vs/base/common/event';
14 15 16
import platform = require('vs/base/common/platform');
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
I
isidor 已提交
17
import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveNotAfterDelayContext } from 'vs/workbench/services/textfile/common/textfiles';
J
Johannes Rieken 已提交
18
import { ConfirmResult } from 'vs/workbench/common/editor';
19
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
20
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
21
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
22 23
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
24
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
25
import { IUntitledEditorService, UNTITLED_SCHEMA } from 'vs/workbench/services/untitled/common/untitledEditorService';
J
Johannes Rieken 已提交
26 27 28
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
B
Benjamin Pasero 已提交
29
import { IMessageService, Severity } from 'vs/platform/message/common/message';
B
Benjamin Pasero 已提交
30
import { ResourceMap } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
31 32
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
33
import { IRevertOptions } from 'vs/platform/editor/common/editor';
I
isidor 已提交
34
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
E
Erich Gamma 已提交
35

36 37 38 39
export interface IBackupResult {
	didBackup: boolean;
}

E
Erich Gamma 已提交
40 41 42 43 44 45
/**
 * The workbench file service implementation implements the raw file service spec and adds additional methods on top.
 *
 * It also adds diagnostics and logging around file system operations.
 */
export abstract class TextFileService implements ITextFileService {
46

47
	public _serviceBrand: any;
E
Erich Gamma 已提交
48

B
Benjamin Pasero 已提交
49
	private toUnbind: IDisposable[];
50 51
	private _models: TextFileEditorModelManager;

52 53 54
	private _onFilesAssociationChange: Emitter<void>;
	private currentFilesAssociationConfig: { [key: string]: string; };

55
	private _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration>;
56
	private configuredAutoSaveDelay: number;
57 58
	private configuredAutoSaveOnFocusChange: boolean;
	private configuredAutoSaveOnWindowChange: boolean;
E
Erich Gamma 已提交
59

I
isidor 已提交
60 61
	private autoSaveNotAfterDelayContext: IContextKey<boolean>;

62
	private configuredHotExit: string;
63

E
Erich Gamma 已提交
64
	constructor(
65 66 67 68 69 70 71 72 73 74
		private lifecycleService: ILifecycleService,
		private contextService: IWorkspaceContextService,
		private configurationService: IConfigurationService,
		protected fileService: IFileService,
		private untitledEditorService: IUntitledEditorService,
		private instantiationService: IInstantiationService,
		private messageService: IMessageService,
		protected environmentService: IEnvironmentService,
		private backupFileService: IBackupFileService,
		private windowsService: IWindowsService,
I
isidor 已提交
75 76
		private historyService: IHistoryService,
		contextKeyService: IContextKeyService
E
Erich Gamma 已提交
77
	) {
B
Benjamin Pasero 已提交
78
		this.toUnbind = [];
79

80
		this._onAutoSaveConfigurationChange = new Emitter<IAutoSaveConfiguration>();
81 82 83 84 85
		this.toUnbind.push(this._onAutoSaveConfigurationChange);

		this._onFilesAssociationChange = new Emitter<void>();
		this.toUnbind.push(this._onFilesAssociationChange);

86
		this._models = this.instantiationService.createInstance(TextFileEditorModelManager);
I
isidor 已提交
87
		this.autoSaveNotAfterDelayContext = AutoSaveNotAfterDelayContext.bindTo(contextKeyService);
88

89
		const configuration = this.configurationService.getValue<IFilesConfiguration>();
90 91
		this.currentFilesAssociationConfig = configuration && configuration.files && configuration.files.associations;

92
		this.onFilesConfigurationChange(configuration);
93

94
		this.registerListeners();
E
Erich Gamma 已提交
95 96
	}

97 98 99 100
	public get models(): ITextFileEditorModelManager {
		return this._models;
	}

101
	abstract resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise<IRawTextContent>;
A
Alex Dima 已提交
102

103
	abstract promptForPath(defaultPath: string): TPromise<string>;
104

105
	abstract confirmSave(resources?: URI[]): TPromise<ConfirmResult>;
106

107 108 109 110
	public get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> {
		return this._onAutoSaveConfigurationChange.event;
	}

111 112 113 114
	public get onFilesAssociationChange(): Event<void> {
		return this._onFilesAssociationChange.event;
	}

115
	private registerListeners(): void {
116

117
		// Lifecycle
118
		this.lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown(event.reason)));
119 120
		this.lifecycleService.onShutdown(this.dispose, this);

121 122 123
		// Files configuration changes
		this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('files')) {
124
				this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
125 126
			}
		}));
127 128
	}

129
	private beforeShutdown(reason: ShutdownReason): boolean | TPromise<boolean> {
B
Benjamin Pasero 已提交
130

131 132 133 134
		// Dirty files need treatment on shutdown
		const dirty = this.getDirty();
		if (dirty.length) {

135
			// If auto save is enabled, save all files and then check again for dirty files
136
			// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
137
			let handleAutoSave: TPromise<URI[] /* remaining dirty resources */>;
138
			if (this.getAutoSaveMode() !== AutoSaveMode.OFF) {
139
				handleAutoSave = this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => this.getDirty());
140 141
			} else {
				handleAutoSave = TPromise.as(dirty);
B
Benjamin Pasero 已提交
142 143
			}

144
			return handleAutoSave.then(dirty => {
145

146 147 148
				// If we still have dirty files, we either have untitled ones or files that cannot be saved
				// or auto save was not enabled and as such we did not save any dirty files to disk automatically
				if (dirty.length) {
B
Benjamin Pasero 已提交
149

150
					// If hot exit is enabled, backup dirty files and allow to exit without confirmation
151
					if (this.isHotExitEnabled) {
152
						return this.backupBeforeShutdown(dirty, this.models, reason).then(result => {
153 154 155 156 157 158 159 160
							if (result.didBackup) {
								return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
							}

							// since a backup did not happen, we have to confirm for the dirty files now
							return this.confirmBeforeShutdown();
						}, errors => {
							const firstError = errors[0];
B
Benjamin Pasero 已提交
161
							this.messageService.show(Severity.Error, nls.localize('files.backup.failSave', "Files that are dirty could not be written to the backup location (Error: {0}). Try saving your files first and then exit.", firstError.message));
162

163 164 165 166 167 168 169
							return true; // veto, the backups failed
						});
					}

					// Otherwise just confirm from the user what to do with the dirty files
					return this.confirmBeforeShutdown();
				}
170 171

				return void 0;
172
			});
173 174
		}

B
Benjamin Pasero 已提交
175 176
		// No dirty files: no veto
		return this.noVeto({ cleanUpBackups: true });
177 178
	}

179 180 181 182 183 184 185 186 187 188 189
	private backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): TPromise<IBackupResult> {
		return this.windowsService.getWindowCount().then(windowCount => {

			// When quit is requested skip the confirm callback and attempt to backup all workspaces.
			// When quit is not requested the confirm callback should be shown when the window being
			// closed is the only VS Code window open, except for on Mac where hot exit is only
			// ever activated when quit is requested.

			let doBackup: boolean;
			switch (reason) {
				case ShutdownReason.CLOSE:
190
					if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
D
Daniel Imms 已提交
191
						doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
192
					} else if (windowCount > 1 || platform.isMacintosh) {
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
						doBackup = false; // do not backup if a window is closed that does not cause quitting of the application
					} else {
						doBackup = true; // backup if last window is closed on win/linux where the application quits right after
					}
					break;

				case ShutdownReason.QUIT:
					doBackup = true; // backup because next start we restore all backups
					break;

				case ShutdownReason.RELOAD:
					doBackup = true; // backup because after window reload, backups restore
					break;

				case ShutdownReason.LOAD:
208
					if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
209 210 211 212
						doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
					} else {
						doBackup = false; // do not backup because we are switching contexts
					}
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
					break;
			}

			if (!doBackup) {
				return TPromise.as({ didBackup: false });
			}

			// Backup
			return this.backupAll(dirtyToBackup, textFileEditorModelManager).then(() => { return { didBackup: true }; });
		});
	}

	private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): TPromise<void> {

		// split up between files and untitled
		const filesToBackup: ITextFileEditorModel[] = [];
		const untitledToBackup: URI[] = [];
		dirtyToBackup.forEach(s => {
231
			if (s.scheme === Schemas.file) {
232
				filesToBackup.push(textFileEditorModelManager.get(s));
233
			} else if (s.scheme === UNTITLED_SCHEMA) {
234 235 236 237 238 239 240 241 242 243 244 245 246
				untitledToBackup.push(s);
			}
		});

		return this.doBackupAll(filesToBackup, untitledToBackup);
	}

	private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): TPromise<void> {

		// Handle file resources first
		return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()))).then(results => {

			// Handle untitled resources
247 248 249
			const untitledModelPromises = untitledResources
				.filter(untitled => this.untitledEditorService.exists(untitled))
				.map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
250 251 252 253 254 255 256 257 258 259 260

			return TPromise.join(untitledModelPromises).then(untitledModels => {
				const untitledBackupPromises = untitledModels.map(model => {
					return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId());
				});

				return TPromise.join(untitledBackupPromises).then(() => void 0);
			});
		});
	}

261
	private confirmBeforeShutdown(): boolean | TPromise<boolean> {
262
		return this.confirmSave().then(confirm => {
263

264 265 266 267 268 269
			// Save
			if (confirm === ConfirmResult.SAVE) {
				return this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true }).then(result => {
					if (result.results.some(r => !r.success)) {
						return true; // veto if some saves failed
					}
270

271 272 273
					return this.noVeto({ cleanUpBackups: true });
				});
			}
274

275 276
			// Don't Save
			else if (confirm === ConfirmResult.DONT_SAVE) {
277

278 279 280
				// Make sure to revert untitled so that they do not restore
				// see https://github.com/Microsoft/vscode/issues/29572
				this.untitledEditorService.revertAll();
281

282 283
				return this.noVeto({ cleanUpBackups: true });
			}
284

285 286 287 288
			// Cancel
			else if (confirm === ConfirmResult.CANCEL) {
				return true; // veto
			}
289

290 291
			return void 0;
		});
292 293
	}

B
Benjamin Pasero 已提交
294 295
	private noVeto(options: { cleanUpBackups: boolean }): boolean | TPromise<boolean> {
		if (!options.cleanUpBackups) {
B
Benjamin Pasero 已提交
296 297 298
			return false;
		}

299 300 301
		return this.cleanupBackupsBeforeShutdown().then(() => false, () => false);
	}

302
	protected cleanupBackupsBeforeShutdown(): TPromise<void> {
303 304 305 306 307
		if (this.environmentService.isExtensionDevelopment) {
			return TPromise.as(void 0);
		}

		return this.backupFileService.discardAllWorkspaceBackups();
B
Benjamin Pasero 已提交
308 309
	}

310
	protected onFilesConfigurationChange(configuration: IFilesConfiguration): void {
311
		const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF);
312

313
		const autoSaveMode = (configuration && configuration.files && configuration.files.autoSave) || AutoSaveConfiguration.OFF;
314
		switch (autoSaveMode) {
315
			case AutoSaveConfiguration.AFTER_DELAY:
316 317
				this.configuredAutoSaveDelay = configuration && configuration.files && configuration.files.autoSaveDelay;
				this.configuredAutoSaveOnFocusChange = false;
318
				this.configuredAutoSaveOnWindowChange = false;
I
isidor 已提交
319
				this.autoSaveNotAfterDelayContext.set(false);
320 321
				break;

322
			case AutoSaveConfiguration.ON_FOCUS_CHANGE:
323 324
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = true;
325
				this.configuredAutoSaveOnWindowChange = false;
I
isidor 已提交
326
				this.autoSaveNotAfterDelayContext.set(true);
327 328 329 330 331 332
				break;

			case AutoSaveConfiguration.ON_WINDOW_CHANGE:
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = false;
				this.configuredAutoSaveOnWindowChange = true;
I
isidor 已提交
333
				this.autoSaveNotAfterDelayContext.set(true);
334 335 336 337 338
				break;

			default:
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = false;
339
				this.configuredAutoSaveOnWindowChange = false;
I
isidor 已提交
340
				this.autoSaveNotAfterDelayContext.set(true);
341 342
				break;
		}
343

344 345
		// Emit as event
		this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
346

347
		// save all dirty when enabling auto save
348
		if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) {
349
			this.saveAll().done(null, errors.onUnexpectedError);
350
		}
351 352 353 354 355 356 357

		// Check for change in files associations
		const filesAssociation = configuration && configuration.files && configuration.files.associations;
		if (!objects.equals(this.currentFilesAssociationConfig, filesAssociation)) {
			this.currentFilesAssociationConfig = filesAssociation;
			this._onFilesAssociationChange.fire();
		}
358 359

		// Hot exit
B
Benjamin Pasero 已提交
360
		const hotExitMode = configuration && configuration.files && configuration.files.hotExit;
B
Benjamin Pasero 已提交
361
		if (hotExitMode === HotExitConfiguration.OFF || hotExitMode === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
362
			this.configuredHotExit = hotExitMode;
B
Benjamin Pasero 已提交
363 364
		} else {
			this.configuredHotExit = HotExitConfiguration.ON_EXIT;
365
		}
E
Erich Gamma 已提交
366 367
	}

368
	public getDirty(resources?: URI[]): URI[] {
369 370 371 372 373

		// Collect files
		const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());

		// Add untitled ones
374
		dirty.push(...this.untitledEditorService.getDirty(resources));
375 376

		return dirty;
E
Erich Gamma 已提交
377 378 379
	}

	public isDirty(resource?: URI): boolean {
380 381

		// Check for dirty file
382
		if (this._models.getAll(resource).some(model => model.isDirty())) {
383 384 385 386 387
			return true;
		}

		// Check for dirty untitled
		return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
E
Erich Gamma 已提交
388 389
	}

390 391
	public save(resource: URI, options?: ISaveOptions): TPromise<boolean> {

392
		// Run a forced save if we detect the file is not dirty so that save participants can still run
393
		if (options && options.force && resource.scheme === Schemas.file && !this.isDirty(resource)) {
394 395 396 397
			const model = this._models.get(resource);
			if (model) {
				model.save({ force: true, reason: SaveReason.EXPLICIT }).then(() => !model.isDirty());
			}
398 399
		}

400
		return this.saveAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
E
Erich Gamma 已提交
401 402
	}

403 404 405
	public saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
	public saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
	public saveAll(arg1?: any, options?: ISaveOptions): TPromise<ITextFileOperationResult> {
406 407 408 409 410 411 412 413 414 415 416 417 418

		// get all dirty
		let toSave: URI[] = [];
		if (Array.isArray(arg1)) {
			toSave = this.getDirty(arg1);
		} else {
			toSave = this.getDirty();
		}

		// split up between files and untitled
		const filesToSave: URI[] = [];
		const untitledToSave: URI[] = [];
		toSave.forEach(s => {
J
Johannes Rieken 已提交
419
			// TODO@remote
J
Johannes Rieken 已提交
420 421 422 423
			// if (s.scheme === Schemas.file) {
			// 	filesToSave.push(s);
			// } else
			if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) {
424
				untitledToSave.push(s);
J
Johannes Rieken 已提交
425 426
			} else {
				filesToSave.push(s);
427 428 429
			}
		});

430
		return this.doSaveAll(filesToSave, untitledToSave, options);
431 432
	}

433
	private doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult> {
434 435

		// Handle files first that can just be saved
436
		return this.doSaveAllFiles(fileResources, options).then(async result => {
437 438 439 440

			// Preflight for untitled to handle cancellation from the dialog
			const targetsForUntitled: URI[] = [];
			for (let i = 0; i < untitledResources.length; i++) {
441 442
				const untitled = untitledResources[i];
				if (this.untitledEditorService.exists(untitled)) {
443 444 445
					let targetPath: string;

					// Untitled with associated file path don't need to prompt
446 447
					if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
						targetPath = untitled.fsPath;
448 449 450 451
					}

					// Otherwise ask user
					else {
452
						targetPath = await this.promptForPath(this.suggestFileName(untitled));
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
						if (!targetPath) {
							return TPromise.as({
								results: [...fileResources, ...untitledResources].map(r => {
									return {
										source: r
									};
								})
							});
						}
					}

					targetsForUntitled.push(URI.file(targetPath));
				}
			}

			// Handle untitled
			const untitledSaveAsPromises: TPromise<void>[] = [];
			targetsForUntitled.forEach((target, index) => {
				const untitledSaveAsPromise = this.saveAs(untitledResources[index], target).then(uri => {
					result.results.push({
						source: untitledResources[index],
						target: uri,
						success: !!uri
					});
				});

				untitledSaveAsPromises.push(untitledSaveAsPromise);
			});

			return TPromise.join(untitledSaveAsPromises).then(() => {
				return result;
			});
		});
	}

488
	private doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): TPromise<ITextFileOperationResult> {
B
Benjamin Pasero 已提交
489
		const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : void 0 /* Save All */)
490
			.filter(model => {
491
				if (model.hasState(ModelState.CONFLICT) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) {
492
					return false; // if model is in save conflict, do not save unless save reason is explicit or not provided at all
493 494 495 496
				}

				return true;
			});
E
Erich Gamma 已提交
497

B
Benjamin Pasero 已提交
498
		const mapResourceToResult = new ResourceMap<IResult>();
499
		dirtyFileModels.forEach(m => {
B
Benjamin Pasero 已提交
500
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
501
				source: m.getResource()
B
Benjamin Pasero 已提交
502
			});
E
Erich Gamma 已提交
503 504
		});

505
		return TPromise.join(dirtyFileModels.map(model => {
506
			return model.save(options).then(() => {
E
Erich Gamma 已提交
507
				if (!model.isDirty()) {
B
Benjamin Pasero 已提交
508
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
509 510
				}
			});
511
		})).then(r => {
E
Erich Gamma 已提交
512
			return {
B
Benjamin Pasero 已提交
513
				results: mapResourceToResult.values()
E
Erich Gamma 已提交
514 515 516 517
			};
		});
	}

518 519 520
	private getFileModels(resources?: URI[]): ITextFileEditorModel[];
	private getFileModels(resource?: URI): ITextFileEditorModel[];
	private getFileModels(arg1?: any): ITextFileEditorModel[] {
E
Erich Gamma 已提交
521
		if (Array.isArray(arg1)) {
522
			const models: ITextFileEditorModel[] = [];
523
			(<URI[]>arg1).forEach(resource => {
E
Erich Gamma 已提交
524 525 526 527 528 529
				models.push(...this.getFileModels(resource));
			});

			return models;
		}

530
		return this._models.getAll(<URI>arg1);
E
Erich Gamma 已提交
531 532
	}

533 534 535
	private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[];
	private getDirtyFileModels(resource?: URI): ITextFileEditorModel[];
	private getDirtyFileModels(arg1?: any): ITextFileEditorModel[] {
536
		return this.getFileModels(arg1).filter(model => model.isDirty());
E
Erich Gamma 已提交
537 538
	}

539
	public saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
540 541

		// Get to target resource
542 543 544 545
		let targetPromise: TPromise<URI>;
		if (target) {
			targetPromise = TPromise.wrap(target);
		} else {
546
			let dialogPath = resource.fsPath;
547
			if (resource.scheme === UNTITLED_SCHEMA) {
548 549 550
				dialogPath = this.suggestFileName(resource);
			}

551 552 553 554
			targetPromise = this.promptForPath(dialogPath).then(pathRaw => {
				if (pathRaw) {
					return URI.file(pathRaw);
				}
555

556 557
				return void 0;
			});
558 559
		}

560 561 562 563
		return targetPromise.then(target => {
			if (!target) {
				return TPromise.as(null); // user canceled
			}
564

565 566 567 568 569 570 571 572
			// Just save if target is same as models own resource
			if (resource.toString() === target.toString()) {
				return this.save(resource, options).then(() => resource);
			}

			// Do it
			return this.doSaveAs(resource, target, options);
		});
573 574
	}

575
	private doSaveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
576 577

		// Retrieve text model from provided resource if any
578
		let modelPromise: TPromise<ITextFileEditorModel | UntitledEditorModel> = TPromise.as(null);
579
		if (resource.scheme === Schemas.file) {
580
			modelPromise = TPromise.as(this._models.get(resource));
581 582
		} else if (resource.scheme === UNTITLED_SCHEMA && this.untitledEditorService.exists(resource)) {
			modelPromise = this.untitledEditorService.loadOrCreate({ resource });
583 584
		}

R
Ron Buckton 已提交
585
		return modelPromise.then<any>(model => {
586 587 588

			// We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before)
			if (model) {
589
				return this.doSaveTextFileAs(model, resource, target, options);
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
			}

			// Otherwise we can only copy
			return this.fileService.copyFile(resource, target);
		}).then(() => {

			// Revert the source
			return this.revert(resource).then(() => {

				// Done: return target
				return target;
			});
		});
	}

605
	private doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): TPromise<void> {
606
		let targetModelResolver: TPromise<ITextFileEditorModel>;
607

608 609 610 611 612
		// Prefer an existing model if it is already loaded for the given target resource
		const targetModel = this.models.get(target);
		if (targetModel && targetModel.isResolved()) {
			targetModelResolver = TPromise.as(targetModel);
		}
613

614 615 616 617 618 619
		// Otherwise create the target file empty if it does not exist already and resolve it from there
		else {
			targetModelResolver = this.fileService.resolveFile(target).then(stat => stat, () => null).then(stat => stat || this.fileService.updateContent(target, '')).then(stat => {
				return this.models.loadOrCreate(target);
			});
		}
620

621
		return targetModelResolver.then(targetModel => {
622

623 624 625
			// take over encoding and model value from source model
			targetModel.updatePreferredEncoding(sourceModel.getEncoding());
			targetModel.textEditorModel.setValue(sourceModel.getValue());
626

627
			// save model
628
			return targetModel.save(options);
629
		}, error => {
630

631
			// binary model: delete the file and run the operation again
632
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
633
				return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target, options));
634 635 636
			}

			return TPromise.wrapError(error);
637 638 639 640
		});
	}

	private suggestFileName(untitledResource: URI): string {
641 642 643 644 645
		const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);

		const lastActiveFile = this.historyService.getLastActiveFile();
		if (lastActiveFile) {
			return URI.file(paths.join(paths.dirname(lastActiveFile.fsPath), untitledFileName)).fsPath;
646 647
		}

648 649 650 651 652
		const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot('file');
		if (lastActiveFolder) {
			return URI.file(paths.join(lastActiveFolder.fsPath, untitledFileName)).fsPath;
		}

653
		return untitledFileName;
654
	}
E
Erich Gamma 已提交
655

656 657
	public revert(resource: URI, options?: IRevertOptions): TPromise<boolean> {
		return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
E
Erich Gamma 已提交
658 659
	}

660
	public revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
661 662

		// Revert files first
663
		return this.doRevertAllFiles(resources, options).then(operation => {
664 665 666 667 668 669 670 671 672

			// Revert untitled
			const reverted = this.untitledEditorService.revertAll(resources);
			reverted.forEach(res => operation.results.push({ source: res, success: true }));

			return operation;
		});
	}

673 674
	private doRevertAllFiles(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
		const fileModels = options && options.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
E
Erich Gamma 已提交
675

B
Benjamin Pasero 已提交
676
		const mapResourceToResult = new ResourceMap<IResult>();
677
		fileModels.forEach(m => {
B
Benjamin Pasero 已提交
678
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
679
				source: m.getResource()
B
Benjamin Pasero 已提交
680
			});
E
Erich Gamma 已提交
681 682
		});

683
		return TPromise.join(fileModels.map(model => {
684
			return model.revert(options && options.soft).then(() => {
E
Erich Gamma 已提交
685
				if (!model.isDirty()) {
B
Benjamin Pasero 已提交
686
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
687
				}
688
			}, error => {
E
Erich Gamma 已提交
689

690
				// FileNotFound means the file got deleted meanwhile, so still record as successful revert
691
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
692
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
693 694 695 696
				}

				// Otherwise bubble up the error
				else {
697
					return TPromise.wrapError(error);
E
Erich Gamma 已提交
698
				}
B
Benjamin Pasero 已提交
699

700
				return void 0;
E
Erich Gamma 已提交
701
			});
702
		})).then(r => {
703
			return {
B
Benjamin Pasero 已提交
704
				results: mapResourceToResult.values()
705
			};
E
Erich Gamma 已提交
706 707 708
		});
	}

709 710 711 712 713
	public getAutoSaveMode(): AutoSaveMode {
		if (this.configuredAutoSaveOnFocusChange) {
			return AutoSaveMode.ON_FOCUS_CHANGE;
		}

714 715 716 717
		if (this.configuredAutoSaveOnWindowChange) {
			return AutoSaveMode.ON_WINDOW_CHANGE;
		}

718
		if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
719
			return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
720 721 722
		}

		return AutoSaveMode.OFF;
723 724 725 726
	}

	public getAutoSaveConfiguration(): IAutoSaveConfiguration {
		return {
727
			autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : void 0,
728 729
			autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
			autoSaveApplicationChange: this.configuredAutoSaveOnWindowChange
B
Benjamin Pasero 已提交
730
		};
731 732
	}

733
	public get isHotExitEnabled(): boolean {
734
		return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
735 736
	}

E
Erich Gamma 已提交
737
	public dispose(): void {
B
Benjamin Pasero 已提交
738
		this.toUnbind = dispose(this.toUnbind);
E
Erich Gamma 已提交
739 740

		// Clear all caches
741
		this._models.clear();
E
Erich Gamma 已提交
742
	}
J
Johannes Rieken 已提交
743
}