textFileService.ts 27.8 KB
Newer Older
B
Benjamin Pasero 已提交
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.
 *--------------------------------------------------------------------------------------------*/

6 7
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
8
import { Emitter, AsyncEmitter } from 'vs/base/common/event';
9
import * as platform from 'vs/base/common/platform';
B
Benjamin Pasero 已提交
10
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
11
import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
12 13
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
14 15
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
16 17
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel';
18 19 20
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceMap } from 'vs/base/common/map';
21
import { Schemas } from 'vs/base/common/network';
22 23 24 25
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
26
import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
27 28 29 30 31 32
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';
import { VSBuffer } from 'vs/base/common/buffer';
import { ITextSnapshot } from 'vs/editor/common/model';
S
rename  
Sandeep Somavarapu 已提交
33
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
34
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
35
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
36
import { CancellationToken } from 'vs/base/common/cancellation';
37
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
B
Benjamin Pasero 已提交
38

39 40 41 42
/**
 * The workbench file service implementation implements the raw file service spec and adds additional methods on top.
 */
export abstract class AbstractTextFileService extends Disposable implements ITextFileService {
B
Benjamin Pasero 已提交
43

44 45
	_serviceBrand: undefined;

46
	//#region events
47

48 49 50 51 52 53
	private _onWillRunOperation = this._register(new AsyncEmitter<FileOperationWillRunEvent>());
	readonly onWillRunOperation = this._onWillRunOperation.event;

	private _onDidRunOperation = this._register(new Emitter<FileOperationDidRunEvent>());
	readonly onDidRunOperation = this._onDidRunOperation.event;

54
	//#endregion
55

B
Benjamin Pasero 已提交
56
	readonly models = this._register(this.instantiationService.createInstance(TextFileEditorModelManager));
57 58 59 60 61

	abstract get encoding(): IResourceEncodings;

	constructor(
		@IFileService protected readonly fileService: IFileService,
62
		@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService,
63
		@ILifecycleService protected readonly lifecycleService: ILifecycleService,
64
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
65 66 67 68 69 70 71
		@IModeService private readonly modeService: IModeService,
		@IModelService private readonly modelService: IModelService,
		@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
		@IHistoryService private readonly historyService: IHistoryService,
		@IDialogService private readonly dialogService: IDialogService,
		@IFileDialogService private readonly fileDialogService: IFileDialogService,
		@IEditorService private readonly editorService: IEditorService,
S
rename  
Sandeep Somavarapu 已提交
72
		@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService,
73 74
		@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
		@ITextModelService private readonly textModelService: ITextModelService
75 76 77 78 79 80
	) {
		super();

		this.registerListeners();
	}

81
	protected registerListeners(): void {
82 83 84 85 86

		// Lifecycle
		this.lifecycleService.onShutdown(this.dispose, this);
	}

B
Benjamin Pasero 已提交
87
	//#region text file IO primitives (read, create, move, delete, update)
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

	async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
		const content = await this.fileService.readFile(resource, options);

		// in case of acceptTextOnly: true, we check the first
		// chunk for possibly being binary by looking for 0-bytes
		// we limit this check to the first 512 bytes
		this.validateBinary(content.value, options);

		return {
			...content,
			encoding: 'utf8',
			value: content.value.toString()
		};
	}

	async readStream(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileStreamContent> {
		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
		// we limit this check to the first 512 bytes
		let checkedForBinary = false;
		const throwOnBinary = (data: VSBuffer): Error | undefined => {
			if (!checkedForBinary) {
				checkedForBinary = true;

				this.validateBinary(data, options);
			}

			return undefined;
		};

		return {
			...stream,
			encoding: 'utf8',
B
Benjamin Pasero 已提交
124
			value: await createTextBufferFactoryFromStream(stream.value, undefined, options?.acceptTextOnly ? throwOnBinary : undefined)
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
		};
	}

	private validateBinary(buffer: VSBuffer, options?: IReadTextFileOptions): void {
		if (!options || !options.acceptTextOnly) {
			return; // no validation needed
		}

		// in case of acceptTextOnly: true, we check the first
		// chunk for possibly being binary by looking for 0-bytes
		// we limit this check to the first 512 bytes
		for (let i = 0; i < buffer.byteLength && i < 512; i++) {
			if (buffer.readUInt8(i) === 0) {
				throw new TextFileOperationError(nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), TextFileOperationResult.FILE_IS_BINARY, options);
			}
		}
	}

	async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
