textFileService.ts 25.7 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
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';
32
import { ITextSnapshot, ITextModel } 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';
38
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
B
Benjamin Pasero 已提交
39

40 41 42 43
/**
 * 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 已提交
44

45 46
	_serviceBrand: undefined;

47
	//#region events
48

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

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

55
	//#endregion
56

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

	abstract get encoding(): IResourceEncodings;

	constructor(
		@IFileService protected readonly fileService: IFileService,
63
		@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService,
64
		@ILifecycleService protected readonly lifecycleService: ILifecycleService,
65
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
66 67 68 69 70 71 72
		@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 已提交
73
		@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService,
74 75
		@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
		@ITextModelService private readonly textModelService: ITextModelService
76 77 78 79 80 81
	) {
		super();

		this.registerListeners();
	}

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

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

B
Benjamin Pasero 已提交
88
	//#region text file IO primitives (read, create, move, delete, update)
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 124

	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 已提交
125
			value: await createTextBufferFactoryFromStream(stream.value, undefined, options?.acceptTextOnly ? throwOnBinary : undefined)
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
		};
	}

	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> {
145 146

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

149 150 151 152 153 154 155 156 157 158 159
		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();
		}

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

163 164 165 166 167 168 169 170 171 172 173 174 175
		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> {

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

179 180
		const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource));
		await this.doRevertAllFiles(dirtyFiles, { soft: true });
181

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

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

	async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
I
isidor 已提交
189 190 191 192 193 194 195 196
		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> {
197

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

201 202 203 204
		// 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()) {
205
			const resource = model.resource;
206 207 208 209 210 211 212 213 214 215 216 217

			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
218
		type ModelToRestore = { resource: URI; snapshot?: ITextSnapshot; encoding?: string; mode?: string };
219 220
		const modelsToRestore: ModelToRestore[] = [];
		for (const sourceModel of sourceModels) {
221
			const sourceModelResource = sourceModel.resource;
222 223 224 225 226 227 228 229 230 231 232 233 234

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

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

			modelsToRestore.push(modelToRestore);
		}

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

		// now we can rename the source to target via file operation
		let stat: IFileStatWithMetadata;
		try {
I
isidor 已提交
251 252 253 254 255
			if (move) {
				stat = await this.fileService.move(source, target, overwrite);
			} else {
				stat = await this.fileService.copy(source, target, overwrite);
			}
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
		} 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.
271
			const restoredModel = await this.models.loadOrCreate(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode });
272 273 274 275 276 277 278 279 280 281

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

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

285 286 287 288 289
		return stat;
	}

	//#endregion

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

292
	async save(resource: URI, options?: ITextFileSaveOptions): Promise<boolean> {
293

294 295 296 297
		// Untitled
		if (resource.scheme === Schemas.untitled) {
			if (this.untitledTextEditorService.exists(resource)) {
				let targetUri: URI | undefined;
298 299

				// Untitled with associated file path don't need to prompt
300 301
				if (this.untitledTextEditorService.hasAssociatedFilePath(resource)) {
					targetUri = toLocalResource(resource, this.environmentService.configuration.remoteAuthority);
302 303 304 305
				}

				// Otherwise ask user
				else {
306
					targetUri = await this.promptForPath(resource, this.suggestFileName(resource));
307 308
				}

309 310 311 312 313 314
				// Save as if target provided
				if (targetUri) {
					await this.saveAs(resource, targetUri, options);

					return true;
				}
315 316 317
			}
		}

318 319 320 321
		// File
		else {
			const model = this.models.get(resource);
			if (model) {
322

323 324
				// Save with options
				await model.save(options);
325

326 327 328 329 330
				return !model.isDirty();
			}
		}

		return false;
331 332
	}

333
	protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise<URI | undefined> {
334 335 336 337 338 339 340

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

341
	private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: readonly string[]): ISaveDialogOptions {
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 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 400 401 402 403
		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;
	}

	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 已提交
404
		return this.models.getAll(arg1);
405 406 407 408 409 410
	}

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

411
	async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
412 413

		// Get to target resource
414 415 416 417
		if (!target) {
			let dialogPath = source;
			if (source.scheme === Schemas.untitled) {
				dialogPath = this.suggestFileName(source);
418 419
			}

420
			target = await this.promptForPath(source, dialogPath, options ? options.availableFileSystems : undefined);
421 422
		}

423
		if (!target) {
424 425 426 427
			return; // user canceled
		}

		// Just save if target is same as models own resource
428 429
		if (source.toString() === target.toString()) {
			await this.save(source, options);
430

431
			return source;
B
Benjamin Pasero 已提交
432
		}
433

434
		// Do it
435
		return this.doSaveAs(source, target, options);
436 437
	}

438 439
	private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
		let success = false;
440

441 442
		// 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 已提交
443
		const textFileModel = this.models.get(source);
444 445
		if (textFileModel && textFileModel.isResolved()) {
			success = await this.doSaveAsTextFile(textFileModel, source, target, options);
446 447
		}

448 449 450 451 452 453
		// 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;
454 455
		}

456 457
		// Next, if the source does not seem to be a file, we try to
		// resolve a text model from the resource to get at the
458 459 460 461
		// 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);
462

463
			modelReference.dispose(); // free up our use of the reference
464 465
		}

466 467 468 469 470
		// Finally we simply check if we can find a editor model that
		// would give us access to the contents.
		else {
			const textModel = this.modelService.getModel(source);
			if (textModel) {
471
				success = await this.doSaveAsTextFile(textModel, source, target, options);
472 473 474
			}
		}

475 476 477
		// Revert the source if result is success
		if (success) {
			await this.revert(source);
478 479
		}

480 481
		return target;
	}
482

483
	private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel | ITextModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
484 485 486 487 488 489 490

		// 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();
		}
491 492 493 494

		// 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 已提交
495
		if (targetModel?.isResolved()) {
496 497 498 499 500 501 502
			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);

503
			// create target file adhoc if it does not exist yet
504 505
			if (!targetExists) {
				await this.create(target, '');
506
			}
507

508 509 510 511 512 513 514
			// 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
				}
515 516
			}

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
			try {
				targetModel = await this.models.loadOrCreate(target, { encoding: sourceModelEncoding, mode });
			} catch (error) {
				// if the target already exists and was not created by us, it is possible
				// that we cannot load the target as text model if it is binary or too
				// large. in that case we have to delete the target file first and then
				// re-run the operation.
				if (targetExists) {
					if (
						(<TextFileOperationError>error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY ||
						(<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE
					) {
						await this.fileService.del(target);

						return this.doSaveAsTextFile(sourceModel, source, target, options);
					}
533 534
				}

535
				throw error;
536
			}
537 538 539 540 541 542 543 544 545 546 547 548
		}

		// 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;
		if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.configuration.remoteAuthority))) {
			write = await this.confirmOverwrite(target);
		} else {
			write = true;
		}
549

550 551 552
		if (!write) {
			return false;
		}
553

554 555 556 557
		let sourceTextModel: ITextModel | undefined = undefined;
		if (sourceModel instanceof BaseTextEditorModel) {
			if (sourceModel.isResolved()) {
				sourceTextModel = sourceModel.textEditorModel;
558
			}
559 560 561
		} else {
			sourceTextModel = sourceModel as ITextModel;
		}
562

563 564 565 566
		let targetTextModel: ITextModel | undefined = undefined;
		if (targetModel.isResolved()) {
			targetTextModel = targetModel.textEditorModel;
		}
567

568 569 570 571
		// take over model value, encoding and mode (only if more specific) from source model
		targetModel.updatePreferredEncoding(sourceModelEncoding);
		if (sourceTextModel && targetTextModel) {
			this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot()));
572

573 574 575 576
			const sourceMode = sourceTextModel.getLanguageIdentifier();
			const targetMode = targetTextModel.getLanguageIdentifier();
			if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) {
				targetTextModel.setMode(sourceMode); // only use if more specific than plain/text
577 578
			}
		}
579 580 581 582 583

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

		return true;
584 585
	}

586 587 588
	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 已提交
589
			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))),
590 591 592 593 594 595 596
			primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
			type: 'warning'
		};

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

597
	private suggestFileName(untitledResource: URI): URI {
598
		const untitledFileName = this.untitledTextEditorService.exists(untitledResource) ? this.untitledTextEditorService.createOrGet(untitledResource).suggestFileName() : basename(untitledResource);
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615
		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 已提交
616 617 618 619
	//#endregion

	//#region revert

620 621
	async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {

622 623 624 625
		// Untitled
		if (this.untitledTextEditorService.exists(resource)) {
			return this.untitledTextEditorService.createOrGet(resource).revert(options);
		}
626

627 628
		// File
		return !(await this.doRevertAllFiles([resource], options)).results.some(result => result.error);
629 630
	}

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

		const mapResourceToResult = new ResourceMap<IResult>();
635 636 637
		fileModels.forEach(fileModel => {
			mapResourceToResult.set(fileModel.resource, {
				source: fileModel.resource
638 639 640 641 642
			});
		});

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

B
Benjamin Pasero 已提交
645 646
				// If model is still dirty, mark the resulting operation as error
				if (model.isDirty()) {
647
					const result = mapResourceToResult.get(model.resource);
648
					if (result) {
B
Benjamin Pasero 已提交
649
						result.error = true;
650 651 652 653
					}
				}
			} catch (error) {

B
Benjamin Pasero 已提交
654
				// FileNotFound means the file got deleted meanwhile, so ignore it
655
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
656
					return;
657 658 659 660 661 662
				}

				// Otherwise bubble up the error
				else {
					throw error;
				}
663
			}
664 665 666 667 668
		}));

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

B
Benjamin Pasero 已提交
669 670
	//#endregion

671 672
	//#region dirty

673
	isDirty(resource: URI): boolean {
674 675

		// Check for dirty file
B
Benjamin Pasero 已提交
676
		if (this.models.getAll(resource).some(model => model.isDirty())) {
677
			return true;
678 679
		}

680
		// Check for dirty untitled
681
		return this.untitledTextEditorService.exists(resource) && this.untitledTextEditorService.createOrGet(resource).isDirty();
682 683
	}

684
	//#endregion
685
}