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

B
Benjamin Pasero 已提交
6
import * as nls from 'vs/nls';
7
import { URI } from 'vs/base/common/uri';
8 9
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
M
Matt Bierner 已提交
10
import { Event, Emitter } from 'vs/base/common/event';
11
import * as platform from 'vs/base/common/platform';
12
import { IWindowsService } from 'vs/platform/windows/common/windows';
13
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
14
import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent } from 'vs/workbench/services/textfile/common/textfiles';
15
import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
16
import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
17
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
18
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, toBufferOrReadable, ICreateFileOptions } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
19
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
B
Benjamin Pasero 已提交
20
import { Disposable } from 'vs/base/common/lifecycle';
21
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
22
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
J
Johannes Rieken 已提交
23 24
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
25
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
B
Benjamin Pasero 已提交
26
import { ResourceMap } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
27 28
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
I
isidor 已提交
29
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
30
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
B
Benjamin Pasero 已提交
31
import { IModelService } from 'vs/editor/common/services/modelService';
32
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
B
Benjamin Pasero 已提交
33
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
34
import { posix } from 'vs/base/common/path';
35 36 37 38 39
import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { coalesce } from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
E
Erich Gamma 已提交
40 41 42 43

/**
 * The workbench file service implementation implements the raw file service spec and adds additional methods on top.
 */
44
export class TextFileService extends Disposable implements ITextFileService {
45

46
	_serviceBrand: ServiceIdentifier<any>;
E
Erich Gamma 已提交
47

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

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

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

B
Benjamin Pasero 已提交
57
	private _models: TextFileEditorModelManager;
B
Benjamin Pasero 已提交
58 59
	get models(): ITextFileEditorModelManager { return this._models; }

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

E
Erich Gamma 已提交
67
	constructor(
68 69 70 71
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IFileService protected readonly fileService: IFileService,
		@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
		@ILifecycleService private readonly lifecycleService: ILifecycleService,
72
		@IInstantiationService protected instantiationService: IInstantiationService,
73 74 75
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IModeService private readonly modeService: IModeService,
		@IModelService private readonly modelService: IModelService,
76
		@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
77 78 79 80 81 82 83 84
		@INotificationService private readonly notificationService: INotificationService,
		@IBackupFileService private readonly backupFileService: IBackupFileService,
		@IWindowsService private readonly windowsService: IWindowsService,
		@IHistoryService private readonly historyService: IHistoryService,
		@IContextKeyService contextKeyService: IContextKeyService,
		@IDialogService private readonly dialogService: IDialogService,
		@IFileDialogService private readonly fileDialogService: IFileDialogService,
		@IEditorService private readonly editorService: IEditorService
E
Erich Gamma 已提交
85
	) {
B
Benjamin Pasero 已提交
86
		super();
87

B
Benjamin Pasero 已提交
88
		this._models = this._register(instantiationService.createInstance(TextFileEditorModelManager));
I
isidor 已提交
89
		this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
90

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

94
		this.onFilesConfigurationChange(configuration);
95

96
		this.registerListeners();
E
Erich Gamma 已提交
97 98
	}

B
Benjamin Pasero 已提交
99
	//#region event handling
B
Benjamin Pasero 已提交
100

101
	private registerListeners(): void {
102

103
		// Lifecycle
104
		this.lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(event.reason)));
105 106
		this.lifecycleService.onShutdown(this.dispose, this);

107
		// Files configuration changes
B
Benjamin Pasero 已提交
108
		this._register(this.configurationService.onDidChangeConfiguration(e => {
109
			if (e.affectsConfiguration('files')) {
110
				this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
111 112
			}
		}));
113 114
	}

J
Johannes Rieken 已提交
115
	private beforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
B
Benjamin Pasero 已提交
116

