textFileService.ts 30.6 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';
9
import { URI } from 'vs/base/common/uri';
10 11 12
import * as paths from 'vs/base/common/paths';
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
M
Matt Bierner 已提交
13
import { Event, Emitter } from 'vs/base/common/event';
14
import * as platform from 'vs/base/common/platform';
15 16
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
17
import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent } from 'vs/workbench/services/textfile/common/textfiles';
18
import { ConfirmResult, IRevertOptions } 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
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
B
Benjamin Pasero 已提交
23
import { Disposable } from 'vs/base/common/lifecycle';
24
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
25
import { IUntitledEditorService } 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 { ResourceMap } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
30 31
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
I
isidor 已提交
32
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
B
Benjamin Pasero 已提交
33 34
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
35
import { INotificationService } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
36
import { isEqualOrParent, isEqual } from 'vs/base/common/resources';
E
Erich Gamma 已提交
37

38 39 40 41
export interface IBackupResult {
	didBackup: boolean;
}

E
Erich Gamma 已提交
42 43 44 45 46
/**
 * 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.
 */
B
Benjamin Pasero 已提交
47
export abstract class TextFileService extends Disposable implements ITextFileService {
48

B
Benjamin Pasero 已提交
49
	_serviceBrand: any;
E
Erich Gamma 已提交
50

B
Benjamin Pasero 已提交
51
	private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
B
Benjamin Pasero 已提交
52
	get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> { return this._onAutoSaveConfigurationChange.event; }
53

B
Benjamin Pasero 已提交
54
	private readonly _onFilesAssociationChange: Emitter<void> = this._register(new Emitter<void>());
B
Benjamin Pasero 已提交
55
	get onFilesAssociationChange(): Event<void> { return this._onFilesAssociationChange.event; }
56

B
Benjamin Pasero 已提交
57 58
	private readonly _onWillMove = this._register(new Emitter<IWillMoveEvent>());
	get onWillMove(): Event<IWillMoveEvent> { return this._onWillMove.event; }
59

B
Benjamin Pasero 已提交
60 61
	private _models: TextFileEditorModelManager;
	private currentFilesAssociationConfig: { [key: string]: string; };
62
	private configuredAutoSaveDelay: number;
63 64
	private configuredAutoSaveOnFocusChange: boolean;
	private configuredAutoSaveOnWindowChange: boolean;
65
	private configuredHotExit: string;
B
Benjamin Pasero 已提交
66
	private autoSaveContext: IContextKey<string>;
67

E
Erich Gamma 已提交
68
	constructor(
69 70 71 72 73 74
		private lifecycleService: ILifecycleService,
		private contextService: IWorkspaceContextService,
		private configurationService: IConfigurationService,
		protected fileService: IFileService,
		private untitledEditorService: IUntitledEditorService,
		private instantiationService: IInstantiationService,
75
		private notificationService: INotificationService,
76 77 78
		protected environmentService: IEnvironmentService,
		private backupFileService: IBackupFileService,
		private windowsService: IWindowsService,
I
isidor 已提交
79
		private historyService: IHistoryService,
B
Benjamin Pasero 已提交
80 81
		contextKeyService: IContextKeyService,
		private modelService: IModelService
E
Erich Gamma 已提交
82
	) {
B
Benjamin Pasero 已提交
83
		super();
84

85
		this._models = this.instantiationService.createInstance(TextFileEditorModelManager);
I
isidor 已提交
86
		this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
87

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

91
		this.onFilesConfigurationChange(configuration);
92

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

B
Benjamin Pasero 已提交
96
	get models(): ITextFileEditorModelManager {
97 98 99
		return this._models;
	}

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

B
Benjamin Pasero 已提交
102
	abstract promptForPath(resource: URI, defaultPath: string): TPromise<string>;
103

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

106
	private registerListeners(): void {
107

108
		// Lifecycle
109
		this.lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown(event.reason)));
110 111
		this.lifecycleService.onShutdown(this.dispose, this);

112
		// Files configuration changes
B
Benjamin Pasero 已提交
113
		this._register(this.configurationService.onDidChangeConfiguration(e => {
114
			if (e.affectsConfiguration('files')) {
115
				this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
116 117
			}
		}));
118 119
	}

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

