textFileService.ts 23.4 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';
B
Benjamin Pasero 已提交
35

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

41 42
	_serviceBrand: undefined;

43
	//#region events
44

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

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

51
	//#endregion
52

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

55 56 57 58 59
	private _untitled: IUntitledTextEditorModelManager;
	get untitled(): IUntitledTextEditorModelManager {
		return this._untitled;
	}

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

	constructor(
		@IFileService protected readonly fileService: IFileService,
64
		@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
65
		@ILifecycleService protected readonly lifecycleService: ILifecycleService,
66
		@IInstantiationService protected readonly instantiationService: IInstantiationService,
67 68 69 70 71 72
		@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
	) {
		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 182
		const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource));
		await this.doRevertAllFiles(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());
248
		await this.doRevertAllFiles(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 344 345 346 347 348 349 350 351 352 353
	}

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

			return models;
		}

354
		return this.files.getAll(arg1);
355 356 357 358 359 360
	}

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

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

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

370
			target = await this.promptForPath(source, dialogPath, options ? options.availableFileSystems : undefined);
371 372
		}

373
		if (!target) {
374 375 376 377
			return; // user canceled
		}

		// Just save if target is same as models own resource
378 379
		if (source.toString() === target.toString()) {
			await this.save(source, options);
380

381
			return source;
B
Benjamin Pasero 已提交
382
		}
383

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

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

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

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

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

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

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

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

430 431
		return target;
	}
432

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

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

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

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

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

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

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

491 492 493
		if (!write) {
			return false;
		}
494

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

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

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

			// encoding
			targetModel.updatePreferredEncoding(sourceModelEncoding);

			// content
516
			this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot()));
517

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

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

		return true;
530 531
	}

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

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

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

	//#region revert

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

568
		// Untitled
569 570 571 572 573 574 575
		if (resource.scheme === Schemas.untitled) {
			const model = this.untitled.get(resource);
			if (model) {
				return model.revert(options);
			}

			return false;
576
		}
577

578 579
		// File
		return !(await this.doRevertAllFiles([resource], options)).results.some(result => result.error);
580 581
	}

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

		const mapResourceToResult = new ResourceMap<IResult>();
586 587 588
		fileModels.forEach(fileModel => {
			mapResourceToResult.set(fileModel.resource, {
				source: fileModel.resource
589 590 591 592 593
			});
		});

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

B
Benjamin Pasero 已提交
596 597
				// If model is still dirty, mark the resulting operation as error
				if (model.isDirty()) {
598
					const result = mapResourceToResult.get(model.resource);
599
					if (result) {
B
Benjamin Pasero 已提交
600
						result.error = true;
601 602 603 604
					}
				}
			} catch (error) {

B
Benjamin Pasero 已提交
605
				// FileNotFound means the file got deleted meanwhile, so ignore it
606
				if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
B
Benjamin Pasero 已提交
607
					return;
608 609 610 611 612 613
				}

				// Otherwise bubble up the error
				else {
					throw error;
				}
614
			}
615 616 617 618 619
		}));

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

B
Benjamin Pasero 已提交
620 621
	//#endregion

622 623
	//#region dirty

624
	isDirty(resource: URI): boolean {
625

626 627 628 629 630 631 632 633
		// Check for dirty untitled
		if (resource.scheme === Schemas.untitled) {
			const model = this.untitled.get(resource);
			if (model) {
				return model.isDirty();
			}

			return false;
634 635
		}

636 637
		// Check for dirty file
		return this.files.getAll(resource).some(model => model.isDirty());
638 639
	}

640
	//#endregion
641
}