117 118 119 120
		// Dirty files need treatment on shutdown
		const dirty = this.getDirty();
		if (dirty.length) {

121
			// If auto save is enabled, save all files and then check again for dirty files
122
			// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
123
			if (this.getAutoSaveMode() !== AutoSaveMode.OFF) {
B
Benjamin Pasero 已提交
124
				return this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => {
B
Benjamin Pasero 已提交
125

B
Benjamin Pasero 已提交
126 127 128 129 130
					// If we still have dirty files, we either have untitled ones or files that cannot be saved
					const remainingDirty = this.getDirty();
					if (remainingDirty.length) {
						return this.handleDirtyBeforeShutdown(remainingDirty, reason);
					}
131

M
Matt Bierner 已提交
132
					return false;
B
Benjamin Pasero 已提交
133 134
				});
			}
B
Benjamin Pasero 已提交
135

B
Benjamin Pasero 已提交
136 137 138
			// Auto save is not enabled
			return this.handleDirtyBeforeShutdown(dirty, reason);
		}
139

B
Benjamin Pasero 已提交
140 141 142
		// No dirty files: no veto
		return this.noVeto({ cleanUpBackups: true });
	}
143

J
Johannes Rieken 已提交
144
	private handleDirtyBeforeShutdown(dirty: URI[], reason: ShutdownReason): boolean | Promise<boolean> {
145

B
Benjamin Pasero 已提交
146 147
		// If hot exit is enabled, backup dirty files and allow to exit without confirmation
		if (this.isHotExitEnabled) {
B
Benjamin Pasero 已提交
148 149
			return this.backupBeforeShutdown(dirty, this.models, reason).then(didBackup => {
				if (didBackup) {
B
Benjamin Pasero 已提交
150
					return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
151
				}
152

B
Benjamin Pasero 已提交
153 154 155 156 157 158 159
				// since a backup did not happen, we have to confirm for the dirty files now
				return this.confirmBeforeShutdown();
			}, errors => {
				const firstError = errors[0];
				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));

				return true; // veto, the backups failed
160
			});
161 162
		}

B
Benjamin Pasero 已提交
163 164
		// Otherwise just confirm from the user what to do with the dirty files
		return this.confirmBeforeShutdown();
165 166
	}

B
Benjamin Pasero 已提交
167
	private async backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise<boolean> {
168
		const windowCount = await this.windowsService.getWindowCount();
169

170 171 172 173
		// 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.
174

175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
		let doBackup: boolean | undefined;
		switch (reason) {
			case ShutdownReason.CLOSE:
				if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
					doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
				} else if (windowCount > 1 || platform.isMacintosh) {
					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;
190

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

195 196 197 198 199 200 201 202
			case ShutdownReason.LOAD:
				if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
					doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
				} else {
					doBackup = false; // do not backup because we are switching contexts
				}
				break;
		}
203

204
		if (!doBackup) {
B
Benjamin Pasero 已提交
205
			return false;
206
		}
207

208
		await this.backupAll(dirtyToBackup, textFileEditorModelManager);
209

B
Benjamin Pasero 已提交
210
		return true;
211 212
	}

J
Johannes Rieken 已提交
213
	private backupAll(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager): Promise<void> {
214 215 216 217 218

		// split up between files and untitled
		const filesToBackup: ITextFileEditorModel[] = [];
		const untitledToBackup: URI[] = [];
		dirtyToBackup.forEach(s => {
219
			if (this.fileService.canHandleResource(s)) {
M
Matt Bierner 已提交
220 221 222 223
				const model = textFileEditorModelManager.get(s);
				if (model) {
					filesToBackup.push(model);
				}
224
			} else if (s.scheme === Schemas.untitled) {
225 226 227 228 229 230 231
				untitledToBackup.push(s);
			}
		});

		return this.doBackupAll(filesToBackup, untitledToBackup);
	}

