From e3033faeee942a15184eb7d3e95f80af51919251 Mon Sep 17 00:00:00 2001
From: Pascal Fong Kye
Date: Wed, 24 Jun 2020 10:32:55 +0200
Subject: [PATCH] File operation events support multiple resources (#98988)
* refactor: use array of resources
* refactor: use an array of uricomponentspair
* feat: move many resources
* refactor: rename data to files
* feat: use array of files for copy
* refactor: use move with multiple resources
* refactor: use move method with array of files
* refactor: rename data to files
* feat: moveOrCopy array of resources on paste
* refactor: use concise loop syntax
* test: assert number of events
* refactor: rename uricomponentspair
* support multiple files on WorkingCopyFileEvent
* feat: support multiple resources
onWillRunWorkingCopyFIleOperation onDidRunWorkingCopyFileOperation
* refactor: make source optional for consistency
* refactor: support resources for delete
* test: isolate tests
* fix: iterate over resources
* feat: support operations on delete
* feat: adopt deleting multiple resources
* fix: typing and sequential flow of copyservice
* fix: typing and naming
* fix: typing and naming
* fix: use different message for multiple overwrites
* refactor: naming consistency
* fix: use array resources
* fix: message for multiple overwrites
* fix format
* clean up working copy file service
* refactor multiple overwrites message helper
* use openeditors to bulk open
* split drop copy and move
* add returns
Co-authored-by: Benjamin Pasero
---
.../api/browser/mainThreadDocuments.ts | 8 +-
.../mainThreadFileSystemEventService.ts | 8 +-
.../workbench/api/common/extHost.protocol.ts | 9 +-
.../common/extHostFileSystemEventService.ts | 20 +-
.../contrib/files/browser/fileActions.ts | 68 +--
.../files/browser/views/explorerViewer.ts | 69 ++-
.../bulkEdit/browser/bulkFileEdits.ts | 4 +-
.../textfile/browser/textFileService.ts | 4 +-
.../common/textFileEditorModelManager.ts | 86 ++--
.../test/browser/textFileService.test.ts | 4 +-
.../workingCopyFileOperationParticipant.ts | 4 +-
.../common/workingCopyFileService.ts | 175 +++++---
.../browser/workingCopyFileService.test.ts | 393 ++++++++++++------
.../browser/api/mainThreadEditors.test.ts | 12 +-
.../test/common/workbenchTestServices.ts | 8 +-
15 files changed, 557 insertions(+), 315 deletions(-)
diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts
index 025dae93162..45e8a6cdd18 100644
--- a/src/vs/workbench/api/browser/mainThreadDocuments.ts
+++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts
@@ -126,8 +126,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
}));
this._toDispose.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
- if (e.source && (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE)) {
- this._modelReferenceCollection.remove(e.source);
+ if (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE) {
+ for (const { source } of e.files) {
+ if (source) {
+ this._modelReferenceCollection.remove(source);
+ }
+ }
}
}));
diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts
index aec20a76da9..8b407cfe40f 100644
--- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts
+++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts
@@ -57,14 +57,14 @@ export class MainThreadFileSystemEventService {
// BEFORE file operation
workingCopyFileService.addFileOperationParticipant({
- participate: (target, source, operation, progress, timeout, token) => {
- return proxy.$onWillRunFileOperation(operation, target, source, timeout, token);
+ participate: (files, operation, progress, timeout, token) => {
+ return proxy.$onWillRunFileOperation(operation, files, timeout, token);
}
});
// AFTER file operation
- this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, e.resource, undefined)));
- this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target, e.source)));
+ this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, [{ target: e.resource }])));
+ this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files)));
}
dispose(): void {
diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts
index 3e8649520e6..ecc9065c054 100644
--- a/src/vs/workbench/api/common/extHost.protocol.ts
+++ b/src/vs/workbench/api/common/extHost.protocol.ts
@@ -1068,10 +1068,15 @@ export interface FileSystemEvents {
deleted: UriComponents[];
}
+export interface SourceTargetPair {
+ source?: UriComponents;
+ target: UriComponents;
+}
+
export interface ExtHostFileSystemEventServiceShape {
$onFileEvent(events: FileSystemEvents): void;
- $onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise;
- $onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void;
+ $onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise;
+ $onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void;
}
export interface ObjectIdentifier {
diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts
index f8114192102..41606d40393 100644
--- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts
+++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts
@@ -5,10 +5,10 @@
import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event';
import { IRelativePattern, parse } from 'vs/base/common/glob';
-import { URI, UriComponents } from 'vs/base/common/uri';
+import { URI } from 'vs/base/common/uri';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import type * as vscode from 'vscode';
-import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto } from './extHost.protocol';
+import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto, SourceTargetPair } from './extHost.protocol';
import * as typeConverter from './extHostTypeConverters';
import { Disposable, WorkspaceEdit } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -142,16 +142,16 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
//--- file operations
- $onDidRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): void {
+ $onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {
switch (operation) {
case FileOperation.MOVE:
- this._onDidRenameFile.fire(Object.freeze({ files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }));
+ this._onDidRenameFile.fire(Object.freeze({ files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }));
break;
case FileOperation.DELETE:
- this._onDidDeleteFile.fire(Object.freeze({ files: [URI.revive(target)] }));
+ this._onDidDeleteFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
break;
case FileOperation.CREATE:
- this._onDidCreateFile.fire(Object.freeze({ files: [URI.revive(target)] }));
+ this._onDidCreateFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
break;
default:
//ignore, dont send
@@ -179,16 +179,16 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
};
}
- async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise {
+ async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise {
switch (operation) {
case FileOperation.MOVE:
- await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }, timeout, token);
+ await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token);
break;
case FileOperation.DELETE:
- await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] }, timeout, token);
+ await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
break;
case FileOperation.CREATE:
- await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] }, timeout, token);
+ await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
break;
default:
//ignore, dont send
diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts
index 1ef66d58ce7..21ceb2e52a6 100644
--- a/src/vs/workbench/contrib/files/browser/fileActions.ts
+++ b/src/vs/workbench/contrib/files/browser/fileActions.ts
@@ -15,7 +15,7 @@ import { Action } from 'vs/base/common/actions';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { VIEWLET_ID, IExplorerService, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
-import { IFileService } from 'vs/platform/files/common/files';
+import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { toResource, SideBySideEditor } from 'vs/workbench/common/editor';
import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput';
@@ -222,7 +222,7 @@ async function deleteFiles(workingCopyFileService: IWorkingCopyFileService, dial
// Call function
try {
- await Promise.all(distinctElements.map(e => workingCopyFileService.delete(e.resource, { useTrash: useTrash, recursive: true })));
+ await workingCopyFileService.delete(distinctElements.map(e => e.resource), { useTrash, recursive: true });
} catch (error) {
// Handle error to delete file(s) from a modal confirmation dialog
@@ -947,7 +947,7 @@ export const renameHandler = async (accessor: ServicesAccessor) => {
const targetResource = resources.joinPath(parentResource, value);
if (stat.resource.toString() !== targetResource.toString()) {
try {
- await workingCopyFileService.move(stat.resource, targetResource);
+ await workingCopyFileService.move([{ source: stat.resource, target: targetResource }]);
await refreshIfSeparator(value, explorerService);
} catch (e) {
notificationService.error(e);
@@ -1033,7 +1033,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => {
defaultUri
});
if (destination) {
- await workingCopyFileService.copy(s.resource, destination, true);
+ await workingCopyFileService.copy([{ source: s.resource, target: destination }], true);
} else {
// User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100
canceled = true;
@@ -1060,14 +1060,13 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
const toPaste = resources.distinctParents(await clipboardService.readResources(), r => r);
const element = context.length ? context[0] : explorerService.roots[0];
- // Check if target is ancestor of pasted folder
- const stats = await Promise.all(toPaste.map(async fileToPaste => {
+ try {
+ // Check if target is ancestor of pasted folder
+ const sourceTargetPairs = await Promise.all(toPaste.map(async fileToPaste => {
- if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
- throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
- }
-
- try {
+ if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
+ throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
+ }
const fileToPasteStat = await fileService.resolve(fileToPaste);
// Find target
@@ -1081,30 +1080,33 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
const incrementalNaming = configurationService.getValue().explorer.incrementalNaming;
const targetFile = findValidPasteFileTarget(explorerService, target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
- // Move/Copy File
- if (pasteShouldMove) {
- return await workingCopyFileService.move(fileToPaste, targetFile);
- } else {
- return await workingCopyFileService.copy(fileToPaste, targetFile);
- }
- } catch (e) {
- onError(notificationService, new Error(nls.localize('fileDeleted', "The file to paste has been deleted or moved since you copied it. {0}", getErrorMessage(e))));
- return undefined;
- }
- }));
+ return { source: fileToPaste, target: targetFile };
+ }));
- if (pasteShouldMove) {
- // Cut is done. Make sure to clear cut state.
- await explorerService.setToCopy([], false);
- pasteShouldMove = false;
- }
- if (stats.length >= 1) {
- const stat = stats[0];
- if (stat && !stat.isDirectory && stats.length === 1) {
- await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
+ // Move/Copy File
+ let stats: IFileStatWithMetadata[] = [];
+ if (pasteShouldMove) {
+ stats = await workingCopyFileService.move(sourceTargetPairs);
+ } else {
+ stats = await workingCopyFileService.copy(sourceTargetPairs);
}
- if (stat) {
- await explorerService.select(stat.resource);
+
+ if (stats.length >= 1) {
+ const stat = stats[0];
+ if (stat && !stat.isDirectory && stats.length === 1) {
+ await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
+ }
+ if (stat) {
+ await explorerService.select(stat.resource);
+ }
+ }
+ } catch (e) {
+ onError(notificationService, new Error(nls.localize('fileDeleted', "The file(s) to paste have been deleted or moved since you copied them. {0}", getErrorMessage(e))));
+ } finally {
+ if (pasteShouldMove) {
+ // Cut is done. Make sure to clear cut state.
+ await explorerService.setToCopy([], false);
+ pasteShouldMove = false;
}
}
};
diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
index 86c3f6b20ed..54d3e8fc359 100644
--- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
+++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
@@ -723,6 +723,20 @@ const getFileOverwriteConfirm = (name: string) => {
};
};
+const getMultipleFilesOverwriteConfirm = (files: URI[]) => {
+ if (files.length > 1) {
+ return {
+ message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length),
+ detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"),
+ primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
+ type: 'warning'
+ };
+ } else {
+ return getFileOverwriteConfirm(basename(files[0]));
+ }
+
+};
+
interface IWebkitDataTransfer {
items: IWebkitDataTransferItem[];
}
@@ -1010,7 +1024,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop {
continue;
}
- await this.workingCopyFileService.delete(joinPath(target.resource, entry.name), { recursive: true });
+ await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true });
}
// Upload entry
@@ -1263,7 +1277,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop {
const sourceFile = resource;
const targetFile = joinPath(target.resource, basename(sourceFile));
- const stat = await this.workingCopyFileService.copy(sourceFile, targetFile, true);
+ const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], true))[0];
// if we only add one file, just open it directly
if (resources.length === 1 && !stat.isDirectory) {
this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
@@ -1310,7 +1324,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop {
}
const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
- await Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise));
+ await Promise.all([this.doHandleExplorerDrop(items.filter(s => !s.isRoot), target, isCopy), rootDropPromise]);
}
private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise {
@@ -1346,36 +1360,39 @@ export class FileDragAndDrop implements ITreeDragAndDrop {
return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
}
- private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise {
- // Reuse duplicate action if user copies
- if (isCopy) {
- const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming;
- const stat = await this.workingCopyFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
- if (!stat.isDirectory) {
- await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
- }
+ private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise {
+ // Reuse duplicate action when user copies
+ const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming;
+ const sourceTargetPairs = sources.map(({ resource, isDirectory }) => ({ source: resource, target: findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming) }));
+ const editors = (await this.workingCopyFileService.copy(sourceTargetPairs)).filter(stat => !stat.isDirectory).map(({ resource }) => ({ resource, options: { pinned: true } }));
+ await this.editorService.openEditors(editors);
+ }
- return;
- }
+ private async doHandleExplorerDropOnMove(sources: ExplorerItem[], target: ExplorerItem): Promise {
- // Otherwise move
- const targetResource = joinPath(target.resource, source.name);
- if (source.isReadonly) {
- // Do not allow moving readonly items
- return Promise.resolve();
- }
+ // Do not allow moving readonly items
+ const sourceTargetPairs = sources.filter(source => !source.isReadonly).map(source => ({ source: source.resource, target: joinPath(target.resource, source.name) }));
try {
- await this.workingCopyFileService.move(source.resource, targetResource);
+ await this.workingCopyFileService.move(sourceTargetPairs);
} catch (error) {
// Conflict
if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
- const confirm = getFileOverwriteConfirm(source.name);
+
+ const overwrites: URI[] = [];
+ for (const { target } of sourceTargetPairs) {
+ if (await this.fileService.exists(target)) {
+ overwrites.push(target);
+ }
+ }
+
+ const confirm = getMultipleFilesOverwriteConfirm(overwrites);
+
// Move with overwrite if the user confirms
const { confirmed } = await this.dialogService.confirm(confirm);
if (confirmed) {
try {
- await this.workingCopyFileService.move(source.resource, targetResource, true /* overwrite */);
+ await this.workingCopyFileService.move(sourceTargetPairs, true /* overwrite */);
} catch (error) {
this.notificationService.error(error);
}
@@ -1388,6 +1405,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop {
}
}
+ private async doHandleExplorerDrop(sources: ExplorerItem[], target: ExplorerItem, isCopy: boolean): Promise {
+ if (isCopy) {
+ return this.doHandleExplorerDropOnCopy(sources, target);
+ } else {
+ return this.doHandleExplorerDropOnMove(sources, target);
+ }
+ }
+
private static getStatsFromDragAndDropData(data: ElementsDragAndDropData, dragStartEvent?: DragEvent): ExplorerItem[] {
if (data.context) {
return data.context;
diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
index f371ee227a4..7b7a9b2e450 100644
--- a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
+++ b/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
@@ -46,7 +46,7 @@ class RenameOperation implements IFileOperation {
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
return new Noop(); // not overwriting, but ignoring, and the target file exists
}
- await this._workingCopyFileService.move(this.oldUri, this.newUri, this.options.overwrite);
+ await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], this.options.overwrite);
return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService);
}
}
@@ -109,7 +109,7 @@ class DeleteOperation implements IFileOperation {
}
const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue('files.enableTrash');
- await this._workingCopyFileService.delete(this.oldUri, { useTrash, recursive: this.options.recursive });
+ await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive });
return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, contents);
}
}
diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts
index 58328694581..1f60e61aa8d 100644
--- a/src/vs/workbench/services/textfile/browser/textFileService.ts
+++ b/src/vs/workbench/services/textfile/browser/textFileService.ts
@@ -155,7 +155,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
async create(resource: URI, value?: string | ITextSnapshot | VSBuffer, options?: ICreateFileOptions): Promise {
// file operation participation
- await this.workingCopyFileService.runFileOperationParticipants(resource, undefined, FileOperation.CREATE);
+ await this.workingCopyFileService.runFileOperationParticipants([{ target: resource, source: undefined }], FileOperation.CREATE);
// create file on disk
const stat = await this.doCreate(resource, value, options);
@@ -246,7 +246,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// However, this will only work if the source exists
// and is not orphaned, so we need to check that too.
if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) {
- await this.workingCopyFileService.move(source, target);
+ await this.workingCopyFileService.move([{ source, target }]);
return this.save(target, options);
}
diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
index 64678af9e2d..782930dc058 100644
--- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
+++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts
@@ -135,50 +135,56 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
// Move / Copy: remember models to restore after the operation
- const source = e.source;
- if (source && (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
+ if (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE) {
- // find all models that related to either source or target (can be many if resource is a folder)
- const sourceModels: TextFileEditorModel[] = [];
- const targetModels: TextFileEditorModel[] = [];
- for (const model of this.models) {
- const resource = model.resource;
-
- if (extUri.isEqualOrParent(resource, e.target)) {
- // EXPLICITLY do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384
- targetModels.push(model);
- }
-
- if (this.uriIdentityService.extUri.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
const modelsToRestore: { source: URI, target: URI, snapshot?: ITextSnapshot; mode?: string; encoding?: string; }[] = [];
- for (const sourceModel of sourceModels) {
- const sourceModelResource = sourceModel.resource;
- // If the source is the actual model, just use target as new resource
- let targetModelResource: URI;
- if (this.uriIdentityService.extUri.isEqual(sourceModelResource, e.source)) {
- targetModelResource = e.target;
+ for (const { source, target } of e.files) {
+ if (source) {
+
+ // find all models that related to either source or target (can be many if resource is a folder)
+ const sourceModels: TextFileEditorModel[] = [];
+ const targetModels: TextFileEditorModel[] = [];
+ for (const model of this.models) {
+ const resource = model.resource;
+
+ if (extUri.isEqualOrParent(resource, target)) {
+ // EXPLICITLY do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384
+ targetModels.push(model);
+ }
+
+ if (this.uriIdentityService.extUri.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
+ for (const sourceModel of sourceModels) {
+ const sourceModelResource = sourceModel.resource;
+
+ // If the source is the actual model, just use target as new resource
+ let targetModelResource: URI;
+ if (this.uriIdentityService.extUri.isEqual(sourceModelResource, source)) {
+ targetModelResource = target;
+ }
+
+ // Otherwise a parent folder of the source is being moved, so we need
+ // to compute the target resource based on that
+ else {
+ targetModelResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1));
+ }
+
+ modelsToRestore.push({
+ source: sourceModelResource,
+ target: targetModelResource,
+ mode: sourceModel.getMode(),
+ encoding: sourceModel.getEncoding(),
+ snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined
+ });
+ }
+
}
-
- // Otherwise a parent folder of the source is being moved, so we need
- // to compute the target resource based on that
- else {
- targetModelResource = joinPath(e.target, sourceModelResource.path.substr(source.path.length + 1));
- }
-
- modelsToRestore.push({
- source: sourceModelResource,
- target: targetModelResource,
- mode: sourceModel.getMode(),
- encoding: sourceModel.getEncoding(),
- snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined
- });
}
this.mapCorrelationIdToModelsToRestore.set(e.correlationId, modelsToRestore);
diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts
index 062219d1e51..c7548eb593f 100644
--- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts
+++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts
@@ -111,8 +111,8 @@ suite('Files - TextFileService', () => {
let eventCounter = 0;
const disposable1 = accessor.workingCopyFileService.addFileOperationParticipant({
- participate: async target => {
- assert.equal(target.toString(), model.resource.toString());
+ participate: async files => {
+ assert.equal(files[0].target, model.resource.toString());
eventCounter++;
}
});
diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts
index 4b0928035ee..7d60ea29d7f 100644
--- a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts
+++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts
@@ -33,7 +33,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable {
return toDisposable(() => remove());
}
- async participate(target: URI, source: URI | undefined, operation: FileOperation): Promise {
+ async participate(files: { source?: URI, target: URI }[], operation: FileOperation): Promise {
const timeout = this.configurationService.getValue('files.participants.timeout');
if (timeout <= 0) {
return; // disabled
@@ -53,7 +53,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable {
}
try {
- const promise = participant.participate(target, source, operation, progress, timeout, cts.token);
+ const promise = participant.participate(files, operation, progress, timeout, cts.token);
await raceTimeout(promise, timeout, () => cts.dispose(true /* cancel */));
} catch (err) {
this.logService.warn(err);
diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts
index dfec25cce8e..6a07bd68642 100644
--- a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts
+++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts
@@ -18,6 +18,19 @@ import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/worki
export const IWorkingCopyFileService = createDecorator('workingCopyFileService');
+interface SourceTargetPair {
+
+ /**
+ * The source resource that is defined for move operations.
+ */
+ readonly source?: URI;
+
+ /**
+ * The target resource the event is about.
+ */
+ readonly target: URI
+}
+
export interface WorkingCopyFileEvent extends IWaitUntil {
/**
@@ -32,25 +45,19 @@ export interface WorkingCopyFileEvent extends IWaitUntil {
readonly operation: FileOperation;
/**
- * The resource the event is about.
+ * The array of source/target pair of files involved in given operation.
*/
- readonly target: URI;
-
- /**
- * A property that is defined for move operations.
- */
- readonly source?: URI;
+ readonly files: SourceTargetPair[]
}
export interface IWorkingCopyFileOperationParticipant {
/**
- * Participate in a file operation of a working copy. Allows to
- * change the working copy before it is being saved to disk.
+ * Participate in a file operation of working copies. Allows to
+ * change the working copies before they are being saved to disk.
*/
participate(
- target: URI,
- source: URI | undefined,
+ files: SourceTargetPair[],
operation: FileOperation,
progress: IProgress,
timeout: number,
@@ -114,40 +121,43 @@ export interface IWorkingCopyFileService {
/**
* Execute all known file operation participants.
*/
- runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise
+ runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation): Promise
+
+ //#endregion
//#region File operations
/**
- * Will move working copies matching the provided resource and children
- * to the target resource using the associated file service for that resource.
+ * Will move working copies matching the provided resources and corresponding children
+ * to the target resources using the associated file service for those resources.
*
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
* `onDidRunWorkingCopyFileOperation` events to participate.
*/
- move(source: URI, target: URI, overwrite?: boolean): Promise;
+ move(files: SourceTargetPair[], overwrite?: boolean): Promise;
/**
- * Will copy working copies matching the provided resource and children
- * to the target using the associated file service for that resource.
+ * Will copy working copies matching the provided resources and corresponding children
+ * to the target resources using the associated file service for those resources.
*
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
* `onDidRunWorkingCopyFileOperation` events to participate.
*/
- copy(source: URI, target: URI, overwrite?: boolean): Promise;
+ copy(files: SourceTargetPair[], overwrite?: boolean): Promise;
/**
- * Will delete working copies matching the provided resource and children
- * using the associated file service for that resource.
+ * Will delete working copies matching the provided resources and children
+ * using the associated file service for those resources.
*
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
* `onDidRunWorkingCopyFileOperation` events to participate.
*/
- delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise;
+ delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise;
//#endregion
+
//#region Path related
/**
@@ -209,93 +219,118 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
});
}
- async move(source: URI, target: URI, overwrite?: boolean): Promise {
- return this.moveOrCopy(source, target, true, overwrite);
+ //#region File operations
+
+ async move(files: SourceTargetPair[], overwrite?: boolean): Promise {
+ return this.doMoveOrCopy(files, true, overwrite);
}
- async copy(source: URI, target: URI, overwrite?: boolean): Promise {
- return this.moveOrCopy(source, target, false, overwrite);
+ async copy(files: SourceTargetPair[], overwrite?: boolean): Promise {
+ return this.doMoveOrCopy(files, false, overwrite);
}
- private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise {
+ private async doMoveOrCopy(files: SourceTargetPair[], move: boolean, overwrite?: boolean): Promise {
+ const stats: IFileStatWithMetadata[] = [];
// validate move/copy operation before starting
- const validateMoveOrCopy = await (move ? this.fileService.canMove(source, target, overwrite) : this.fileService.canCopy(source, target, overwrite));
- if (validateMoveOrCopy instanceof Error) {
- throw validateMoveOrCopy;
+ for (const { source, target } of files) {
+ const validateMoveOrCopy = await (move ? this.fileService.canMove(source!, target, overwrite) : this.fileService.canCopy(source!, target, overwrite));
+ if (validateMoveOrCopy instanceof Error) {
+ throw validateMoveOrCopy;
+ }
}
// file operation participant
- await this.runFileOperationParticipants(target, source, move ? FileOperation.MOVE : FileOperation.COPY);
+ await this.runFileOperationParticipants(files, move ? FileOperation.MOVE : FileOperation.COPY);
- // Before doing the heave operations, check first if source and target
+ // Before doing the heavy operations, check first if source and target
// are either identical or are considered to be identical for the file
// system. In that case we want the model to stay as is and only do the
// raw file operation.
- if (this.uriIdentityService.extUri.isEqual(source, target)) {
- if (move) {
- return this.fileService.move(source, target, overwrite);
+ const remainingFiles: SourceTargetPair[] = [];
+ for (const { source, target } of files) {
+ if (this.uriIdentityService.extUri.isEqual(source, target)) {
+ if (move) {
+ stats.push(await this.fileService.move(source!, target, overwrite));
+ } else {
+ stats.push(await this.fileService.copy(source!, target, overwrite));
+ }
} else {
- return this.fileService.copy(source, target, overwrite);
+ remainingFiles.push({ source, target });
}
}
- // before event
- const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source };
- await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
+ // Now handle all the file operations that are not identical files
+ if (remainingFiles.length > 0) {
- // handle dirty working copies depending on the operation:
- // - move: revert both source and target (if any)
- // - copy: revert target (if any)
- const dirtyWorkingCopies = (move ? [...this.getDirty(source), ...this.getDirty(target)] : this.getDirty(target));
- await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
+ // before event
+ const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, files: remainingFiles };
+ await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
- // now we can rename the source to target via file operation
- let stat: IFileStatWithMetadata;
- try {
- if (move) {
- stat = await this.fileService.move(source, target, overwrite);
- } else {
- stat = await this.fileService.copy(source, target, overwrite);
+
+ // handle dirty working copies depending on the operation:
+ // - move: revert both source and target (if any)
+ // - copy: revert target (if any)
+ for (const { source, target } of remainingFiles) {
+ const dirtyWorkingCopies = (move ? [...this.getDirty(source!), ...this.getDirty(target)] : this.getDirty(target));
+ await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
}
- } catch (error) {
- // error event
- await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
+ // now we can rename the source to target via file operation
+ try {
+ for (const { source, target } of remainingFiles) {
+ if (move) {
+ stats.push(await this.fileService.move(source!, target, overwrite));
+ } else {
+ stats.push(await this.fileService.copy(source!, target, overwrite));
+ }
+ }
+ } catch (error) {
- throw error;
+ // error event
+ await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
+
+ throw error;
+ }
+
+ // after event
+ await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
}
- // after event
- await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
-
- return stat;
+ return stats;
}
- async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise {
+ async delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise {
// validate delete operation before starting
- const validateDelete = await this.fileService.canDelete(resource, options);
- if (validateDelete instanceof Error) {
- throw validateDelete;
+ for (const resource of resources) {
+ const validateDelete = await this.fileService.canDelete(resource, options);
+ if (validateDelete instanceof Error) {
+ throw validateDelete;
+ }
}
// file operation participant
- await this.runFileOperationParticipants(resource, undefined, FileOperation.DELETE);
+ const files = resources.map(target => ({ target }));
+ await this.runFileOperationParticipants(files, FileOperation.DELETE);
// before events
- const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, target: resource };
+ const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files };
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
// Check for any existing dirty working copies for the resource
// and do a soft revert before deleting to be able to close
// any opened editor with these working copies
- const dirtyWorkingCopies = this.getDirty(resource);
- await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
+ for (const resource of resources) {
+ const dirtyWorkingCopies = this.getDirty(resource);
+ await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
+ }
// Now actually delete from disk
try {
- await this.fileService.del(resource, options);
+ for (const resource of resources) {
+ await this.fileService.del(resource, options);
+ }
} catch (error) {
// error event
@@ -308,6 +343,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
}
+ //#endregion
+
//#region File operation participants
@@ -317,8 +354,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
return this.fileOperationParticipants.addFileOperationParticipant(participant);
}
- runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise {
- return this.fileOperationParticipants.participate(target, source, operation);
+ runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation): Promise {
+ return this.fileOperationParticipants.participate(files, operation);
}
//#endregion
diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts
index db3f0a8e4fb..70d59ffc7ad 100644
--- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts
+++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts
@@ -16,7 +16,6 @@ import { TestWorkingCopy } from 'vs/workbench/services/workingCopy/test/common/w
suite('WorkingCopyFileService', () => {
let instantiationService: IInstantiationService;
- let model: TextFileEditorModel;
let accessor: TestServiceAccessor;
setup(() => {
@@ -25,144 +24,116 @@ suite('WorkingCopyFileService', () => {
});
teardown(() => {
- model?.dispose();
(accessor.textFileService.files).dispose();
});
test('delete - dirty file', async function () {
- model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
- (accessor.textFileService.files).add(model.resource, model);
+ await testDelete([toResource.call(this, '/path/file.txt')]);
+ });
- await model.load();
- model!.textEditorModel!.setValue('foo');
- assert.ok(accessor.workingCopyService.isDirty(model.resource));
-
- let eventCounter = 0;
- let correlationId: number | undefined = undefined;
-
- const participant = accessor.workingCopyFileService.addFileOperationParticipant({
- participate: async (target, source, operation) => {
- assert.equal(target.toString(), model.resource.toString());
- assert.equal(operation, FileOperation.DELETE);
- eventCounter++;
- }
- });
-
- const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
- assert.equal(e.target.toString(), model.resource.toString());
- assert.equal(e.operation, FileOperation.DELETE);
- correlationId = e.correlationId;
- eventCounter++;
- });
-
- const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
- assert.equal(e.target.toString(), model.resource.toString());
- assert.equal(e.operation, FileOperation.DELETE);
- assert.equal(e.correlationId, correlationId);
- eventCounter++;
- });
-
- await accessor.workingCopyFileService.delete(model.resource);
- assert.ok(!accessor.workingCopyService.isDirty(model.resource));
-
- assert.equal(eventCounter, 3);
-
- participant.dispose();
- listener1.dispose();
- listener2.dispose();
+ test('delete multiple - dirty files', async function () {
+ await testDelete([
+ toResource.call(this, '/path/file1.txt'),
+ toResource.call(this, '/path/file2.txt'),
+ toResource.call(this, '/path/file3.txt'),
+ toResource.call(this, '/path/file4.txt')]);
});
test('move - dirty file', async function () {
- await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true);
+ await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true);
+ });
+
+ test('move - source identical to target', async function () {
+ let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
+ (accessor.textFileService.files).add(sourceModel.resource, sourceModel);
+
+ const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }], true);
+
+ sourceModel.dispose();
+ assert.equal(eventCounter, 1);
+ });
+
+ test('move - one source == target and another source != target', async function () {
+ let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
+ let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
+ let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
+ (accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
+ (accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
+ (accessor.textFileService.files).add(targetModel2.resource, targetModel2);
+
+ const eventCounter = await testEventsMoveOrCopy([
+ { source: sourceModel1.resource, target: sourceModel1.resource },
+ { source: sourceModel2.resource, target: targetModel2.resource }
+ ], true);
+
+ sourceModel1.dispose();
+ sourceModel2.dispose();
+ targetModel2.dispose();
+ assert.equal(eventCounter, 3);
+ });
+
+ test('move multiple - dirty file', async function () {
+ await testMoveOrCopy([
+ { source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file1_target.txt') },
+ { source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file2_target.txt') }],
+ true);
});
test('move - dirty file (target exists and is dirty)', async function () {
- await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true, true);
+ await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true, true);
});
test('copy - dirty file', async function () {
- await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false);
+ await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false);
+ });
+
+ test('copy - source identical to target', async function () {
+ let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
+ (accessor.textFileService.files).add(sourceModel.resource, sourceModel);
+
+ const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }]);
+
+ sourceModel.dispose();
+ assert.equal(eventCounter, 1);
+ });
+
+ test('copy - one source == target and another source != target', async function () {
+ let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
+ let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
+ let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
+ (accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
+ (accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
+ (accessor.textFileService.files).add(targetModel2.resource, targetModel2);
+
+ const eventCounter = await testEventsMoveOrCopy([
+ { source: sourceModel1.resource, target: sourceModel1.resource },
+ { source: sourceModel2.resource, target: targetModel2.resource }
+ ]);
+
+ sourceModel1.dispose();
+ sourceModel2.dispose();
+ targetModel2.dispose();
+ assert.equal(eventCounter, 3);
+ });
+
+ test('copy multiple - dirty file', async function () {
+ await testMoveOrCopy([
+ { source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file_target1.txt') },
+ { source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file_target2.txt') },
+ { source: toResource.call(this, '/path/file3.txt'), target: toResource.call(this, '/path/file_target3.txt') }],
+ false);
});
test('copy - dirty file (target exists and is dirty)', async function () {
- await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false, true);
+ await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false, true);
});
- async function testMoveOrCopy(source: URI, target: URI, move: boolean, targetDirty?: boolean): Promise {
- let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined);
- let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined);
- (accessor.textFileService.files).add(sourceModel.resource, sourceModel);
- (accessor.textFileService.files).add(targetModel.resource, targetModel);
-
- await sourceModel.load();
- sourceModel.textEditorModel!.setValue('foo');
- assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
-
- if (targetDirty) {
- await targetModel.load();
- targetModel.textEditorModel!.setValue('bar');
- assert.ok(accessor.textFileService.isDirty(targetModel.resource));
- }
-
- let eventCounter = 0;
- let correlationId: number | undefined = undefined;
-
- const participant = accessor.workingCopyFileService.addFileOperationParticipant({
- participate: async (target, source, operation) => {
- assert.equal(target.toString(), targetModel.resource.toString());
- assert.equal(source?.toString(), sourceModel.resource.toString());
- assert.equal(operation, move ? FileOperation.MOVE : FileOperation.COPY);
- eventCounter++;
- }
- });
-
- const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
- assert.equal(e.target.toString(), targetModel.resource.toString());
- assert.equal(e.source?.toString(), sourceModel.resource.toString());
- assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
- eventCounter++;
- correlationId = e.correlationId;
- });
-
- const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
- assert.equal(e.target.toString(), targetModel.resource.toString());
- assert.equal(e.source?.toString(), sourceModel.resource.toString());
- assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
- eventCounter++;
- assert.equal(e.correlationId, correlationId);
- });
-
- if (move) {
- await accessor.workingCopyFileService.move(sourceModel.resource, targetModel.resource, true);
- } else {
- await accessor.workingCopyFileService.copy(sourceModel.resource, targetModel.resource, true);
- }
-
- assert.equal(targetModel.textEditorModel!.getValue(), 'foo');
-
- if (move) {
- assert.ok(!accessor.textFileService.isDirty(sourceModel.resource));
- } else {
- assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
- }
- assert.ok(accessor.textFileService.isDirty(targetModel.resource));
-
- assert.equal(eventCounter, 3);
-
- sourceModel.dispose();
- targetModel.dispose();
-
- participant.dispose();
- listener1.dispose();
- listener2.dispose();
- }
-
test('getDirty', async function () {
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
- (accessor.textFileService.files).add(model.resource, model);
+ (accessor.textFileService.files).add(model1.resource, model1);
const model2 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-2.txt'), 'utf8', undefined);
- (accessor.textFileService.files).add(model.resource, model);
+ (accessor.textFileService.files).add(model2.resource, model2);
let dirty = accessor.workingCopyFileService.getDirty(model1.resource);
assert.equal(dirty.length, 0);
@@ -190,7 +161,7 @@ suite('WorkingCopyFileService', () => {
test('registerWorkingCopyProvider', async function () {
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
- (accessor.textFileService.files).add(model.resource, model);
+ (accessor.textFileService.files).add(model1.resource, model1);
await model1.load();
model1.textEditorModel!.setValue('foo');
@@ -212,4 +183,192 @@ suite('WorkingCopyFileService', () => {
model1.dispose();
});
+
+ async function testEventsMoveOrCopy(files: { source: URI, target: URI }[], move?: boolean): Promise {
+ let eventCounter = 0;
+
+ const participant = accessor.workingCopyFileService.addFileOperationParticipant({
+ participate: async files => {
+ eventCounter++;
+ }
+ });
+
+ const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
+ eventCounter++;
+ });
+
+ const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
+ eventCounter++;
+ });
+
+ if (move) {
+ await accessor.workingCopyFileService.move(files, true);
+ } else {
+ await accessor.workingCopyFileService.copy(files, true);
+ }
+
+ participant.dispose();
+ listener1.dispose();
+ listener2.dispose();
+ return eventCounter;
+ }
+
+ async function testMoveOrCopy(files: { source: URI, target: URI }[], move: boolean, targetDirty?: boolean): Promise {
+
+ let eventCounter = 0;
+ const models = await Promise.all(files.map(async ({ source, target }, i) => {
+ let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined);
+ let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined);
+ (accessor.textFileService.files).add(sourceModel.resource, sourceModel);
+ (accessor.textFileService.files).add(targetModel.resource, targetModel);
+
+ await sourceModel.load();
+ sourceModel.textEditorModel!.setValue('foo' + i);
+ assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
+ if (targetDirty) {
+ await targetModel.load();
+ targetModel.textEditorModel!.setValue('bar' + i);
+ assert.ok(accessor.textFileService.isDirty(targetModel.resource));
+ }
+
+ return { sourceModel, targetModel };
+ }));
+
+ const participant = accessor.workingCopyFileService.addFileOperationParticipant({
+ participate: async (files, operation) => {
+ for (let i = 0; i < files.length; i++) {
+ const { target, source } = files[i];
+ const { targetModel, sourceModel } = models[i];
+
+ assert.equal(target.toString(), targetModel.resource.toString());
+ assert.equal(source?.toString(), sourceModel.resource.toString());
+ }
+
+ eventCounter++;
+
+ assert.equal(operation, move ? FileOperation.MOVE : FileOperation.COPY);
+ }
+ });
+
+ let correlationId: number;
+
+ const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
+ for (let i = 0; i < e.files.length; i++) {
+ const { target, source } = files[i];
+ const { targetModel, sourceModel } = models[i];
+
+ assert.equal(target.toString(), targetModel.resource.toString());
+ assert.equal(source?.toString(), sourceModel.resource.toString());
+ }
+
+ eventCounter++;
+
+ correlationId = e.correlationId;
+ assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
+ });
+
+ const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
+ for (let i = 0; i < e.files.length; i++) {
+ const { target, source } = files[i];
+ const { targetModel, sourceModel } = models[i];
+ assert.equal(target.toString(), targetModel.resource.toString());
+ assert.equal(source?.toString(), sourceModel.resource.toString());
+ }
+
+ eventCounter++;
+
+ assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
+ assert.equal(e.correlationId, correlationId);
+ });
+
+ if (move) {
+ await accessor.workingCopyFileService.move(models.map(m => ({ source: m.sourceModel.resource, target: m.targetModel.resource })), true);
+ } else {
+ await accessor.workingCopyFileService.copy(models.map(m => ({ source: m.sourceModel.resource, target: m.targetModel.resource })), true);
+ }
+
+ for (let i = 0; i < models.length; i++) {
+ const { sourceModel, targetModel } = models[i];
+
+ assert.equal(targetModel.textEditorModel!.getValue(), 'foo' + i);
+
+ if (move) {
+ assert.ok(!accessor.textFileService.isDirty(sourceModel.resource));
+ } else {
+ assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
+ }
+ assert.ok(accessor.textFileService.isDirty(targetModel.resource));
+
+ sourceModel.dispose();
+ targetModel.dispose();
+ }
+ assert.equal(eventCounter, 3);
+
+ participant.dispose();
+ listener1.dispose();
+ listener2.dispose();
+ }
+
+ async function testDelete(resources: URI[]) {
+
+ const models = await Promise.all(resources.map(async resource => {
+ const model = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8', undefined);
+ (accessor.textFileService.files).add(model.resource, model);
+
+ await model.load();
+ model!.textEditorModel!.setValue('foo');
+ assert.ok(accessor.workingCopyService.isDirty(model.resource));
+ return model;
+ }));
+
+ let eventCounter = 0;
+ let correlationId: number | undefined = undefined;
+
+ const participant = accessor.workingCopyFileService.addFileOperationParticipant({
+ participate: async (files, operation) => {
+ for (let i = 0; i < models.length; i++) {
+ const model = models[i];
+ const file = files[i];
+ assert.equal(file.target.toString(), model.resource.toString());
+ }
+ assert.equal(operation, FileOperation.DELETE);
+ eventCounter++;
+ }
+ });
+
+ const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
+ for (let i = 0; i < models.length; i++) {
+ const model = models[i];
+ const file = e.files[i];
+ assert.equal(file.target.toString(), model.resource.toString());
+ }
+ assert.equal(e.operation, FileOperation.DELETE);
+ correlationId = e.correlationId;
+ eventCounter++;
+ });
+
+ const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
+ for (let i = 0; i < models.length; i++) {
+ const model = models[i];
+ const file = e.files[i];
+ assert.equal(file.target.toString(), model.resource.toString());
+ }
+ assert.equal(e.operation, FileOperation.DELETE);
+ assert.equal(e.correlationId, correlationId);
+ eventCounter++;
+ });
+
+ await accessor.workingCopyFileService.delete(models.map(m => m.resource));
+ for (const model of models) {
+ assert.ok(!accessor.workingCopyService.isDirty(model.resource));
+ model.dispose();
+ }
+
+ assert.equal(eventCounter, 3);
+
+ participant.dispose();
+ listener1.dispose();
+ listener2.dispose();
+ }
+
});
diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts
index 306c6662332..3964d05782a 100644
--- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts
+++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts
@@ -107,16 +107,20 @@ suite('MainThreadEditors', () => {
});
services.set(IWorkingCopyFileService, new class extends mock() {
onDidRunWorkingCopyFileOperation = Event.None;
- move(source: URI, target: URI) {
+ move(files: { source: URI, target: URI }[]) {
+ const { source, target } = files[0];
movedResources.set(source, target);
return Promise.resolve(Object.create(null));
}
- copy(source: URI, target: URI) {
+ copy(files: { source: URI, target: URI }[]) {
+ const { source, target } = files[0];
copiedResources.set(source, target);
return Promise.resolve(Object.create(null));
}
- delete(resource: URI) {
- deletedResources.add(resource);
+ delete(resources: URI[]) {
+ for (const resource of resources) {
+ deletedResources.add(resource);
+ }
return Promise.resolve(undefined);
}
});
diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts
index 5ec62b286e5..30eab90aa6a 100644
--- a/src/vs/workbench/test/common/workbenchTestServices.ts
+++ b/src/vs/workbench/test/common/workbenchTestServices.ts
@@ -134,17 +134,17 @@ export class TestWorkingCopyFileService implements IWorkingCopyFileService {
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable { return Disposable.None; }
- async runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise { }
+ async runFileOperationParticipants(files: { source?: URI, target: URI }[], operation: FileOperation): Promise { }
- async delete(resource: URI, options?: { useTrash?: boolean | undefined; recursive?: boolean | undefined; } | undefined): Promise { }
+ async delete(resources: URI[], options?: { useTrash?: boolean | undefined; recursive?: boolean | undefined; } | undefined): Promise { }
registerWorkingCopyProvider(provider: (resourceOrFolder: URI) => IWorkingCopy[]): IDisposable { return Disposable.None; }
getDirty(resource: URI): IWorkingCopy[] { return []; }
- move(source: URI, target: URI, overwrite?: boolean | undefined): Promise { throw new Error('Method not implemented.'); }
+ move(files: { source: URI; target: URI; }[], overwrite?: boolean | undefined): Promise { throw new Error('Method not implemented.'); }
- copy(source: URI, target: URI, overwrite?: boolean | undefined): Promise { throw new Error('Method not implemented.'); }
+ copy(files: { source: URI; target: URI; }[], overwrite?: boolean | undefined): Promise { throw new Error('Method not implemented.'); }
}
export function mock(): Ctor {