144 145

		// before event
J
Johannes Rieken 已提交
146
		await this._onWillRunOperation.fireAsync({ operation: FileOperation.CREATE, target: resource }, CancellationToken.None);
147

148 149 150 151 152 153 154 155 156 157 158
		const stat = await this.doCreate(resource, value, options);

		// 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()
		const existingModel = this.models.get(resource);
		if (existingModel && !existingModel.isDisposed()) {
			await existingModel.revert();
		}

159 160 161
		// after event
		this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.CREATE, resource));

162 163 164 165 166 167 168 169 170 171 172 173 174
		return stat;
	}

	protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.createFile(resource, toBufferOrReadable(value), options);
	}

	async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.writeFile(resource, toBufferOrReadable(value), options);
	}

	async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {

175
		// before event
J
Johannes Rieken 已提交
176
		await this._onWillRunOperation.fireAsync({ operation: FileOperation.DELETE, target: resource }, CancellationToken.None);
177 178

		const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
179 180
		await this.revertAll(dirtyFiles, { soft: true });

181
		await this.fileService.del(resource, options);
182 183

		// after event
184
		this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.DELETE, resource));
185 186 187
	}

	async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
I
isidor 已提交
188 189 190 191 192 193 194 195
		return this.moveOrCopy(source, target, true, overwrite);
	}

	async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
		return this.moveOrCopy(source, target, false, overwrite);
	}

	private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise<IFileStatWithMetadata> {
196

197
		// before event
I
isidor 已提交
198
		await this._onWillRunOperation.fireAsync({ operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source }, CancellationToken.None);
199

200 201 202 203
		// find all models that related to either source or target (can be many if resource is a folder)
		const sourceModels: ITextFileEditorModel[] = [];
		const conflictingModels: ITextFileEditorModel[] = [];
		for (const model of this.getFileModels()) {
204
			const resource = model.resource;
205 206 207 208 209 210 211 212 213 214 215 216

			if (isEqualOrParent(resource, target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) {
				conflictingModels.push(model);
			}

			if (isEqualOrParent(resource, source)) {
				sourceModels.push(model);
			}
		}

		// remember each source model to load again after move is done
		// with optional content to restore if it was dirty
217
		type ModelToRestore = { resource: URI; snapshot?: ITextSnapshot; encoding?: string; mode?: string };
218 219
		const modelsToRestore: ModelToRestore[] = [];
		for (const sourceModel of sourceModels) {
220
			const sourceModelResource = sourceModel.resource;
221 222 223 224 225 226 227 228 229 230 231 232 233

			// If the source is the actual model, just use target as new resource
			let modelToRestoreResource: URI;
			if (isEqual(sourceModelResource, source)) {
				modelToRestoreResource = target;
			}

			// Otherwise a parent folder of the source is being moved, so we need
			// to compute the target resource based on that
			else {
				modelToRestoreResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1));
			}

234
			const modelToRestore: ModelToRestore = { resource: modelToRestoreResource, encoding: sourceModel.getEncoding() };
235 236 237 238 239 240 241
			if (sourceModel.isDirty()) {
				modelToRestore.snapshot = sourceModel.createSnapshot();
			}

			modelsToRestore.push(modelToRestore);
		}

I
isidor 已提交
242
		// in order to move and copy, we need to soft revert all dirty models,
243 244
		// both from the source as well as the target if any
		const dirtyModels = [...sourceModels, ...conflictingModels].filter(model => model.isDirty());
245
		await this.revertAll(dirtyModels.map(dirtyModel => dirtyModel.resource), { soft: true });
