Skip to content

Commit e3033fa

Browse files
Pascal Fong KyeBenjamin Pasero
andauthored
File operation events support multiple resources (microsoft#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 <benjpas@microsoft.com>
1 parent ac314ec commit e3033fa

15 files changed

Lines changed: 545 additions & 303 deletions

File tree

src/vs/workbench/api/browser/mainThreadDocuments.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
126126
}));
127127

128128
this._toDispose.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
129-
if (e.source && (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE)) {
130-
this._modelReferenceCollection.remove(e.source);
129+
if (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE) {
130+
for (const { source } of e.files) {
131+
if (source) {
132+
this._modelReferenceCollection.remove(source);
133+
}
134+
}
131135
}
132136
}));
133137

src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ export class MainThreadFileSystemEventService {
5757

5858
// BEFORE file operation
5959
workingCopyFileService.addFileOperationParticipant({
60-
participate: (target, source, operation, progress, timeout, token) => {
61-
return proxy.$onWillRunFileOperation(operation, target, source, timeout, token);
60+
participate: (files, operation, progress, timeout, token) => {
61+
return proxy.$onWillRunFileOperation(operation, files, timeout, token);
6262
}
6363
});
6464

6565
// AFTER file operation
66-
this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, e.resource, undefined)));
67-
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target, e.source)));
66+
this._listener.add(textFileService.onDidCreateTextFile(e => proxy.$onDidRunFileOperation(FileOperation.CREATE, [{ target: e.resource }])));
67+
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files)));
6868
}
6969

7070
dispose(): void {

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,10 +1068,15 @@ export interface FileSystemEvents {
10681068
deleted: UriComponents[];
10691069
}
10701070

1071+
export interface SourceTargetPair {
1072+
source?: UriComponents;
1073+
target: UriComponents;
1074+
}
1075+
10711076
export interface ExtHostFileSystemEventServiceShape {
10721077
$onFileEvent(events: FileSystemEvents): void;
1073-
$onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise<any>;
1074-
$onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void;
1078+
$onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<any>;
1079+
$onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void;
10751080
}
10761081

10771082
export interface ObjectIdentifier {

src/vs/workbench/api/common/extHostFileSystemEventService.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event';
77
import { IRelativePattern, parse } from 'vs/base/common/glob';
8-
import { URI, UriComponents } from 'vs/base/common/uri';
8+
import { URI } from 'vs/base/common/uri';
99
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
1010
import type * as vscode from 'vscode';
11-
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto } from './extHost.protocol';
11+
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto, SourceTargetPair } from './extHost.protocol';
1212
import * as typeConverter from './extHostTypeConverters';
1313
import { Disposable, WorkspaceEdit } from './extHostTypes';
1414
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -142,16 +142,16 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
142142

143143
//--- file operations
144144

145-
$onDidRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): void {
145+
$onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {
146146
switch (operation) {
147147
case FileOperation.MOVE:
148-
this._onDidRenameFile.fire(Object.freeze({ files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }));
148+
this._onDidRenameFile.fire(Object.freeze({ files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }));
149149
break;
150150
case FileOperation.DELETE:
151-
this._onDidDeleteFile.fire(Object.freeze({ files: [URI.revive(target)] }));
151+
this._onDidDeleteFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
152152
break;
153153
case FileOperation.CREATE:
154-
this._onDidCreateFile.fire(Object.freeze({ files: [URI.revive(target)] }));
154+
this._onDidCreateFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
155155
break;
156156
default:
157157
//ignore, dont send
@@ -179,16 +179,16 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
179179
};
180180
}
181181

182-
async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise<any> {
182+
async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<any> {
183183
switch (operation) {
184184
case FileOperation.MOVE:
185-
await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }, timeout, token);
185+
await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token);
186186
break;
187187
case FileOperation.DELETE:
188-
await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] }, timeout, token);
188+
await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
189189
break;
190190
case FileOperation.CREATE:
191-
await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] }, timeout, token);
191+
await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
192192
break;
193193
default:
194194
//ignore, dont send