122 123 124 125
		// Dirty files need treatment on shutdown
		const dirty = this.getDirty();
		if (dirty.length) {

126
			// If auto save is enabled, save all files and then check again for dirty files
127
			// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
128
			let handleAutoSave: TPromise<URI[] /* remaining dirty resources */>;
129
			if (this.getAutoSaveMode() !== AutoSaveMode.OFF) {
130
				handleAutoSave = this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => this.getDirty());
131 132
			} else {
				handleAutoSave = TPromise.as(dirty);
B
Benjamin Pasero 已提交
133 134
			}

135
			return handleAutoSave.then(dirty => {
136

137 138 139
				// 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 已提交
140

141
					// If hot exit is enabled, backup dirty files and allow to exit without confirmation
142
					if (this.isHotExitEnabled) {
143
						return this.backupBeforeShutdown(dirty, this.models, reason).then(result => {
144 145 146 147 148 149 150 151
							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];
152
							this.notificationService.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));
153

154 155 156 157 158 159 160
							return true; // veto, the backups failed
						});
					}

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

				return void 0;
163
			});
164 165
		}

B
Benjamin Pasero 已提交
166 167
		// No dirty files: no veto
		return this.noVeto({ cleanUpBackups: true });
168 169
	}

170 171 172 173 174 175 176 177 178 179 180
	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:
181
					if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
D
Daniel Imms 已提交
182
						doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
183
					} else if (windowCount > 1 || platform.isMacintosh) {
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
						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:
199
					if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
200 201 202 203
						doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
					} else {
						doBackup = false; // do not backup because we are switching contexts
					}
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
					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 => {
222
			if (this.fileService.canHandleResource(s)) {
223
				filesToBackup.push(textFileEditorModelManager.get(s));
224
			} else if (s.scheme === Schemas.untitled) {
225 226 227 228 229 230 231 232 233 234
				untitledToBackup.push(s);
			}
		});

		return this.doBackupAll(filesToBackup, untitledToBackup);
	}

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

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

			// Handle untitled resources
238 239 240
			const untitledModelPromises = untitledResources
				.filter(untitled => this.untitledEditorService.exists(untitled))
				.map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
241 242 243

			return TPromise.join(untitledModelPromises).then(untitledModels => {
				const untitledBackupPromises = untitledModels.map(model => {
244
					return this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId());
245 246 247 248 249 250 251
				});

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

252
	private confirmBeforeShutdown(): boolean | TPromise<boolean> {
253
		return this.confirmSave().then(confirm => {
254

255 256 257 258 259 260
			// 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
					}
261

262 263 264
					return this.noVeto({ cleanUpBackups: true });
				});
			}
265

266 267
			// Don't Save
			else if (confirm === ConfirmResult.DONT_SAVE) {
268

269 270 271
				// Make sure to revert untitled so that they do not restore
				// see https://github.com/Microsoft/vscode/issues/29572
				this.untitledEditorService.revertAll();
272

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

276 277 278 279
			// Cancel
			else if (confirm === ConfirmResult.CANCEL) {
				return true; // veto
			}
280

281 282
			return void 0;
		});
283 284
	}

B
Benjamin Pasero 已提交
285 286
	private noVeto(options: { cleanUpBackups: boolean }): boolean | TPromise<boolean> {
		if (!options.cleanUpBackups) {
B
Benjamin Pasero 已提交
287 288 289
			return false;
		}

290 291 292
		return this.cleanupBackupsBeforeShutdown().then(() => false, () => false);
	}

293
	protected cleanupBackupsBeforeShutdown(): TPromise<void> {
294 295 296 297 298
		if (this.environmentService.isExtensionDevelopment) {
			return TPromise.as(void 0);
		}

		return this.backupFileService.discardAllWorkspaceBackups();
B
Benjamin Pasero 已提交
299 300
	}

301
	protected onFilesConfigurationChange(configuration: IFilesConfiguration): void {
302
		const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF);
303

304
		const autoSaveMode = (configuration && configuration.files && configuration.files.autoSave) || AutoSaveConfiguration.OFF;
I
isidor 已提交
305
		this.autoSaveContext.set(autoSaveMode);