246 247 248 249

		// now we can rename the source to target via file operation
		let stat: IFileStatWithMetadata;
		try {
I
isidor 已提交
250 251 252 253 254
			if (move) {
				stat = await this.fileService.move(source, target, overwrite);
			} else {
				stat = await this.fileService.copy(source, target, overwrite);
			}
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
		} catch (error) {

			// in case of any error, ensure to set dirty flag back
			dirtyModels.forEach(dirtyModel => dirtyModel.makeDirty());

			throw error;
		}

		// finally, restore models that we had loaded previously
		await Promise.all(modelsToRestore.map(async modelToRestore => {

			// restore the model, forcing a reload. this is important because
			// we know the file has changed on disk after the move and the
			// model might have still existed with the previous state. this
			// ensures we are not tracking a stale state.
270
			const restoredModel = await this.models.loadOrCreate(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode });
271 272 273 274 275 276 277 278 279 280

			// restore previous dirty content if any and ensure to mark
			// the model as dirty
			if (modelToRestore.snapshot && restoredModel.isResolved()) {
				this.modelService.updateModel(restoredModel.textEditorModel, createTextBufferFactoryFromSnapshot(modelToRestore.snapshot));

				restoredModel.makeDirty();
			}
		}));

281
		// after event
I
isidor 已提交
282
		this._onDidRunOperation.fire(new FileOperationDidRunEvent(move ? FileOperation.MOVE : FileOperation.COPY, target, source));
283

284 285 286 287 288
		return stat;
	}

	//#endregion

B
Benjamin Pasero 已提交
289
	//#region save
290

291
	async save(resource: URI, options?: ITextFileSaveOptions): Promise<boolean> {
B
Benjamin Pasero 已提交
292
		return !(await this.saveAll([resource], options)).results.some(result => result.error);
293 294
	}

295 296 297
	saveAll(includeUntitled?: boolean, options?: ITextFileSaveOptions): Promise<ITextFileOperationResult>;
	saveAll(resources: URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult>;
	saveAll(arg1?: boolean | URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult> {
298

299 300
		// Extract the resources to save
		let resourcesToSave: URI[] = [];
301
		if (Array.isArray(arg1)) {
302 303 304 305 306 307 308
			// if specific resources are given, we consider even
			// non-dirty ones if options.force is provided
			if (options?.force) {
				resourcesToSave = arg1;
			} else {
				resourcesToSave = this.getDirty(arg1);
			}
309
		} else {
310 311 312
			// if no resources are given, we only consider dirty
			// resources even if options.force is provided
			resourcesToSave = this.getDirty();
313 314 315 316 317
		}

		// split up between files and untitled
		const filesToSave: URI[] = [];
		const untitledToSave: URI[] = [];
318
		resourcesToSave.forEach(resourceToSave => {
319 320
			if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && resourceToSave.scheme === Schemas.untitled) {
				untitledToSave.push(resourceToSave);
321
			} else if (this.fileService.canHandleResource(resourceToSave)) {
322 323 324 325 326 327 328
				filesToSave.push(resourceToSave);
			}
		});

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

329
	private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult> {
330 331 332 333 334 335 336

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

		// Preflight for untitled to handle cancellation from the dialog
		const targetsForUntitled: URI[] = [];
		for (const untitled of untitledResources) {
337
			if (this.untitledTextEditorService.exists(untitled)) {
338 339 340
				let targetUri: URI;

				// Untitled with associated file path don't need to prompt
341
				if (this.untitledTextEditorService.hasAssociatedFilePath(untitled)) {
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
					targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority);
				}

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

					targetUri = targetPath;
				}

				targetsForUntitled.push(targetUri);
			}
		}

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

			result.results.push({
				source: untitledResources[index],
				target: uri,
B
Benjamin Pasero 已提交
366
				error: !uri // the operation was canceled or failed, so mark as error
367 368 369 370 371 372
			});
		}));

		return result;
	}

373
	protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise<URI | undefined> {
374 375 376 377 378 379 380

		// 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.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems));
	}

