textFileService.ts 23.9 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 } 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 23
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';
24 25
import { isEqualOrParent, isEqual, joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources';
import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
26 27
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { VSBuffer } from 'vs/base/common/buffer';
28
import { ITextSnapshot, ITextModel } from 'vs/editor/common/model';
S
rename  
Sandeep Somavarapu 已提交
29
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
30
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
31
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
32
import { CancellationToken } from 'vs/base/common/cancellation';
33
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
34
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
35
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
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

54
	readonly files = this._register(this.instantiationService.createInstance(TextFileEditorModelManager));
55

56
	private _untitled: IUntitledTextEditorModelManager;
B
Benjamin Pasero 已提交
57
	get untitled(): IUntitledTextEditorModelManager { return this._untitled; }
58

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

	constructor(
		@IFileService protected readonly fileService: IFileService,
63
		@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
64
		@ILifecycleService protected readonly lifecycleService: ILifecycleService,
65
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
66 67 68 69 70 71
		@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
		@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
74 75
		@ITextModelService private readonly textModelService: ITextModelService,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService
76 77 78
	) {
		super();

79 80
		this._untitled = untitledTextEditorService;

81 82 83
		this.registerListeners();
	}

84
	protected registerListeners(): void {
85 86 87 88 89

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

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

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

	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> {
147 148

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

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

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

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

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

181
		const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource));
B
Benjamin Pasero 已提交
182
		await this.doRevertFiles(dirtyFiles, { soft: true });
183

184
		await this.fileService.del(resource, options);
185 186

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

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

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

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

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

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

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

			modelsToRestore.push(modelToRestore);
		}

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

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

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

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

287 288 289 290 291
		return stat;
	}

	//#endregion

B
Benjamin Pasero 已提交
292
	//#region save
293

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

296 297
		// Untitled
		if (resource.scheme === Schemas.untitled) {
298 299
			const model = this.untitled.get(resource);
			if (model) {
300
				let targetUri: URI | undefined;
301 302

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

				// Otherwise ask user
				else {
309
					targetUri = await this.promptForPath(resource, this.suggestFilePath(resource));
310 311
				}

312 313 314 315 316 317
				// Save as if target provided
				if (targetUri) {
					await this.saveAs(resource, targetUri, options);

					return true;
				}
318 319 320
			}
		}

321 322
		// File
		else {
323
			const model = this.files.get(resource);
324
			if (model) {
325

326 327
				// Save with options
				await model.save(options);
328

329 330 331 332 333
				return !model.isDirty();
			}
		}

		return false;
334 335
	}

336
	protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
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 } });

341
		return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems);
342 343
	}

B
Benjamin Pasero 已提交
344 345
	private getFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
		if (Array.isArray(resources)) {
346
			const models: ITextFileEditorModel[] = [];
B
Benjamin Pasero 已提交
347
			resources.forEach(resource => models.push(...this.getFileModels(resource)));
348 349 350 351

			return models;
		}

B
Benjamin Pasero 已提交
352
		return this.files.getAll(resources);
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.suggestFilePath(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 377
		if (source.toString() === target.toString()) {
			await this.save(source, options);
378

379
			return source;
B
Benjamin Pasero 已提交
380
		}
381

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

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

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

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

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

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

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

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

428 429
		return target;
	}
430

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

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

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

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

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

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

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

489 490 491
		if (!write) {
			return false;
		}
492

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

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

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

			// encoding
			targetModel.updatePreferredEncoding(sourceModelEncoding);

			// content
514
			this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot()));
515

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

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

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

		return true;
536 537
	}

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

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

549
	private suggestFilePath(untitledResource: URI): URI {
550
		const untitledFileName = this.untitled.get(untitledResource)?.suggestFileName() ?? basename(untitledResource);
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
		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 已提交
568 569 570 571
	//#endregion

	//#region revert

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

574
		// Untitled
575 576 577 578 579 580 581
		if (resource.scheme === Schemas.untitled) {
			const model = this.untitled.get(resource);
			if (model) {
				return model.revert(options);
			}

			return false;
582
		}
583

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

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

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

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

B
Benjamin Pasero 已提交
602 603
				// If model is still dirty, mark the resulting operation as error
				if (model.isDirty()) {
604
					const result = mapResourceToResult.get(model.resource);
605
					if (result) {
B
Benjamin Pasero 已提交
606
						result.error = true;
607 608 609 610
					}
				}
			} catch (error) {

B
Benjamin Pasero 已提交
611
				// FileNotFound means the file got deleted meanwhile, so ignore it
612
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
613
					return;
614 615 616 617 618 619
				}

				// Otherwise bubble up the error
				else {
					throw error;
				}
620
			}
621 622 623 624 625
		}));

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

B
Benjamin Pasero 已提交
626 627
	//#endregion

628 629
	//#region dirty

630
	isDirty(resource: URI): boolean {
631

632 633 634 635 636 637 638 639
		// Check for dirty untitled
		if (resource.scheme === Schemas.untitled) {
			const model = this.untitled.get(resource);
			if (model) {
				return model.isDirty();
			}

			return false;
640 641
		}

642 643
		// Check for dirty file
		return this.files.getAll(resource).some(model => model.isDirty());
644 645
	}

646
	//#endregion
647
}