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

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';
B
Benjamin Pasero 已提交
14
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileContent, 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';
B
Benjamin Pasero 已提交
18
import { IFileService, IReadTextFileOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, toBufferOrReadable, ICreateFileOptions, IResourceEncodings } 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';
40
import { VSBuffer } from 'vs/base/common/buffer';
E
Erich Gamma 已提交
41 42 43 44

/**
 * The workbench file service implementation implements the raw file service spec and adds additional methods on top.
 */
B
Benjamin Pasero 已提交
45
export abstract class TextFileService extends Disposable implements ITextFileService {
46

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

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

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

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

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

B
Benjamin Pasero 已提交
61
	abstract get encoding(): IResourceEncodings;
62

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

E
Erich Gamma 已提交
70
	constructor(
71 72 73 74
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IFileService protected readonly fileService: IFileService,
		@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
		@ILifecycleService private readonly lifecycleService: ILifecycleService,
75
		@IInstantiationService protected instantiationService: IInstantiationService,
76 77 78
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IModeService private readonly modeService: IModeService,
		@IModelService private readonly modelService: IModelService,
79
		@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
80 81 82 83 84 85 86 87
		@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 已提交
88
	) {
B
Benjamin Pasero 已提交
89
		super();
90

B
Benjamin Pasero 已提交
91
		this._models = this._register(instantiationService.createInstance(TextFileEditorModelManager));
I
isidor 已提交
92
		this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
93

94
		const configuration = configurationService.getValue<IFilesConfiguration>();
95 96
		this.currentFilesAssociationConfig = configuration && configuration.files && configuration.files.associations;

97
		this.onFilesConfigurationChange(configuration);
98

99
		this.registerListeners();
E
Erich Gamma 已提交
100 101
	}

B
Benjamin Pasero 已提交
102
	//#region event handling
B
Benjamin Pasero 已提交
103

104
	private registerListeners(): void {
105

106
		// Lifecycle
107
		this.lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(event.reason)));
108 109
		this.lifecycleService.onShutdown(this.dispose, this);

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

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

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

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

B
Benjamin Pasero 已提交
129 130 131 132 133
					// 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);
					}
134

M
Matt Bierner 已提交
135
					return false;
B
Benjamin Pasero 已提交
136 137
				});
			}
B
Benjamin Pasero 已提交
138

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

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

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

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

B
Benjamin Pasero 已提交
156 157 158 159 160 161 162
				// 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
163
			});
164 165
		}

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

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

173 174 175 176
		// 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.
177

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
		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;
193

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

198 199 200 201 202 203 204 205
			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;
		}
206

207
		if (!doBackup) {
B
Benjamin Pasero 已提交
208
			return false;
209
		}
210

211
		await this.backupAll(dirtyToBackup, textFileEditorModelManager);
212

B
Benjamin Pasero 已提交
213
		return true;
214 215
	}

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

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

		return this.doBackupAll(filesToBackup, untitledToBackup);
	}

235 236 237 238
	private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise<void> {

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

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

250 251 252 253 254 255 256 257
		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());
			}
		}));
258 259
	}

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

263 264 265 266 267 268
			// 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
					}
269

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

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

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

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

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

M
Matt Bierner 已提交
289
			return false;
290
		});
291 292
	}

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

298 299 300 301
		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
		}

302 303 304
		return this.cleanupBackupsBeforeShutdown().then(() => false, () => false);
	}

305
	protected async cleanupBackupsBeforeShutdown(): Promise<void> {
306
		if (this.environmentService.isExtensionDevelopment) {
307
			return;
308 309
		}

310
		await this.backupFileService.discardAllWorkspaceBackups();
B
Benjamin Pasero 已提交
311 312
	}

313
	protected onFilesConfigurationChange(configuration: IFilesConfiguration): void {
314
		const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF);
315

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

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

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

			default:
R
Rob Lourens 已提交
338
				this.configuredAutoSaveDelay = undefined;
339
				this.configuredAutoSaveOnFocusChange = false;
340
				this.configuredAutoSaveOnWindowChange = false;
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();
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
	}

B
Benjamin Pasero 已提交
368
	//#endregion
369

370
	//#region primitives (read, create, move, delete, update)
371