381
	private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: readonly string[]): ISaveDialogOptions {
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
		const options: ISaveDialogOptions = {
			defaultUri,
			title: nls.localize('saveAsTitle', "Save As"),
			availableFileSystems,
		};

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

434
	private async doSaveAllFiles(resources?: URI[], options: ITextFileSaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
435
		const fileModelsToSave = this.getFileModels(resources);
436 437

		const mapResourceToResult = new ResourceMap<IResult>();
438 439 440 441 442 443
		for (const fileModelToSave of fileModelsToSave) {
			mapResourceToResult.set(fileModelToSave.resource, { source: fileModelToSave.resource });
		}

		// Save all in parallel
		await Promise.all(fileModelsToSave.map(async model => {
444

445
			// Save with options
446 447
			await model.save(options);

B
Benjamin Pasero 已提交
448 449
			// If model is still dirty, mark the resulting operation as error
			if (model.isDirty()) {
450
				const result = mapResourceToResult.get(model.resource);
451
				if (result) {
B
Benjamin Pasero 已提交
452
					result.error = true;
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
				}
			}
		}));

		return { results: mapResourceToResult.values() };
	}

	private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
		if (Array.isArray(arg1)) {
			const models: ITextFileEditorModel[] = [];
			arg1.forEach(resource => {
				models.push(...this.getFileModels(resource));
			});

			return models;
		}

B
Benjamin Pasero 已提交
470
		return this.models.getAll(arg1);
471 472 473 474 475 476
	}

	private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
		return this.getFileModels(resources).filter(model => model.isDirty());
	}

477
	async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
478 479

		// Get to target resource
480 481 482 483
		if (!target) {
			let dialogPath = source;
			if (source.scheme === Schemas.untitled) {
				dialogPath = this.suggestFileName(source);
484 485
			}

486
			target = await this.promptForPath(source, dialogPath, options ? options.availableFileSystems : undefined);
487 488
		}

489
		if (!target) {
490 491 492 493
			return; // user canceled
		}

		// Just save if target is same as models own resource
494 495
		if (source.toString() === target.toString()) {
			await this.save(source, options);
496

497
			return source;
B
Benjamin Pasero 已提交
498
		}
499

500
		// Do it
501
		return this.doSaveAs(source, target, options);
502 503
	}

504 505
	private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
		let success = false;
506

507 508
		// If the source is an existing text file model, we can directly
		// use that model to copy the contents to the target destination
B
Benjamin Pasero 已提交
509
		const textFileModel = this.models.get(source);
510 511
		if (textFileModel && textFileModel.isResolved()) {
			success = await this.doSaveAsTextFile(textFileModel, source, target, options);
512 513
		}

514 515 516 517 518 519
		// Otherwise if the source can be handled by the file service
		// we can simply invoke the copy() function to save as
		else if (this.fileService.canHandleResource(source)) {
			await this.fileService.copy(source, target);

			success = true;
520 521
		}

522 523 524 525 526 527
		// Finally, if the source does not seem to be a file, we have to
		// try to resolve a text model from the resource to get at the
		// contents and additional meta data (e.g. encoding).
		else if (this.textModelService.hasTextModelContentProvider(source.scheme)) {
			const modelReference = await this.textModelService.createModelReference(source);
			success = await this.doSaveAsTextFile(modelReference.object, source, target, options);
528

529
			modelReference.dispose(); // free up our use of the reference
530 531
		}

532 533 534
		// Revert the source if result is success
		if (success) {
			await this.revert(source);
535 536
		}

537 538
		return target;
	}
539

540 541 542 543 544 545 546 547
	private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {

		// Find source encoding if any
		let sourceModelEncoding: string | undefined = undefined;
		const sourceModelWithEncodingSupport = (sourceModel as unknown as IEncodingSupport);
		if (typeof sourceModelWithEncodingSupport.getEncoding === 'function') {
			sourceModelEncoding = sourceModelWithEncodingSupport.getEncoding();
		}
548 549 550 551

		// Prefer an existing model if it is already loaded for the given target resource
		let targetExists: boolean = false;
		let targetModel = this.models.get(target);
B
Benjamin Pasero 已提交
552
		if (targetModel?.isResolved()) {
553 554 555 556 557 558 559 560 561 562
			targetExists = true;
		}

		// Otherwise create the target file empty if it does not exist already and resolve it from there
		else {
			targetExists = await this.fileService.exists(target);

			// create target model adhoc if file does not exist yet
			if (!targetExists) {
				await this.create(target, '');
563
			}
564

565 566 567 568 569 570 571
			// Carry over the mode if this is an untitled file and the mode was picked by the user
			let mode: string | undefined;
			if (sourceModel instanceof UntitledTextEditorModel) {
				mode = sourceModel.getMode();
				if (mode === PLAINTEXT_MODE_ID) {
					mode = undefined; // never enforce plain text mode when moving as it is unspecific
				}
572 573
			}

574
			targetModel = await this.models.loadOrCreate(target, { encoding: sourceModelEncoding, mode });
575 576 577 578 579 580 581 582 583
		}

		try {

			// 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
			let write: boolean;
584
			if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.configuration.remoteAuthority))) {
585 586 587 588 589 590 591 592 593 594
				write = await this.confirmOverwrite(target);
			} else {
				write = true;
			}

			if (!write) {
				return false;
			}

			// take over model value, encoding and mode (only if more specific) from source model