232 233 234 235
	private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise<void> {

		// Handle file resources first
		await Promise.all(dirtyFileModels.map(async model => {
M
Matt Bierner 已提交
236 237
			const snapshot = model.createSnapshot();
			if (snapshot) {
238
				await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
M
Matt Bierner 已提交
239
			}
240
		}));
241

242 243 244 245
		// Handle untitled resources
		const untitledModelPromises = untitledResources
			.filter(untitled => this.untitledEditorService.exists(untitled))
			.map(untitled => this.untitledEditorService.loadOrCreate({ resource: untitled }));
246

247 248 249 250 251 252 253 254
		const untitledModels = await Promise.all(untitledModelPromises);

		await Promise.all(untitledModels.map(async model => {
			const snapshot = model.createSnapshot();
			if (snapshot) {
				await this.backupFileService.backupResource(model.getResource(), snapshot, model.getVersionId());
			}
		}));
255 256
	}

J
Johannes Rieken 已提交
257
	private confirmBeforeShutdown(): boolean | Promise<boolean> {
258
		return this.confirmSave().then(confirm => {
259

260 261 262 263 264 265
			// 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
					}
266

267 268 269
					return this.noVeto({ cleanUpBackups: true });
				});
			}
270

271 272
			// Don't Save
			else if (confirm === ConfirmResult.DONT_SAVE) {
273

274 275 276
				// Make sure to revert untitled so that they do not restore
				// see https://github.com/Microsoft/vscode/issues/29572
				this.untitledEditorService.revertAll();
277

278 279
				return this.noVeto({ cleanUpBackups: true });
			}
280

281 282 283 284
			// Cancel
			else if (confirm === ConfirmResult.CANCEL) {
				return true; // veto
			}
285

M
Matt Bierner 已提交
286
			return false;
287
		});
288 289
	}