306
		switch (autoSaveMode) {
307
			case AutoSaveConfiguration.AFTER_DELAY:
308 309
				this.configuredAutoSaveDelay = configuration && configuration.files && configuration.files.autoSaveDelay;
				this.configuredAutoSaveOnFocusChange = false;
310
				this.configuredAutoSaveOnWindowChange = false;
311 312
				break;

313
			case AutoSaveConfiguration.ON_FOCUS_CHANGE:
314 315
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = true;
316 317 318 319 320 321 322
				this.configuredAutoSaveOnWindowChange = false;
				break;

			case AutoSaveConfiguration.ON_WINDOW_CHANGE:
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = false;
				this.configuredAutoSaveOnWindowChange = true;
323 324 325 326 327
				break;

			default:
				this.configuredAutoSaveDelay = void 0;
				this.configuredAutoSaveOnFocusChange = false;
328
				this.configuredAutoSaveOnWindowChange = false;
329 330
				break;
		}
331

332 333
		// Emit as event
		this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
334

335
		// save all dirty when enabling auto save
336
		if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) {
337
			this.saveAll();
338
		}
339 340 341 342 343 344 345

		// 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();
		}
346 347

		// Hot exit
B
Benjamin Pasero 已提交
348
		const hotExitMode = configuration && configuration.files && configuration.files.hotExit;
B
Benjamin Pasero 已提交
349
		if (hotExitMode === HotExitConfiguration.OFF || hotExitMode === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
350
			this.configuredHotExit = hotExitMode;
B
Benjamin Pasero 已提交
351 352
		} else {
			this.configuredHotExit = HotExitConfiguration.ON_EXIT;
353
		}
E
Erich Gamma 已提交
354 355
	}

B
Benjamin Pasero 已提交
356
	getDirty(resources?: URI[]): URI[] {
357 358 359 360 361

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

		// Add untitled ones
362
		dirty.push(...this.untitledEditorService.getDirty(resources));
363 364

		return dirty;
E
Erich Gamma 已提交
365 366
	}

B
Benjamin Pasero 已提交
367
	isDirty(resource?: URI): boolean {
368 369

		// Check for dirty file
370
		if (this._models.getAll(resource).some(model => model.isDirty())) {
371 372 373 374 375
			return true;
		}

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

B
Benjamin Pasero 已提交
378
	save(resource: URI, options?: ISaveOptions): TPromise<boolean> {
379

380
		// Run a forced save if we detect the file is not dirty so that save participants can still run
381
		if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) {
382 383 384 385
			const model = this._models.get(resource);
			if (model) {
				model.save({ force: true, reason: SaveReason.EXPLICIT }).then(() => !model.isDirty());
			}
386 387
		}

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

B
Benjamin Pasero 已提交
391 392 393
	saveAll(includeUntitled?: boolean, options?: ISaveOptions): TPromise<ITextFileOperationResult>;
	saveAll(resources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult>;
	saveAll(arg1?: any, options?: ISaveOptions): TPromise<ITextFileOperationResult> {
394 395 396 397 398 399 400 401 402 403 404 405 406

		// 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 => {
407
			if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === Schemas.untitled) {
408
				untitledToSave.push(s);
J
Johannes Rieken 已提交
409 410
			} else {
				filesToSave.push(s);
411 412 413
			}
		});

414
		return this.doSaveAll(filesToSave, untitledToSave, options);
415 416
	}

417
	private doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): TPromise<ITextFileOperationResult> {
418 419

		// Handle files first that can just be saved
420
		return this.doSaveAllFiles(fileResources, options).then(async result => {
421 422 423 424

			// Preflight for untitled to handle cancellation from the dialog
			const targetsForUntitled: URI[] = [];
			for (let i = 0; i < untitledResources.length; i++) {
425 426
				const untitled = untitledResources[i];
				if (this.untitledEditorService.exists(untitled)) {
427
					let targetUri: URI;
428 429

					// Untitled with associated file path don't need to prompt
430
					if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
431
						targetUri = untitled.with({ scheme: Schemas.file });
432 433 434 435
					}

					// Otherwise ask user
					else {
B
Benjamin Pasero 已提交
436
						const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled));
437 438 439 440 441 442 443 444 445
						if (!targetPath) {
							return TPromise.as({
								results: [...fileResources, ...untitledResources].map(r => {
									return {
										source: r
									};
								})
							});
						}
446 447

						targetUri = URI.file(targetPath);
448 449
					}

450
					targetsForUntitled.push(targetUri);
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
				}
			}

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