595
			targetModel.updatePreferredEncoding(sourceModelEncoding);
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
			if (sourceModel.isResolved() && targetModel.isResolved()) {
				this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));

				const sourceMode = sourceModel.textEditorModel.getLanguageIdentifier();
				const targetMode = targetModel.textEditorModel.getLanguageIdentifier();
				if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) {
					targetModel.textEditorModel.setMode(sourceMode); // only use if more specific than plain/text
				}
			}

			// save model
			await targetModel.save(options);

			return true;
		} catch (error) {

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

619
				return this.doSaveAsTextFile(sourceModel, source, target, options);
620 621 622 623 624 625
			}

			throw error;
		}
	}

626 627 628
	private async confirmOverwrite(resource: URI): Promise<boolean> {
		const confirm: IConfirmation = {
			message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)),
I
isidor 已提交
629
			detail: nls.localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))),
630 631 632 633 634 635 636
			primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
			type: 'warning'
		};

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

637
	private suggestFileName(untitledResource: URI): URI {
638
		const untitledFileName = this.untitledTextEditorService.suggestFileName(untitledResource);
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
		const remoteAuthority = this.environmentService.configuration.remoteAuthority;
		const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;

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

		const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
		if (lastActiveFolder) {
			return joinPath(lastActiveFolder, untitledFileName);
		}

		return untitledResource.with({ path: untitledFileName });
	}

B
Benjamin Pasero 已提交
656 657 658 659
	//#endregion

	//#region revert

660
	async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {
B
Benjamin Pasero 已提交
661
		return !(await this.revertAll([resource], options)).results.some(result => result.error);
662 663 664 665 666 667 668 669
	}

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

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

		// Revert untitled
670
		const untitledReverted = this.untitledTextEditorService.revertAll(resources);
B
Benjamin Pasero 已提交
671
		untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled }));
672 673 674 675 676

		return revertOperationResult;
	}

	private async doRevertAllFiles(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
B
Benjamin Pasero 已提交
677
		const fileModels = options?.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
678 679

		const mapResourceToResult = new ResourceMap<IResult>();
680 681 682
		fileModels.forEach(fileModel => {
			mapResourceToResult.set(fileModel.resource, {
				source: fileModel.resource
683 684 685 686 687
			});
		});

		await Promise.all(fileModels.map(async model => {
			try {
688
				await model.revert(options);
689

B
Benjamin Pasero 已提交
690 691
				// If model is still dirty, mark the resulting operation as error
				if (model.isDirty()) {
692
					const result = mapResourceToResult.get(model.resource);
693
					if (result) {
B
Benjamin Pasero 已提交
694
						result.error = true;
695 696 697 698
					}
				}
			} catch (error) {

B
Benjamin Pasero 已提交
699
				// FileNotFound means the file got deleted meanwhile, so ignore it
700
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
701
					return;
702 703 704 705 706 707
				}

				// Otherwise bubble up the error
				else {
					throw error;
				}
708
			}
709 710 711 712 713
		}));

		return { results: mapResourceToResult.values() };
	}

B
Benjamin Pasero 已提交
714 715
	//#endregion

716 717
	//#region dirty

718 719 720
	getDirty(resources?: URI[]): URI[] {

		// Collect files
721
		const dirty = this.getDirtyFileModels(resources).map(dirtyFileModel => dirtyFileModel.resource);
722 723

		// Add untitled ones
724
		dirty.push(...this.untitledTextEditorService.getDirty(resources));
725 726 727 728 729 730 731

		return dirty;
	}

	isDirty(resource?: URI): boolean {

		// Check for dirty file
B
Benjamin Pasero 已提交
732
		if (this.models.getAll(resource).some(model => model.isDirty())) {
733
			return true;
734 735
		}

736
		// Check for dirty untitled
737
		return this.untitledTextEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
738
	}
739 740

	//#endregion
741
}