J
Johannes Rieken 已提交
290
	private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise<boolean> {
B
Benjamin Pasero 已提交
291
		if (!options.cleanUpBackups) {
B
Benjamin Pasero 已提交
292 293 294
			return false;
		}

295 296 297 298
		if (this.lifecycleService.phase < LifecyclePhase.Restored) {
			return false; // if editors have not restored, we are not up to speed with backups and thus should not clean them
		}

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

302
	protected async cleanupBackupsBeforeShutdown(): Promise<void> {
303
		if (this.environmentService.isExtensionDevelopment) {
304
			return;
305 306
		}

307
		await 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;
I
isidor 已提交
314
		this.autoSaveContext.set(autoSaveMode);
315
		switch (autoSaveMode) {
316
			case AutoSaveConfiguration.AFTER_DELAY:
317 318
				this.configuredAutoSaveDelay = configuration && configuration.files && configuration.files.autoSaveDelay;
				this.configuredAutoSaveOnFocusChange = false;
319
				this.configuredAutoSaveOnWindowChange = false;
320 321
				break;

322
			case AutoSaveConfiguration.ON_FOCUS_CHANGE:
R
Rob Lourens 已提交
323
				this.configuredAutoSaveDelay = undefined;
324
				this.configuredAutoSaveOnFocusChange = true;
325 326 327 328
				this.configuredAutoSaveOnWindowChange = false;
				break;

			case AutoSaveConfiguration.ON_WINDOW_CHANGE:
R
Rob Lourens 已提交
329
				this.configuredAutoSaveDelay = undefined;
330 331
				this.configuredAutoSaveOnFocusChange = false;
				this.configuredAutoSaveOnWindowChange = true;
332 333 334
				break;

			default:
R
Rob Lourens 已提交
335
				this.configuredAutoSaveDelay = undefined;
336
				this.configuredAutoSaveOnFocusChange = false;
337
				this.configuredAutoSaveOnWindowChange = false;
338 339
				break;
		}
340

341 342
		// Emit as event
		this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
343

344
		// save all dirty when enabling auto save
345
		if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) {
346
			this.saveAll();
347
		}
348 349 350 351 352 353 354

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

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

B
Benjamin Pasero 已提交
365
	//#endregion
366

B
Benjamin Pasero 已提交
367
	//#region primitives (resolve, create, move, delete, update)
368

B
Benjamin Pasero 已提交
369 370 371
	async resolve(resource: URI, options?: IResolveContentOptions): Promise<IRawTextContent> {
		const streamContent = await this.fileService.resolveStreamContent(resource, options);
		const value = await createTextBufferFactoryFromStream(streamContent.value);
372

B
Benjamin Pasero 已提交
373 374 375 376 377 378 379 380 381 382
		return {
			resource: streamContent.resource,
			name: streamContent.name,
			mtime: streamContent.mtime,
			etag: streamContent.etag,
			encoding: streamContent.encoding,
			isReadonly: streamContent.isReadonly,
			size: streamContent.size,
			value
		};
E
Erich Gamma 已提交
383 384
	}

385 386
	async create(resource: URI, value?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
		const stat = await this.doCreate(resource, value, options);
B
Benjamin Pasero 已提交
387 388 389 390 391

		// If we had an existing model for the given resource, load
		// it again to make sure it is up to date with the contents
		// we just wrote into the underlying resource by calling
		// revert()
392
		const existingModel = this.models.get(resource);
B
Benjamin Pasero 已提交
393 394
		if (existingModel && !existingModel.isDisposed()) {
			await existingModel.revert();
395 396
		}

B
Benjamin Pasero 已提交
397 398 399
		return stat;
	}

400 401 402
	protected doCreate(resource: URI, value?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.createFile(resource, toBufferOrReadable(value), options);
	}
B
Benjamin Pasero 已提交
403

404 405
	async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.writeFile(resource, toBufferOrReadable(value), options);
B
Benjamin Pasero 已提交
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 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
	}

	async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
		const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource, !platform.isLinux /* ignorecase */));

		await this.revertAll(dirtyFiles, { soft: true });

		return this.fileService.del(resource, options);
	}

	async move(source: URI, target: URI, overwrite?: boolean): Promise<void> {
		const waitForPromises: Promise<unknown>[] = [];

		// Event
		this._onWillMove.fire({
			oldResource: source,
			newResource: target,
			waitUntil(promise: Promise<unknown>) {
				waitForPromises.push(promise.then(undefined, errors.onUnexpectedError));
			}
		});

		// prevent async waitUntil-calls
		Object.freeze(waitForPromises);

		await Promise.all(waitForPromises);

		// Handle target models if existing (if target URI is a folder, this can be multiple)
		const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */));
		if (dirtyTargetModels.length) {
			await this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true });
		}

		// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
		const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
		const dirtyTargetModelUris: URI[] = [];
		if (dirtySourceModels.length) {
			await Promise.all(dirtySourceModels.map(async sourceModel => {
				const sourceModelResource = sourceModel.getResource();
				let targetModelResource: URI;

				// 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: joinPath(target, sourceModelResource.path.substr(source.path.length + 1)).path });
				}

				// Remember as dirty target model to load after the operation
				dirtyTargetModelUris.push(targetModelResource);

				// Backup dirty source model to the target resource it will become later
				const snapshot = sourceModel.createSnapshot();
				if (snapshot) {
					await this.backupFileService.backupResource(targetModelResource, snapshot, sourceModel.getVersionId());
				}
			}));
		}


		// Soft revert the dirty source files if any
		await this.revertAll(dirtySourceModels.map(dirtySourceModel => dirtySourceModel.getResource()), { soft: true });

		// Rename to target
		try {
			await this.fileService.move(source, target, overwrite);

			// Load models that were dirty before
			await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.models.loadOrCreate(dirtyTargetModel)));
		} catch (error) {

			// In case of an error, discard any dirty target backups that were made
			await Promise.all(dirtyTargetModelUris.map(dirtyTargetModel => this.backupFileService.discardResourceBackup(dirtyTargetModel)));

			throw error;
		}
E
Erich Gamma 已提交
486 487
	}

B
Benjamin Pasero 已提交
488 489 490 491
	//#endregion

	//#region save/revert