src/vs/workbench/contrib/files/browser/fileActions.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Action } from 'vs/base/common/actions';
1515
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
1616
import { VIEWLET_ID, IExplorerService, IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files';
1717
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
18-
import { IFileService } from 'vs/platform/files/common/files';
18+
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
1919
import { toResource, SideBySideEditor } from 'vs/workbench/common/editor';
2020
import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet';
2121
import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput';
@@ -222,7 +222,7 @@ async function deleteFiles(workingCopyFileService: IWorkingCopyFileService, dial
222222

223223
// Call function
224224
try {
225-
await Promise.all(distinctElements.map(e => workingCopyFileService.delete(e.resource, { useTrash: useTrash, recursive: true })));
225+
await workingCopyFileService.delete(distinctElements.map(e => e.resource), { useTrash, recursive: true });
226226
} catch (error) {
227227

228228
// Handle error to delete file(s) from a modal confirmation dialog
@@ -947,7 +947,7 @@ export const renameHandler = async (accessor: ServicesAccessor) => {
947947
const targetResource = resources.joinPath(parentResource, value);
948948
if (stat.resource.toString() !== targetResource.toString()) {
949949
try {
950-
await workingCopyFileService.move(stat.resource, targetResource);
950+
await workingCopyFileService.move([{ source: stat.resource, target: targetResource }]);
951951
await refreshIfSeparator(value, explorerService);
952952
} catch (e) {
953953
notificationService.error(e);
@@ -1033,7 +1033,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => {
10331033
defaultUri
10341034
});
10351035
if (destination) {
1036-
await workingCopyFileService.copy(s.resource, destination, true);
1036+
await workingCopyFileService.copy([{ source: s.resource, target: destination }], true);
10371037
} else {
10381038
// User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100
10391039
canceled = true;
@@ -1060,14 +1060,13 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
10601060
const toPaste = resources.distinctParents(await clipboardService.readResources(), r => r);
10611061
const element = context.length ? context[0] : explorerService.roots[0];
10621062

1063-
// Check if target is ancestor of pasted folder
1064-
const stats = await Promise.all(toPaste.map(async fileToPaste => {
1065-
1066-
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
1067-
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
1068-
}
1063+
try {
1064+
// Check if target is ancestor of pasted folder
1065+
const sourceTargetPairs = await Promise.all(toPaste.map(async fileToPaste => {
10691066

1070-
try {
1067+
if (element.resource.toString() !== fileToPaste.toString() && resources.isEqualOrParent(element.resource, fileToPaste)) {
1068+
throw new Error(nls.localize('fileIsAncestor', "File to paste is an ancestor of the destination folder"));
1069+
}
10711070
const fileToPasteStat = await fileService.resolve(fileToPaste);
10721071

10731072
// Find target
@@ -1081,30 +1080,33 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
10811080
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
10821081
const targetFile = findValidPasteFileTarget(explorerService, target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming);
10831082

1084-
// Move/Copy File
1085-
if (pasteShouldMove) {
1086-
return await workingCopyFileService.move(fileToPaste, targetFile);
1087-
} else {
1088-
return await workingCopyFileService.copy(fileToPaste, targetFile);
1089-
}
1090-
} catch (e) {
1091-
onError(notificationService, new Error(nls.localize('fileDeleted', "The file to paste has been deleted or moved since you copied it. {0}", getErrorMessage(e))));
1092-
return undefined;
1083+
return { source: fileToPaste, target: targetFile };
1084+
}));
1085+
1086+
// Move/Copy File
1087+
let stats: IFileStatWithMetadata[] = [];
1088+
if (pasteShouldMove) {
1089+
stats = await workingCopyFileService.move(sourceTargetPairs);
1090+
} else {
1091+
stats = await workingCopyFileService.copy(sourceTargetPairs);
10931092
}
1094-
}));
10951093

1096-
if (pasteShouldMove) {
1097-
// Cut is done. Make sure to clear cut state.
1098-
await explorerService.setToCopy([], false);
1099-
pasteShouldMove = false;
1100-
}
1101-
if (stats.length >= 1) {
1102-
const stat = stats[0];
1103-
if (stat && !stat.isDirectory && stats.length === 1) {
1104-
await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
1094+
if (stats.length >= 1) {
1095+
const stat = stats[0];
1096+
if (stat && !stat.isDirectory && stats.length === 1) {
1097+
await editorService.openEditor({ resource: stat.resource, options: { pinned: true, preserveFocus: true } });
1098+
}
1099+
if (stat) {
1100+
await explorerService.select(stat.resource);
1101+
}
11051102
}
1106-
if (stat) {
1107-
await explorerService.select(stat.resource);
1103+
} catch (e) {
1104+
onError(notificationService, new Error(nls.localize('fileDeleted', "The file(s) to paste have been deleted or moved since you copied them. {0}", getErrorMessage(e))));
1105+
} finally {
1106+
if (pasteShouldMove) {
1107+
// Cut is done. Make sure to clear cut state.
1108+
await explorerService.setToCopy([], false);
1109+
pasteShouldMove = false;
11081110
}
11091111
}
11101112
};

src/vs/workbench/contrib/files/browser/views/explorerViewer.ts

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,20 @@ const getFileOverwriteConfirm = (name: string) => {
723723
};
724724
};
725725

726+
const getMultipleFilesOverwriteConfirm = (files: URI[]) => {
727+
if (files.length > 1) {
728+
return <IConfirmation>{
729+
message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length),
730+
detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"),
731+
primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
732+
type: 'warning'
733+
};
734+
} else {
735+
return getFileOverwriteConfirm(basename(files[0]));
736+
}
737+
738+
};
739+
726740
interface IWebkitDataTransfer {
727741
items: IWebkitDataTransferItem[];
728742
}
@@ -1010,7 +1024,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
10101024
continue;
10111025
}
10121026

1013-
await this.workingCopyFileService.delete(joinPath(target.resource, entry.name), { recursive: true });
1027+
await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true });
10141028
}
10151029

10161030
// Upload entry
@@ -1263,7 +1277,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
12631277
const sourceFile = resource;
12641278
const targetFile = joinPath(target.resource, basename(sourceFile));
12651279

1266-
const stat = await this.workingCopyFileService.copy(sourceFile, targetFile, true);
1280+
const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], true))[0];
12671281
// if we only add one file, just open it directly
12681282
if (resources.length === 1 && !stat.isDirectory) {
12691283
this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
@@ -1310,7 +1324,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
13101324
}
13111325