474
	private doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): TPromise<ITextFileOperationResult> {
B
Benjamin Pasero 已提交
475
		const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : void 0 /* Save All */)
476
			.filter(model => {
477 478
				if ((model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) {
					return false; // if model is in save conflict or error, do not save unless save reason is explicit or not provided at all
479 480 481 482
				}

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

B
Benjamin Pasero 已提交
484
		const mapResourceToResult = new ResourceMap<IResult>();
485
		dirtyFileModels.forEach(m => {
B
Benjamin Pasero 已提交
486
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
487
				source: m.getResource()
B
Benjamin Pasero 已提交
488
			});
E
Erich Gamma 已提交
489 490
		});

491
		return TPromise.join(dirtyFileModels.map(model => {
492
			return model.save(options).then(() => {
E
Erich Gamma 已提交
493
				if (!model.isDirty()) {
B
Benjamin Pasero 已提交
494
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
495 496
				}
			});
497
		})).then(r => {
E
Erich Gamma 已提交
498
			return {
B
Benjamin Pasero 已提交
499
				results: mapResourceToResult.values()
E
Erich Gamma 已提交
500 501 502 503
			};
		});
	}

504 505 506
	private getFileModels(resources?: URI[]): ITextFileEditorModel[];
	private getFileModels(resource?: URI): ITextFileEditorModel[];
	private getFileModels(arg1?: any): ITextFileEditorModel[] {
E
Erich Gamma 已提交
507
		if (Array.isArray(arg1)) {
508
			const models: ITextFileEditorModel[] = [];
509
			(<URI[]>arg1).forEach(resource => {
E
Erich Gamma 已提交
510 511 512 513 514 515
				models.push(...this.getFileModels(resource));
			});

			return models;
		}

516
		return this._models.getAll(<URI>arg1);
E
Erich Gamma 已提交
517 518
	}

519 520 521
	private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[];
	private getDirtyFileModels(resource?: URI): ITextFileEditorModel[];
	private getDirtyFileModels(arg1?: any): ITextFileEditorModel[] {
522
		return this.getFileModels(arg1).filter(model => model.isDirty());
E
Erich Gamma 已提交
523 524
	}

B
Benjamin Pasero 已提交
525
	saveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
526 527

		// Get to target resource
528 529 530 531
		let targetPromise: TPromise<URI>;
		if (target) {
			targetPromise = TPromise.wrap(target);
		} else {
532
			let dialogPath = resource.fsPath;
533
			if (resource.scheme === Schemas.untitled) {
534 535 536
				dialogPath = this.suggestFileName(resource);
			}

B
Benjamin Pasero 已提交
537
			targetPromise = this.promptForPath(resource, dialogPath).then(pathRaw => {
538 539 540
				if (pathRaw) {
					return URI.file(pathRaw);
				}
541

542 543
				return void 0;
			});
544 545
		}

546 547 548 549
		return targetPromise.then(target => {
			if (!target) {
				return TPromise.as(null); // user canceled
			}
550

551 552 553 554 555 556 557 558
			// 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);
		});
559 560
	}

561
	private doSaveAs(resource: URI, target?: URI, options?: ISaveOptions): TPromise<URI> {
562 563

		// Retrieve text model from provided resource if any
564
		let modelPromise: TPromise<ITextFileEditorModel | UntitledEditorModel> = TPromise.as(null);
565
		if (this.fileService.canHandleResource(resource)) {
566
			modelPromise = TPromise.as(this._models.get(resource));
567
		} else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) {
568
			modelPromise = this.untitledEditorService.loadOrCreate({ resource });
569 570
		}

R
Ron Buckton 已提交
571
		return modelPromise.then<any>(model => {
572 573 574

			// 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) {
575
				return this.doSaveTextFileAs(model, resource, target, options);
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
			}

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

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

594 595 596 597 598
		// 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);
		}
599

600 601 602 603 604 605
		// 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);
			});
		}
606