492
	async save(resource: URI, options?: ISaveOptions): Promise<boolean> {
493

494
		// Run a forced save if we detect the file is not dirty so that save participants can still run
495
		if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) {
496 497
			const model = this._models.get(resource);
			if (model) {
B
Benjamin Pasero 已提交
498 499
				options.reason = SaveReason.EXPLICIT;

500 501 502
				await model.save(options);

				return !model.isDirty();
503
			}
504 505
		}

506 507 508
		const result = await this.saveAll([resource], options);

		return result.results.length === 1 && !!result.results[0].success;
E
Erich Gamma 已提交
509 510
	}

B
Benjamin Pasero 已提交
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
	async confirmSave(resources?: URI[]): Promise<ConfirmResult> {
		if (this.environmentService.isExtensionDevelopment) {
			return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assum we run interactive (e.g. tests)
		}

		const resourcesToConfirm = this.getDirty(resources);
		if (resourcesToConfirm.length === 0) {
			return ConfirmResult.DONT_SAVE;
		}

		const message = resourcesToConfirm.length === 1 ? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0]))
			: getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm);

		const buttons: string[] = [
			resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
			nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
			nls.localize('cancel', "Cancel")
		];

		const index = await this.dialogService.show(Severity.Warning, message, buttons, {
			cancelId: 2,
			detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
		});

		switch (index) {
			case 0: return ConfirmResult.SAVE;
			case 1: return ConfirmResult.DONT_SAVE;
			default: return ConfirmResult.CANCEL;
		}
	}

	async confirmOverwrite(resource: URI): Promise<boolean> {
		const confirm: IConfirmation = {
			message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)),
			detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))),
			primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
			type: 'warning'
		};

		return (await this.dialogService.confirm(confirm)).confirmed;
	}

J
Johannes Rieken 已提交
553 554
	saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise<ITextFileOperationResult>;
	saveAll(resources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult>;
555
	saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
556 557 558 559 560 561 562 563 564 565 566 567 568

		// 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 => {
569
			if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === Schemas.untitled) {
570
				untitledToSave.push(s);
J
Johannes Rieken 已提交
571 572
			} else {
				filesToSave.push(s);
573 574 575
			}
		});

576
		return this.doSaveAll(filesToSave, untitledToSave, options);
577 578
	}

579
	private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
580 581

		// Handle files first that can just be saved
582
		const result = await this.doSaveAllFiles(fileResources, options);
583

584 585 586 587 588
		// Preflight for untitled to handle cancellation from the dialog
		const targetsForUntitled: URI[] = [];
		for (const untitled of untitledResources) {
			if (this.untitledEditorService.exists(untitled)) {
				let targetUri: URI;
589

590 591 592 593
				// Untitled with associated file path don't need to prompt
				if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
					targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority);
				}
594

595 596 597 598 599
				// Otherwise ask user
				else {
					const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled));
					if (!targetPath) {
						return { results: [...fileResources, ...untitledResources].map(r => ({ source: r })) };
600 601
					}

602
					targetUri = targetPath;
603
				}
604 605

				targetsForUntitled.push(targetUri);
606
			}
607
		}
608

609 610 611
		// Handle untitled
		await Promise.all(targetsForUntitled.map(async (target, index) => {
			const uri = await this.saveAs(untitledResources[index], target);
612

613 614 615 616
			result.results.push({
				source: untitledResources[index],
				target: uri,
				success: !!uri
617
			});
618
		}));
619

620
		return result;
621 622
	}