13121326
const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
1313-
await Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise));
1327+
await Promise.all([this.doHandleExplorerDrop(items.filter(s => !s.isRoot), target, isCopy), rootDropPromise]);
13141328
}
13151329

13161330
private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
@@ -1346,36 +1360,39 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
13461360
return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
13471361
}
13481362

1349-
private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
1350-
// Reuse duplicate action if user copies
1351-
if (isCopy) {
1352-
const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1353-
const stat = await this.workingCopyFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
1354-
if (!stat.isDirectory) {
1355-
await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
1356-
}
1363+
private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise<void> {
1364+
// Reuse duplicate action when user copies
1365+
const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1366+
const sourceTargetPairs = sources.map(({ resource, isDirectory }) => ({ source: resource, target: findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming) }));
1367+
const editors = (await this.workingCopyFileService.copy(sourceTargetPairs)).filter(stat => !stat.isDirectory).map(({ resource }) => ({ resource, options: { pinned: true } }));
1368+
await this.editorService.openEditors(editors);
1369+
}
13571370

1358-
return;
1359-
}
1371+
private async doHandleExplorerDropOnMove(sources: ExplorerItem[], target: ExplorerItem): Promise<void> {
13601372

1361-
// Otherwise move
1362-
const targetResource = joinPath(target.resource, source.name);
1363-
if (source.isReadonly) {
1364-
// Do not allow moving readonly items
1365-
return Promise.resolve();
1366-
}
1373+
// Do not allow moving readonly items
1374+
const sourceTargetPairs = sources.filter(source => !source.isReadonly).map(source => ({ source: source.resource, target: joinPath(target.resource, source.name) }));
13671375

13681376
try {
1369-
await this.workingCopyFileService.move(source.resource, targetResource);
1377+
await this.workingCopyFileService.move(sourceTargetPairs);
13701378
} catch (error) {
13711379
// Conflict
13721380
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
1373-
const confirm = getFileOverwriteConfirm(source.name);
1381+
1382+
const overwrites: URI[] = [];
1383+
for (const { target } of sourceTargetPairs) {
1384+
if (await this.fileService.exists(target)) {
1385+
overwrites.push(target);
1386+
}
1387+
}
1388+
1389+
const confirm = getMultipleFilesOverwriteConfirm(overwrites);
1390+
13741391
// Move with overwrite if the user confirms
13751392
const { confirmed } = await this.dialogService.confirm(confirm);
13761393
if (confirmed) {
13771394
try {
1378-
await this.workingCopyFileService.move(source.resource, targetResource, true /* overwrite */);
1395+
await this.workingCopyFileService.move(sourceTargetPairs, true /* overwrite */);
13791396
} catch (error) {
13801397
this.notificationService.error(error);
13811398
}
@@ -1388,6 +1405,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
13881405
}
13891406
}
13901407

1408+
private async doHandleExplorerDrop(sources: ExplorerItem[], target: ExplorerItem, isCopy: boolean): Promise<void> {
1409+
if (isCopy) {
1410+
return this.doHandleExplorerDropOnCopy(sources, target);
1411+
} else {
1412+
return this.doHandleExplorerDropOnMove(sources, target);
1413+
}
1414+
}
1415+
13911416
private static getStatsFromDragAndDropData(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, dragStartEvent?: DragEvent): ExplorerItem[] {
13921417
if (data.context) {
13931418
return data.context;

src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class RenameOperation implements IFileOperation {
4646
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
4747
return new Noop(); // not overwriting, but ignoring, and the target file exists
4848
}
49-
await this._workingCopyFileService.move(this.oldUri, this.newUri, this.options.overwrite);
49+
await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], this.options.overwrite);
5050
return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService);
5151
}
5252
}
@@ -109,7 +109,7 @@ class DeleteOperation implements IFileOperation {
109109
}
110110

111111
const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash');
112-
await this._workingCopyFileService.delete(this.oldUri, { useTrash, recursive: this.options.recursive });
112+
await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive });
113113
return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, contents);
114114
}
115115
}

src/vs/workbench/services/textfile/browser/textFileService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
155155
async create(resource: URI, value?: string | ITextSnapshot | VSBuffer, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
156156

157157
// file operation participation
158-
await this.workingCopyFileService.runFileOperationParticipants(resource, undefined, FileOperation.CREATE);
158+
await this.workingCopyFileService.runFileOperationParticipants([{ target: resource, source: undefined }], FileOperation.CREATE);
159159

160160
// create file on disk
161161
const stat = await this.doCreate(resource, value, options);
@@ -246,7 +246,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
246246
// However, this will only work if the source exists
247247
// and is not orphaned, so we need to check that too.
248248
if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) {
249-
await this.workingCopyFileService.move(source, target);
249+
await this.workingCopyFileService.move([{ source, target }]);
250250

251251
return this.save(target, options);
252252
}

0 commit comments

Comments
 (0)