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

37 38 39 40
/**
 * 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 已提交
41

42 43
	_serviceBrand: undefined;

44
	//#region events
45

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

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

52
	//#endregion
53

B
Benjamin Pasero 已提交
54
	readonly files: ITextFileEditorModelManager = this._register(this.instantiationService.createInstance(TextFileEditorModelManager));
55

B
Benjamin Pasero 已提交
56
	readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService;
57

58 59 60 61
	abstract get encoding(): IResourceEncodings;

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

		this.registerListeners();
	}

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

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

B
Benjamin Pasero 已提交
86
	//#region text file read / write
87 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

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

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

B
Benjamin Pasero 已提交
142 143 144 145 146 147 148 149
	async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
		return this.fileService.writeFile(resource, toBufferOrReadable(value), options);
	}

	//#endregion

	//#region text file IO primitives (create, move, copy, delete)

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

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

155 156 157 158 159 160
		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()
161
		const existingModel = this.files.get(resource);
162 163 164 165
		if (existingModel && !existingModel.isDisposed()) {
			await existingModel.revert();
		}

166 167 168
		// after event
		this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.CREATE, resource));

169 170 171 172 173 174 175 176
		return stat;
	}

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

	async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
I
isidor 已提交
177 178 179 180 181 182 183 184
		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> {
185

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

189 190
		// find all models that related to either source or target (can be many if resource is a folder)
		const sourceModels: ITextFileEditorModel[] = [];
191
		const targetModels: ITextFileEditorModel[] = [];
192
		for (const model of this.getFileModels()) {
193
			const resource = model.resource;
194 195

			if (isEqualOrParent(resource, target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) {
196
				targetModels.push(model);
197 198 199 200 201 202 203 204 205
			}

			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
206
		type ModelToRestore = { resource: URI; snapshot?: ITextSnapshot; encoding?: string; mode?: string };
207 208
		const modelsToRestore: ModelToRestore[] = [];
		for (const sourceModel of sourceModels) {
209
			const sourceModelResource = sourceModel.resource;
210 211 212 213 214 215 216 217 218 219 220 221 222

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

223
			const modelToRestore: ModelToRestore = { resource: modelToRestoreResource, encoding: sourceModel.getEncoding() };
224 225 226 227 228 229 230
			if (sourceModel.isDirty()) {
				modelToRestore.snapshot = sourceModel.createSnapshot();
			}

			modelsToRestore.push(modelToRestore);
		}

231 232 233 234 235
		// handle dirty models depending on the operation:
		// - move: revert both source and target (if any)
		// - copy: revert target (if any)
		const dirtyModelsToRevert = (move ? [...sourceModels, ...targetModels] : [...targetModels]).filter(model => model.isDirty());
		await this.doRevertFiles(dirtyModelsToRevert.map(dirtyModel => dirtyModel.resource), { soft: true });
236 237 238 239

		// now we can rename the source to target via file operation
		let stat: IFileStatWithMetadata;
		try {
I
isidor 已提交
240 241 242 243 244
			if (move) {
				stat = await this.fileService.move(source, target, overwrite);
			} else {
				stat = await this.fileService.copy(source, target, overwrite);
			}
245 246 247
		} catch (error) {

			// in case of any error, ensure to set dirty flag back
248
			dirtyModelsToRevert.forEach(dirtyModel => dirtyModel.makeDirty());
249 250 251 252 253 254 255 256 257 258 259

			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.
B
Benjamin Pasero 已提交
260
			const restoredModel = await this.files.resolve(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode });
261 262 263 264 265 266 267 268 269 270

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

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

274 275 276
		return stat;
	}

B
Benjamin Pasero 已提交
277 278 279 280 281
	async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {

		// before event
		await this._onWillRunOperation.fireAsync({ operation: FileOperation.DELETE, target: resource }, CancellationToken.None);

B
Benjamin Pasero 已提交
282 283 284
		// Check for any existing dirty file model for the resource
		// and do a soft revert before deleting to be able to close
		// any opened editor with these files
B
Benjamin Pasero 已提交
285 286 287
		const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource));
		await this.doRevertFiles(dirtyFiles, { soft: true });

B
Benjamin Pasero 已提交
288
		// Now actually delete from disk
B
Benjamin Pasero 已提交
289 290 291 292 293 294
		await this.fileService.del(resource, options);

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

295 296
	//#endregion

B
Benjamin Pasero 已提交
297
	//#region save
298

299
	async save(resource: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
300

301 302
		// Untitled
		if (resource.scheme === Schemas.untitled) {
303 304
			const model = this.untitled.get(resource);
			if (model) {
305
				let targetUri: URI | undefined;
306 307

				// Untitled with associated file path don't need to prompt
308
				if (model.hasAssociatedFilePath) {
309
					targetUri = toLocalResource(resource, this.environmentService.configuration.remoteAuthority);
310 311 312 313
				}

				// Otherwise ask user
				else {
314
					targetUri = await this.promptForPath(resource, this.suggestUntitledFilePath(resource));
315 316
				}

317 318
				// Save as if target provided
				if (targetUri) {
319
					return this.saveAs(resource, targetUri, options);
320
				}
321 322 323
			}
		}

324 325
		// File
		else {
326
			const model = this.files.get(resource);
327
			if (model) {
328

329 330
				// Save with options
				await model.save(options);
331

332
				return !model.isDirty() ? resource : undefined;
333 334 335
			}
		}

336
		return undefined;
337 338
	}

339
	protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
340 341 342 343

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

344
		return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems);
345 346
	}

B
Benjamin Pasero 已提交
347
	private getFileModels(resources?: URI[]): ITextFileEditorModel[] {
B
Benjamin Pasero 已提交
348
		if (Array.isArray(resources)) {
B
Benjamin Pasero 已提交
349
			return coalesce(resources.map(resource => this.files.get(resource)));
350 351
		}

B
Benjamin Pasero 已提交
352
		return this.files.getAll();
353 354
	}

B
Benjamin Pasero 已提交
355
	private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[] {
356 357 358
		return this.getFileModels(resources).filter(model => model.isDirty());
	}

359
	async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
360 361

		// Get to target resource
362 363 364
		if (!target) {
			let dialogPath = source;
			if (source.scheme === Schemas.untitled) {
365
				dialogPath = this.suggestUntitledFilePath(source);
366 367
			}

B
Benjamin Pasero 已提交
368
			target = await this.promptForPath(source, dialogPath, options?.availableFileSystems);
369 370
		}

371
		if (!target) {
372 373 374 375
			return; // user canceled
		}

		// Just save if target is same as models own resource
376
		if (source.toString() === target.toString()) {
377
			return this.save(source, options);
B
Benjamin Pasero 已提交
378
		}
379

380
		// Do it
381
		return this.doSaveAs(source, target, options);
382 383
	}

384 385
	private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
		let success = false;
386

387 388
		// If the source is an existing text file model, we can directly
		// use that model to copy the contents to the target destination
389
		const textFileModel = this.files.get(source);
390 391
		if (textFileModel && textFileModel.isResolved()) {
			success = await this.doSaveAsTextFile(textFileModel, source, target, options);
392 393
		}

394 395 396 397 398 399
		// 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;
400 401
		}

402 403
		// 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
404 405 406 407
		// 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);
408

409
			modelReference.dispose(); // free up our use of the reference
410 411
		}

412 413 414 415 416
		// 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) {
417
				success = await this.doSaveAsTextFile(textModel, source, target, options);
418 419 420
			}
		}

421 422 423
		// Revert the source if result is success
		if (success) {
			await this.revert(source);
424 425
		}

426 427
		return target;
	}
428

429
	private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel | ITextModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
430 431 432 433 434 435 436

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

		// Prefer an existing model if it is already loaded for the given target resource
		let targetExists: boolean = false;
440
		let targetModel = this.files.get(target);
B
Benjamin Pasero 已提交
441
		if (targetModel?.isResolved()) {
442 443 444 445 446 447 448
			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);

449
			// create target file adhoc if it does not exist yet
450 451
			if (!targetExists) {
				await this.create(target, '');
452
			}
453

454
			try {
455
				targetModel = await this.files.resolve(target, { encoding: sourceModelEncoding });
456 457 458 459 460 461 462 463 464 465 466 467 468 469
			} 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);
					}
470 471
				}

472
				throw error;
473
			}
474 475 476 477 478 479 480 481 482 483 484 485
		}

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

487 488 489
		if (!write) {
			return false;
		}
490

491 492 493 494
		let sourceTextModel: ITextModel | undefined = undefined;
		if (sourceModel instanceof BaseTextEditorModel) {
			if (sourceModel.isResolved()) {
				sourceTextModel = sourceModel.textEditorModel;
495
			}
496 497 498
		} else {
			sourceTextModel = sourceModel as ITextModel;
		}
499

500 501 502 503
		let targetTextModel: ITextModel | undefined = undefined;
		if (targetModel.isResolved()) {
			targetTextModel = targetModel.textEditorModel;
		}
504

505 506
		// take over model value, encoding and mode (only if more specific) from source model
		if (sourceTextModel && targetTextModel) {
507 508 509 510 511

			// encoding
			targetModel.updatePreferredEncoding(sourceModelEncoding);

			// content
512
			this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot()));
513

514
			// mode
515 516 517 518
			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
519
			}
520 521 522 523 524 525 526 527

			// transient properties
			const sourceTransientProperties = this.codeEditorService.getTransientModelProperties(sourceTextModel);
			if (sourceTransientProperties) {
				for (const [key, value] of sourceTransientProperties) {
					this.codeEditorService.setTransientModelProperty(targetTextModel, key, value);
				}
			}
528
		}
529 530 531 532 533

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

		return true;
534 535
	}

536 537 538
	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 已提交
539
			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))),
540 541 542 543 544 545 546
			primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
			type: 'warning'
		};

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

547
	private suggestUntitledFilePath(untitledResource: URI): URI {
548
		const remoteAuthority = this.environmentService.configuration.remoteAuthority;
549
		const targetScheme = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
550

551 552 553 554
		// Untitled with associated file path
		const model = this.untitledTextEditorService.get(untitledResource);
		if (model?.hasAssociatedFilePath) {
			return untitledResource.with({ scheme: targetScheme });
555 556
		}

557 558 559 560 561 562 563
		// Untitled without associated file path
		const untitledFileName = model?.suggestFileName() ?? basename(untitledResource);

		// Try to place where last active file was if any
		const defaultFilePath = this.fileDialogService.defaultFilePath(targetScheme);
		if (defaultFilePath) {
			return joinPath(defaultFilePath, untitledFileName);
564 565
		}

566 567
		// Finally fallback to suggest just the file name
		return untitledResource.with({ scheme: targetScheme, path: untitledFileName });
568 569
	}

B
Benjamin Pasero 已提交
570 571 572 573
	//#endregion

	//#region revert

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

576
		// Untitled
577 578 579
		if (resource.scheme === Schemas.untitled) {
			const model = this.untitled.get(resource);
			if (model) {
B
Benjamin Pasero 已提交
580
				return model.revert(0, options);
581 582 583
			}

			return false;
584
		}
585

586
		// File
B
Benjamin Pasero 已提交
587
		return !(await this.doRevertFiles([resource], options)).results.some(result => result.error);
588 589
	}

B
Benjamin Pasero 已提交
590
	private async doRevertFiles(resources: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
B
Benjamin Pasero 已提交
591
		const fileModels = options?.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
592 593

		const mapResourceToResult = new ResourceMap<IResult>();
594 595 596
		fileModels.forEach(fileModel => {
			mapResourceToResult.set(fileModel.resource, {
				source: fileModel.resource
597 598 599 600
			});
		});

		await Promise.all(fileModels.map(async model => {
601
			await model.revert(options);
602

603 604 605 606 607
			// If model is still dirty, mark the resulting operation as error
			if (model.isDirty()) {
				const result = mapResourceToResult.get(model.resource);
				if (result) {
					result.error = true;
608
				}
609
			}
610 611 612 613 614
		}));

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

B
Benjamin Pasero 已提交
615 616
	//#endregion

617 618
	//#region dirty

619
	isDirty(resource: URI): boolean {
B
Benjamin Pasero 已提交
620 621 622
		const model = resource.scheme === Schemas.untitled ? this.untitled.get(resource) : this.files.get(resource);
		if (model) {
			return model.isDirty();
623 624
		}

B
Benjamin Pasero 已提交
625
		return false;
626 627
	}

628
	//#endregion
629
}