B
Benjamin Pasero 已提交
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
	protected async promptForPath(resource: URI, defaultUri: URI): Promise<URI | undefined> {

		// Help user to find a name for the file by opening it first
		await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true, } });

		return this.fileDialogService.showSaveDialog(this.getSaveDialogOptions(defaultUri));
	}

	private getSaveDialogOptions(defaultUri: URI): ISaveDialogOptions {
		const options: ISaveDialogOptions = {
			defaultUri,
			title: nls.localize('saveAsTitle', "Save As")
		};

		// Filters are only enabled on Windows where they work properly
		if (!platform.isWindows) {
			return options;
		}

		interface IFilter { name: string; extensions: string[]; }

		// Build the file filter by using our known languages
		const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined;
		let matchingFilter: IFilter | undefined;
		const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => {
			const extensions = this.modeService.getExtensions(languageName);
			if (!extensions || !extensions.length) {
				return null;
			}

			const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) };

			if (ext && extensions.indexOf(ext) >= 0) {
				matchingFilter = filter;

				return null; // matching filter will be added last to the top
			}

			return filter;
		}));

		// Filters are a bit weird on Windows, based on having a match or not:
		// Match: we put the matching filter first so that it shows up selected and the all files last
		// No match: we put the all files filter first
		const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
		if (matchingFilter) {
			filters.unshift(matchingFilter);
			filters.unshift(allFilesFilter);
		} else {
			filters.unshift(allFilesFilter);
		}

		// Allow to save file without extension
		filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });

		options.filters = filters;

		return options;
	}

683
	private async doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
R
Rob Lourens 已提交
684
		const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */)
685
			.filter(model => {
686 687
				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
688 689 690 691
				}

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

B
Benjamin Pasero 已提交
693
		const mapResourceToResult = new ResourceMap<IResult>();
694
		dirtyFileModels.forEach(m => {
B
Benjamin Pasero 已提交
695
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
696
				source: m.getResource()
B
Benjamin Pasero 已提交
697
			});
E
Erich Gamma 已提交
698 699
		});

700 701 702 703 704 705 706
		await Promise.all(dirtyFileModels.map(async model => {
			await model.save(options);

			if (!model.isDirty()) {
				const result = mapResourceToResult.get(model.getResource());
				if (result) {
					result.success = true;
E
Erich Gamma 已提交
707
				}
708 709 710 711
			}
		}));

		return { results: mapResourceToResult.values() };
E
Erich Gamma 已提交
712 713
	}

714
	private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
E
Erich Gamma 已提交
715
		if (Array.isArray(arg1)) {
716
			const models: ITextFileEditorModel[] = [];
717
			(<URI[]>arg1).forEach(resource => {
E
Erich Gamma 已提交
718 719 720 721 722 723
				models.push(...this.getFileModels(resource));
			});

			return models;
		}

724
		return this._models.getAll(<URI>arg1);
E
Erich Gamma 已提交
725 726
	}

727 728
	private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
		return this.getFileModels(resources).filter(model => model.isDirty());
E
Erich Gamma 已提交
729 730
	}

731
	async saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise<URI | undefined> {
732 733

		// Get to target resource
734
		if (!targetResource) {
M
Martin Aeschlimann 已提交
735
			let dialogPath = resource;
736
			if (resource.scheme === Schemas.untitled) {
737 738 739
				dialogPath = this.suggestFileName(resource);
			}

740
			targetResource = await this.promptForPath(resource, dialogPath);
741 742
		}

743 744 745
		if (!targetResource) {
			return; // user canceled
		}
746

747 748 749
		// Just save if target is same as models own resource
		if (resource.toString() === targetResource.toString()) {
			await this.save(resource, options);
750

751 752 753 754 755
			return resource;
		}

		// Do it
		return this.doSaveAs(resource, targetResource, options);
756 757
	}

758
	private async doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise<URI> {
759 760

		// Retrieve text model from provided resource if any
761
		let model: ITextFileEditorModel | UntitledEditorModel | undefined;
762
		if (this.fileService.canHandleResource(resource)) {
763
			model = this._models.get(resource);
764
		} else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) {
765
			model = await this.untitledEditorService.loadOrCreate({ resource });
766 767
		}

768 769 770 771 772
		// 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)
		let result: boolean;
		if (model) {
			result = await this.doSaveTextFileAs(model, resource, target, options);
		}
773

774 775 776
		// Otherwise we can only copy
		else {
			await this.fileService.copy(resource, target);
777

778 779
			result = true;
		}
B
Benjamin Pasero 已提交
780

781 782 783 784
		// Return early if the operation was not running
		if (!result) {
			return target;
		}
785