B
Benjamin Pasero 已提交
372
	async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
		const stream = await this.fileService.readFileStream(resource, options);

		// in case of acceptTextOnly: true, we check the first
		// chunk for possibly being binary by looking for 0-bytes
		let checkedForBinary = false;
		const throwOnBinary = (data: VSBuffer): Error | undefined => {
			if (!checkedForBinary) {
				checkedForBinary = true;

				for (let i = 0; i < data.byteLength && i < 512; i++) {
					if (data.readUint8(i) === 0) {
						throw new FileOperationError(nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), FileOperationResult.FILE_IS_BINARY, options);
					}
				}
			}

			return undefined;
		};

		return {
			...stream,
			encoding: 'utf8',
			value: await createTextBufferFactoryFromStream(stream.value, undefined, options && options.acceptTextOnly ? throwOnBinary : undefined)
		};
	}

	async legacyRead(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
B
Benjamin Pasero 已提交
400 401
		const streamContent = await this.fileService.resolveStreamContent(resource, options);
		const value = await createTextBufferFactoryFromStream(streamContent.value);
402

B
Benjamin Pasero 已提交
403 404 405 406 407 408 409 410 411 412
		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 已提交
413 414
	}

415
	async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
416
		const stat = await this.doCreate(resource, value, options);
B
Benjamin Pasero 已提交
417 418 419 420 421

		// 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()
422
		const existingModel = this.models.get(resource);
B
Benjamin Pasero 已提交
423 424
		if (existingModel && !existingModel.isDisposed()) {
			await existingModel.revert();
425 426
		}

B
Benjamin Pasero 已提交
427 428 429
		return stat;
	}

430
	protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
431 432
		return this.fileService.createFile(resource, toBufferOrReadable(value), options);
	}
B
Benjamin Pasero 已提交
433

434 435
	async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.writeFile(resource, toBufferOrReadable(value), options);
B
Benjamin Pasero 已提交
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 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515
	}

	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 已提交
516 517
	}

B
Benjamin Pasero 已提交
518 519 520 521
	//#endregion

	//#region save/revert

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

524
		// Run a forced save if we detect the file is not dirty so that save participants can still run
525
		if (options && options.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) {
526 527
			const model = this._models.get(resource);
			if (model) {
B
Benjamin Pasero 已提交
528 529
				options.reason = SaveReason.EXPLICIT;

530 531 532
				await model.save(options);

				return !model.isDirty();
533
			}
534 535
		}

536 537 538
		const result = await this.saveAll([resource], options);

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

B
Benjamin Pasero 已提交
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
	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 已提交
583 584
	saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise<ITextFileOperationResult>;
	saveAll(resources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult>;
585
	saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
586 587 588 589 590 591 592 593 594 595 596 597 598

		// 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 => {
599
			if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === Schemas.untitled) {
600
				untitledToSave.push(s);
J
Johannes Rieken 已提交
601 602
			} else {
				filesToSave.push(s);
603 604 605
			}
		});

606
		return this.doSaveAll(filesToSave, untitledToSave, options);
607 608
	}

609
	private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
610 611

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

614 615 616 617 618
		// 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;
619

620 621 622 623
				// Untitled with associated file path don't need to prompt
				if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
					targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority);
				}
624

625 626 627 628 629
				// Otherwise ask user
				else {
					const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled));
					if (!targetPath) {
						return { results: [...fileResources, ...untitledResources].map(r => ({ source: r })) };
630 631
					}

632
					targetUri = targetPath;
633
				}
634 635

				targetsForUntitled.push(targetUri);
636
			}
637
		}
638

639 640 641
		// Handle untitled
		await Promise.all(targetsForUntitled.map(async (target, index) => {
			const uri = await this.saveAs(untitledResources[index], target);
642

643 644 645 646
			result.results.push({
				source: untitledResources[index],
				target: uri,
				success: !!uri
647
			});
648
		}));
649

650
		return result;
651 652
	}

B
Benjamin Pasero 已提交
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
	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;
	}

713
	private async doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
R
Rob Lourens 已提交
714
		const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */)
715
			.filter(model => {
716 717
				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
718 719 720 721
				}

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

B
Benjamin Pasero 已提交
723
		const mapResourceToResult = new ResourceMap<IResult>();
724
		dirtyFileModels.forEach(m => {
B
Benjamin Pasero 已提交
725
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
726
				source: m.getResource()
B
Benjamin Pasero 已提交
727
			});
E
Erich Gamma 已提交
728 729
		});

730 731 732 733 734 735 736
		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 已提交
737
				}
738 739 740 741
			}
		}));

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

744
	private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