607
		return targetModelResolver.then(targetModel => {
608

609 610
			// take over encoding and model value from source model
			targetModel.updatePreferredEncoding(sourceModel.getEncoding());
B
Benjamin Pasero 已提交
611
			this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
612

613
			// save model
614
			return targetModel.save(options);
615
		}, error => {
616

617
			// binary model: delete the file and run the operation again
618
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
619
				return this.fileService.del(target).then(() => this.doSaveTextFileAs(sourceModel, resource, target, options));
620 621 622
			}

			return TPromise.wrapError(error);
623 624 625 626
		});
	}

	private suggestFileName(untitledResource: URI): string {
627 628
		const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);

629
		const lastActiveFile = this.historyService.getLastActiveFile(Schemas.file);
630 631
		if (lastActiveFile) {
			return URI.file(paths.join(paths.dirname(lastActiveFile.fsPath), untitledFileName)).fsPath;
632 633
		}

634
		const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(Schemas.file);
635 636 637 638
		if (lastActiveFolder) {
			return URI.file(paths.join(lastActiveFolder.fsPath, untitledFileName)).fsPath;
		}

639
		return untitledFileName;
640
	}
E
Erich Gamma 已提交
641

B
Benjamin Pasero 已提交
642
	revert(resource: URI, options?: IRevertOptions): TPromise<boolean> {
643
		return this.revertAll([resource], options).then(result => result.results.length === 1 && result.results[0].success);
E
Erich Gamma 已提交
644 645
	}

B
Benjamin Pasero 已提交
646
	revertAll(resources?: URI[], options?: IRevertOptions): TPromise<ITextFileOperationResult> {
647 648

		// Revert files first
649
		return this.doRevertAllFiles(resources, options).then(operation => {
650 651 652 653 654 655 656 657 658

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

			return operation;
		});
	}

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

B
Benjamin Pasero 已提交
662
		const mapResourceToResult = new ResourceMap<IResult>();
663
		fileModels.forEach(m => {
B
Benjamin Pasero 已提交
664
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
665
				source: m.getResource()
B
Benjamin Pasero 已提交
666
			});
E
Erich Gamma 已提交
667 668
		});

669
		return TPromise.join(fileModels.map(model => {
670
			return model.revert(options && options.soft).then(() => {
E
Erich Gamma 已提交
671
				if (!model.isDirty()) {
B
Benjamin Pasero 已提交
672
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
673
				}
674
			}, error => {
E
Erich Gamma 已提交
675

676
				// FileNotFound means the file got deleted meanwhile, so still record as successful revert
677
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
678
					mapResourceToResult.get(model.getResource()).success = true;
E
Erich Gamma 已提交
679 680 681 682
				}

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

686
				return void 0;
E
Erich Gamma 已提交
687
			});
688
		})).then(r => {
689
			return {
B
Benjamin Pasero 已提交
690
				results: mapResourceToResult.values()
691
			};
E
Erich Gamma 已提交
692 693 694
		});
	}

B
Benjamin Pasero 已提交
695
	create(resource: URI, contents?: string, options?: { overwrite?: boolean }): TPromise<void> {
B
Benjamin Pasero 已提交
696 697
		const existingModel = this.models.get(resource);

B
Benjamin Pasero 已提交
698 699 700 701
		return this.fileService.createFile(resource, contents, options).then(() => {

			// If we had an existing model for the given resource, load
			// it again to make sure it is up to date with the contents
B
Benjamin Pasero 已提交
702 703 704 705
			// we just wrote into the underlying resource by calling
			// revert()
			if (existingModel && !existingModel.isDisposed()) {
				return existingModel.revert();
B
Benjamin Pasero 已提交
706 707 708 709 710 711
			}

			return void 0;
		});
	}

B
Benjamin Pasero 已提交
712
	delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): TPromise<void> {
B
Benjamin Pasero 已提交
713 714
		const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */));

715
		return this.revertAll(dirtyFiles, { soft: true }).then(() => this.fileService.del(resource, options));
B
Benjamin Pasero 已提交
716 717
	}