786 787
		// Revert the source
		await this.revert(resource);
788

789
		return target;
790 791
	}

792
	private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise<boolean> {
793

794
		// Prefer an existing model if it is already loaded for the given target resource
795 796
		let targetExists: boolean = false;
		let targetModel = this.models.get(target);
797
		if (targetModel && targetModel.isResolved()) {
B
Benjamin Pasero 已提交
798
			targetExists = true;
799
		}
800

801 802
		// Otherwise create the target file empty if it does not exist already and resolve it from there
		else {
803
			targetExists = await this.fileService.exists(target);
B
Benjamin Pasero 已提交
804

805 806
			// create target model adhoc if file does not exist yet
			if (!targetExists) {
B
Benjamin Pasero 已提交
807
				await this.create(target, '');
808
			}
B
Benjamin Pasero 已提交
809

810
			targetModel = await this.models.loadOrCreate(target);
811
		}
812

813
		try {
814

B
Benjamin Pasero 已提交
815 816 817 818
			// Confirm to overwrite if we have an untitled file with associated file where
			// the file actually exists on disk and we are instructed to save to that file
			// path. This can happen if the file was created after the untitled file was opened.
			// See https://github.com/Microsoft/vscode/issues/67946
819
			let write: boolean;
B
Benjamin Pasero 已提交
820
			if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.getResource(), this.environmentService.configuration.remoteAuthority))) {
821
				write = await this.confirmOverwrite(target);
B
Benjamin Pasero 已提交
822
			} else {
823
				write = true;
B
Benjamin Pasero 已提交
824 825
			}

826 827 828
			if (!write) {
				return false;
			}
B
Benjamin Pasero 已提交
829

830 831 832 833 834 835
			// take over encoding and model value from source model
			targetModel.updatePreferredEncoding(sourceModel.getEncoding());
			if (targetModel.textEditorModel) {
				const snapshot = sourceModel.createSnapshot();
				if (snapshot) {
					this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot));
M
Matt Bierner 已提交
836
				}
837
			}
838

839 840 841 842 843
			// save model
			await targetModel.save(options);

			return true;
		} catch (error) {
844

845
			// binary model: delete the file and run the operation again
846
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
847 848 849
				await this.fileService.del(target);

				return this.doSaveTextFileAs(sourceModel, resource, target, options);
850 851
			}

852 853
			throw error;
		}
854 855
	}

M
Martin Aeschlimann 已提交
856
	private suggestFileName(untitledResource: URI): URI {
857
		const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);
858
		const remoteAuthority = this.environmentService.configuration.remoteAuthority;
B
Benjamin Pasero 已提交
859
		const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
M
Martin Aeschlimann 已提交
860 861

		const lastActiveFile = this.historyService.getLastActiveFile(schemeFilter);
862
		if (lastActiveFile) {
863 864
			const lastDir = dirname(lastActiveFile);
			return joinPath(lastDir, untitledFileName);
865 866
		}

M
Martin Aeschlimann 已提交
867
		const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
868
		if (lastActiveFolder) {
M
Martin Aeschlimann 已提交
869
			return joinPath(lastActiveFolder, untitledFileName);
870 871
		}

872
		return schemeFilter === Schemas.file ? URI.file(untitledFileName) : URI.from({ scheme: schemeFilter, authority: remoteAuthority, path: posix.sep + untitledFileName });
873
	}
E
Erich Gamma 已提交
874

875 876 877 878
	async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {
		const result = await this.revertAll([resource], options);

		return result.results.length === 1 && !!result.results[0].success;
E
Erich Gamma 已提交
879 880
	}

881
	async revertAll(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
882 883

		// Revert files first
884
		const revertOperationResult = await this.doRevertAllFiles(resources, options);
885

886 887 888
		// Revert untitled
		const untitledReverted = this.untitledEditorService.revertAll(resources);
		untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled, success: true }));
889

890
		return revertOperationResult;
891 892
	}