E
Erich Gamma 已提交
745
		if (Array.isArray(arg1)) {
746
			const models: ITextFileEditorModel[] = [];
747
			(<URI[]>arg1).forEach(resource => {
E
Erich Gamma 已提交
748 749 750 751 752 753
				models.push(...this.getFileModels(resource));
			});

			return models;
		}

754
		return this._models.getAll(<URI>arg1);
E
Erich Gamma 已提交
755 756
	}

757 758
	private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
		return this.getFileModels(resources).filter(model => model.isDirty());
E
Erich Gamma 已提交
759 760
	}

761
	async saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise<URI | undefined> {
762 763

		// Get to target resource
764
		if (!targetResource) {
M
Martin Aeschlimann 已提交
765
			let dialogPath = resource;
766
			if (resource.scheme === Schemas.untitled) {
767 768 769
				dialogPath = this.suggestFileName(resource);
			}

770
			targetResource = await this.promptForPath(resource, dialogPath);
771 772
		}

773 774 775
		if (!targetResource) {
			return; // user canceled
		}
776

777 778 779
		// Just save if target is same as models own resource
		if (resource.toString() === targetResource.toString()) {
			await this.save(resource, options);
780

781 782 783 784 785
			return resource;
		}

		// Do it
		return this.doSaveAs(resource, targetResource, options);
786 787
	}

788
	private async doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise<URI> {
789 790

		// Retrieve text model from provided resource if any
791
		let model: ITextFileEditorModel | UntitledEditorModel | undefined;
792
		if (this.fileService.canHandleResource(resource)) {
793
			model = this._models.get(resource);
794
		} else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) {
795
			model = await this.untitledEditorService.loadOrCreate({ resource });
796 797
		}

798 799 800 801 802
		// 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);
		}
803

804 805 806
		// Otherwise we can only copy
		else {
			await this.fileService.copy(resource, target);
807

808 809
			result = true;
		}
B
Benjamin Pasero 已提交
810

811 812 813 814
		// Return early if the operation was not running
		if (!result) {
			return target;
		}
815

816 817
		// Revert the source
		await this.revert(resource);
818

819
		return target;
820 821
	}

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

824
		// Prefer an existing model if it is already loaded for the given target resource
825 826
		let targetExists: boolean = false;
		let targetModel = this.models.get(target);
827
		if (targetModel && targetModel.isResolved()) {
B
Benjamin Pasero 已提交
828
			targetExists = true;
829
		}
830

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

835 836
			// create target model adhoc if file does not exist yet
			if (!targetExists) {
B
Benjamin Pasero 已提交
837
				await this.create(target, '');
838
			}
B
Benjamin Pasero 已提交
839

840
			targetModel = await this.models.loadOrCreate(target);
841
		}
842

843
		try {
844

B
Benjamin Pasero 已提交
845 846 847 848
			// 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
849
			let write: boolean;
B
Benjamin Pasero 已提交
850
			if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.getResource(), this.environmentService.configuration.remoteAuthority))) {
851
				write = await this.confirmOverwrite(target);
B
Benjamin Pasero 已提交
852
			} else {
853
				write = true;
B
Benjamin Pasero 已提交
854 855
			}

856 857 858
			if (!write) {
				return false;
			}
B
Benjamin Pasero 已提交
859

860 861 862 863 864 865
			// 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 已提交
866
				}
867
			}
868

869 870 871 872 873
			// save model
			await targetModel.save(options);

			return true;
		} catch (error) {
874

875
			// binary model: delete the file and run the operation again
876
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_BINARY || (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) {
877 878 879
				await this.fileService.del(target);

				return this.doSaveTextFileAs(sourceModel, resource, target, options);
880 881
			}

882 883
			throw error;
		}
884 885
	}

M
Martin Aeschlimann 已提交
886
	private suggestFileName(untitledResource: URI): URI {
887
		const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);
888
		const remoteAuthority = this.environmentService.configuration.remoteAuthority;
B
Benjamin Pasero 已提交
889
		const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
M
Martin Aeschlimann 已提交
890 891

		const lastActiveFile = this.historyService.getLastActiveFile(schemeFilter);
892
		if (lastActiveFile) {
893 894
			const lastDir = dirname(lastActiveFile);
			return joinPath(lastDir, untitledFileName);
895 896
		}

M
Martin Aeschlimann 已提交
897
		const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