B
Benjamin Pasero 已提交
718
	move(source: URI, target: URI, overwrite?: boolean): TPromise<void> {
B
Benjamin Pasero 已提交
719

720 721 722 723
		const waitForPromises: TPromise[] = [];
		this._onWillMove.fire({
			oldResource: source,
			newResource: target,
724
			waitUntil(p: Thenable<any>) {
725 726 727
				waitForPromises.push(TPromise.wrap(p).then(undefined, errors.onUnexpectedError));
			}
		});
B
Benjamin Pasero 已提交
728

729 730
		// prevent async waitUntil-calls
		Object.freeze(waitForPromises);
B
Benjamin Pasero 已提交
731

732
		return TPromise.join(waitForPromises).then(() => {
B
Benjamin Pasero 已提交
733

734 735
			// Handle target models if existing (if target URI is a folder, this can be multiple)
			let handleTargetModelPromise: TPromise<any> = TPromise.as(void 0);
B
Benjamin Pasero 已提交
736
			const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */));
737 738 739
			if (dirtyTargetModels.length) {
				handleTargetModelPromise = this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true });
			}
B
Benjamin Pasero 已提交
740

741
			return handleTargetModelPromise.then(() => {
B
Benjamin Pasero 已提交
742

743 744 745 746 747 748 749 750
				// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
				let handleDirtySourceModels: TPromise<any>;
				const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
				const dirtyTargetModels: URI[] = [];
				if (dirtySourceModels.length) {
					handleDirtySourceModels = TPromise.join(dirtySourceModels.map(sourceModel => {
						const sourceModelResource = sourceModel.getResource();
						let targetModelResource: URI;
B
Benjamin Pasero 已提交
751

752 753 754 755 756 757 758 759 760 761
						// If the source is the actual model, just use target as new resource
						if (isEqual(sourceModelResource, source, !platform.isLinux /* ignorecase */)) {
							targetModelResource = target;
						}

						// Otherwise a parent folder of the source is being moved, so we need
						// to compute the target resource based on that
						else {
							targetModelResource = sourceModelResource.with({ path: paths.join(target.path, sourceModelResource.path.substr(source.path.length + 1)) });
						}
B
Benjamin Pasero 已提交
762

763 764
						// Remember as dirty target model to load after the operation
						dirtyTargetModels.push(targetModelResource);
B
Benjamin Pasero 已提交
765

766 767 768 769 770 771 772 773
						// Backup dirty source model to the target resource it will become later
						return this.backupFileService.backupResource(targetModelResource, sourceModel.createSnapshot(), sourceModel.getVersionId());
					}));
				} else {
					handleDirtySourceModels = TPromise.as(void 0);
				}

				return handleDirtySourceModels.then(() => {
B
Benjamin Pasero 已提交
774

775 776
					// Soft revert the dirty source files if any
					return this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true }).then(() => {
B
Benjamin Pasero 已提交
777

778 779
						// Rename to target
						return this.fileService.moveFile(source, target, overwrite).then(() => {
B
Benjamin Pasero 已提交
780

781 782 783 784 785 786 787 788
							// Load models that were dirty before
							return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel))).then(() => void 0);
						}, error => {

							// In case of an error, discard any dirty target backups that were made
							return TPromise.join(dirtyTargetModels.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel)))
								.then(() => TPromise.wrapError(error));
						});
B
Benjamin Pasero 已提交
789 790 791 792 793 794
					});
				});
			});
		});
	}

B
Benjamin Pasero 已提交
795
	getAutoSaveMode(): AutoSaveMode {
796 797 798 799
		if (this.configuredAutoSaveOnFocusChange) {
			return AutoSaveMode.ON_FOCUS_CHANGE;
		}

800 801 802 803
		if (this.configuredAutoSaveOnWindowChange) {
			return AutoSaveMode.ON_WINDOW_CHANGE;
		}

804
		if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
805
			return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
806 807 808
		}

		return AutoSaveMode.OFF;
809 810
	}

B
Benjamin Pasero 已提交
811
	getAutoSaveConfiguration(): IAutoSaveConfiguration {
812
		return {
813
			autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : void 0,
814 815
			autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
			autoSaveApplicationChange: this.configuredAutoSaveOnWindowChange
B
Benjamin Pasero 已提交
816
		};
817 818
	}

B
Benjamin Pasero 已提交
819
	get isHotExitEnabled(): boolean {
820
		return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
821 822
	}

B
Benjamin Pasero 已提交
823
	dispose(): void {
E
Erich Gamma 已提交
824 825

		// Clear all caches
826
		this._models.clear();
B
Benjamin Pasero 已提交
827 828

		super.dispose();
E
Erich Gamma 已提交
829
	}
J
Johannes Rieken 已提交
830
}