893
	private async doRevertAllFiles(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
894
		const fileModels = options && options.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
E
Erich Gamma 已提交
895

B
Benjamin Pasero 已提交
896
		const mapResourceToResult = new ResourceMap<IResult>();
897
		fileModels.forEach(m => {
B
Benjamin Pasero 已提交
898
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
899
				source: m.getResource()
B
Benjamin Pasero 已提交
900
			});
E
Erich Gamma 已提交
901 902
		});

903 904 905 906
		await Promise.all(fileModels.map(async model => {
			try {
				await model.revert(options && options.soft);

E
Erich Gamma 已提交
907
				if (!model.isDirty()) {
M
Matt Bierner 已提交
908 909 910 911
					const result = mapResourceToResult.get(model.getResource());
					if (result) {
						result.success = true;
					}
E
Erich Gamma 已提交
912
				}
913
			} catch (error) {
E
Erich Gamma 已提交
914

915
				// FileNotFound means the file got deleted meanwhile, so still record as successful revert
916
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
M
Matt Bierner 已提交
917 918 919 920
					const result = mapResourceToResult.get(model.getResource());
					if (result) {
						result.success = true;
					}
E
Erich Gamma 已提交
921 922 923 924
				}

				// Otherwise bubble up the error
				else {
925
					throw error;
E
Erich Gamma 已提交
926
				}
927 928
			}
		}));
B
Benjamin Pasero 已提交
929

930
		return { results: mapResourceToResult.values() };
E
Erich Gamma 已提交
931 932
	}

B
Benjamin Pasero 已提交
933
	getDirty(resources?: URI[]): URI[] {
934

B
Benjamin Pasero 已提交
935 936
		// Collect files
		const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());
B
Benjamin Pasero 已提交
937

B
Benjamin Pasero 已提交
938 939
		// Add untitled ones
		dirty.push(...this.untitledEditorService.getDirty(resources));
940

B
Benjamin Pasero 已提交
941
		return dirty;
B
Benjamin Pasero 已提交
942 943
	}

B
Benjamin Pasero 已提交
944
	isDirty(resource?: URI): boolean {
B
Benjamin Pasero 已提交
945

B
Benjamin Pasero 已提交
946 947 948
		// Check for dirty file
		if (this._models.getAll(resource).some(model => model.isDirty())) {
			return true;
949
		}
B
Benjamin Pasero 已提交
950

B
Benjamin Pasero 已提交
951 952 953
		// Check for dirty untitled
		return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
	}
954

B
Benjamin Pasero 已提交
955
	//#endregion
956

B
Benjamin Pasero 已提交
957
	//#region config
B
Benjamin Pasero 已提交
958

B
Benjamin Pasero 已提交
959
	getAutoSaveMode(): AutoSaveMode {
960 961 962 963
		if (this.configuredAutoSaveOnFocusChange) {
			return AutoSaveMode.ON_FOCUS_CHANGE;
		}

964 965 966 967
		if (this.configuredAutoSaveOnWindowChange) {
			return AutoSaveMode.ON_WINDOW_CHANGE;
		}

968
		if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
969
			return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
970 971 972
		}

		return AutoSaveMode.OFF;
973 974
	}

B
Benjamin Pasero 已提交
975
	getAutoSaveConfiguration(): IAutoSaveConfiguration {
976
		return {
R
Rob Lourens 已提交
977
			autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : undefined,
978 979
			autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
			autoSaveApplicationChange: this.configuredAutoSaveOnWindowChange
B
Benjamin Pasero 已提交
980
		};
981 982
	}

B
Benjamin Pasero 已提交
983
	get isHotExitEnabled(): boolean {
984
		return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
985 986
	}

B
Benjamin Pasero 已提交
987 988
	//#endregion

B
Benjamin Pasero 已提交
989
	dispose(): void {
E
Erich Gamma 已提交
990 991

		// Clear all caches
992
		this._models.clear();
B
Benjamin Pasero 已提交
993 994

		super.dispose();
E
Erich Gamma 已提交
995
	}
996
}