898
		if (lastActiveFolder) {
M
Martin Aeschlimann 已提交
899
			return joinPath(lastActiveFolder, untitledFileName);
900 901
		}

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

905 906 907 908
	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 已提交
909 910
	}

911
	async revertAll(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
912 913

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

916 917 918
		// Revert untitled
		const untitledReverted = this.untitledEditorService.revertAll(resources);
		untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled, success: true }));
919

920
		return revertOperationResult;
921 922
	}

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

B
Benjamin Pasero 已提交
926
		const mapResourceToResult = new ResourceMap<IResult>();
927
		fileModels.forEach(m => {
B
Benjamin Pasero 已提交
928
			mapResourceToResult.set(m.getResource(), {
E
Erich Gamma 已提交
929
				source: m.getResource()
B
Benjamin Pasero 已提交
930
			});
E
Erich Gamma 已提交
931 932
		});

933 934 935 936
		await Promise.all(fileModels.map(async model => {
			try {
				await model.revert(options && options.soft);

E
Erich Gamma 已提交
937
				if (!model.isDirty()) {
M
Matt Bierner 已提交
938 939 940 941
					const result = mapResourceToResult.get(model.getResource());
					if (result) {
						result.success = true;
					}
E
Erich Gamma 已提交
942
				}
943
			} catch (error) {
E
Erich Gamma 已提交
944

945
				// FileNotFound means the file got deleted meanwhile, so still record as successful revert
946
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
M
Matt Bierner 已提交
947 948 949 950
					const result = mapResourceToResult.get(model.getResource());
					if (result) {
						result.success = true;
					}
E
Erich Gamma 已提交
951 952 953 954
				}

				// Otherwise bubble up the error
				else {
955
					throw error;
E
Erich Gamma 已提交
956
				}
957 958
			}
		}));
B
Benjamin Pasero 已提交
959

960
		return { results: mapResourceToResult.values() };
E
Erich Gamma 已提交
961 962
	}

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

B
Benjamin Pasero 已提交
965 966
		// Collect files
		const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());
B
Benjamin Pasero 已提交
967

B
Benjamin Pasero 已提交
968 969
		// Add untitled ones
		dirty.push(...this.untitledEditorService.getDirty(resources));
970

B
Benjamin Pasero 已提交
971
		return dirty;
B
Benjamin Pasero 已提交
972 973
	}

B
Benjamin Pasero 已提交
974
	isDirty(resource?: URI): boolean {
B
Benjamin Pasero 已提交
975

B
Benjamin Pasero 已提交
976 977 978
		// Check for dirty file
		if (this._models.getAll(resource).some(model => model.isDirty())) {
			return true;
979
		}
B
Benjamin Pasero 已提交
980

B
Benjamin Pasero 已提交
981 982 983
		// Check for dirty untitled
		return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
	}
984

B
Benjamin Pasero 已提交
985
	//#endregion
986

B
Benjamin Pasero 已提交
987
	//#region config
B
Benjamin Pasero 已提交
988

B
Benjamin Pasero 已提交
989
	getAutoSaveMode(): AutoSaveMode {
990 991 992 993
		if (this.configuredAutoSaveOnFocusChange) {
			return AutoSaveMode.ON_FOCUS_CHANGE;
		}

994 995 996 997
		if (this.configuredAutoSaveOnWindowChange) {
			return AutoSaveMode.ON_WINDOW_CHANGE;
		}

998
		if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
999
			return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
1000 1001 1002
		}

		return AutoSaveMode.OFF;
1003 1004
	}

B
Benjamin Pasero 已提交
1005
	getAutoSaveConfiguration(): IAutoSaveConfiguration {
1006
		return {
R
Rob Lourens 已提交
1007
			autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : undefined,
1008 1009
			autoSaveFocusChange: this.configuredAutoSaveOnFocusChange,
			autoSaveApplicationChange: this.configuredAutoSaveOnWindowChange
B
Benjamin Pasero 已提交
1010
		};
1011 1012
	}

B
Benjamin Pasero 已提交
1013
	get isHotExitEnabled(): boolean {
1014
		return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
1015 1016
	}

B
Benjamin Pasero 已提交
1017 1018
	//#endregion

B
Benjamin Pasero 已提交
1019
	dispose(): void {
E
Erich Gamma 已提交
1020 1021

		// Clear all caches
1022
		this._models.clear();
B
Benjamin Pasero 已提交
1023 1024

		super.dispose();
E
Erich Gamma 已提交
1025
	}
1026
}