Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File operation events support multiple resources #98988

Merged
merged 34 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7d8a39f
refactor: use array of resources
Jun 1, 2020
359a103
refactor: use an array of uricomponentspair
Jun 1, 2020
69b6766
feat: move many resources
Jun 4, 2020
c657067
refactor: rename data to files
Jun 6, 2020
9a5c688
feat: use array of files for copy
Jun 6, 2020
41abc4d
refactor: use move with multiple resources
Jun 6, 2020
fa95d9e
refactor: use move method with array of files
Jun 7, 2020
0c9ecfa
refactor: rename data to files
Jun 9, 2020
ae583e5
feat: moveOrCopy array of resources on paste
Jun 9, 2020
590fa1e
refactor: use concise loop syntax
Jun 10, 2020
ff72c7b
test: assert number of events
Jun 10, 2020
c85b38f
refactor: rename uricomponentspair
Jun 11, 2020
270fd94
support multiple files on WorkingCopyFileEvent
Jun 11, 2020
0af49f2
feat: support multiple resources
Jun 11, 2020
5efff11
refactor: make source optional for consistency
Jun 12, 2020
7cf8c1a
refactor: support resources for delete
Jun 12, 2020
cd1664f
test: isolate tests
Jun 12, 2020
db5e4dc
fix: iterate over resources
Jun 12, 2020
8c13021
feat: support operations on delete
Jun 12, 2020
4ad934a
feat: adopt deleting multiple resources
Jun 12, 2020
e615a80
fix: typing and sequential flow of copyservice
Jun 15, 2020
ff63341
fix: typing and naming
Jun 15, 2020
51b6fb7
fix: typing and naming
Jun 15, 2020
a74eb2d
fix: use different message for multiple overwrites
Jun 15, 2020
73f27f7
refactor: naming consistency
Jun 16, 2020
545ef80
fix: use array resources
Jun 22, 2020
3aebc9b
fix: message for multiple overwrites
Jun 22, 2020
5b50555
fix format
bpasero Jun 23, 2020
ee0ba2b
clean up working copy file service
bpasero Jun 23, 2020
e98a9a5
refactor multiple overwrites message helper
Jun 23, 2020
b68b71b
use openeditors to bulk open
Jun 23, 2020
497c934
split drop copy and move
Jun 23, 2020
073936a
add returns
Jun 23, 2020
dd1a7a5
Merge branch 'master' into fix/#98309
bpasero Jun 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/vs/workbench/api/browser/mainThreadDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 7 additions & 2 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
$onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void;
$onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<any>;
$onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void;
}

export interface ObjectIdentifier {
Expand Down
20 changes: 10 additions & 10 deletions src/vs/workbench/api/common/extHostFileSystemEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -179,16 +179,16 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
};
}

async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise<any> {
async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<any> {
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
Expand Down
66 changes: 34 additions & 32 deletions src/vs/workbench/contrib/files/browser/fileActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
pfongkye marked this conversation as resolved.
Show resolved Hide resolved
} else {
// User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100
canceled = true;
Expand All @@ -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 => {

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 {
// Check if target is ancestor of pasted folder
const sourceTargetPairs = await Promise.all(toPaste.map(async fileToPaste => {

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
Expand All @@ -1081,30 +1080,33 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => {
const incrementalNaming = configurationService.getValue<IFilesConfiguration>().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 };
}));

// Move/Copy File
let stats: IFileStatWithMetadata[] = [];
if (pasteShouldMove) {
stats = await workingCopyFileService.move(sourceTargetPairs);
} else {
stats = await workingCopyFileService.copy(sourceTargetPairs);
}
}));

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 } });
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);
}
}
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;
}
}
};
Expand Down
69 changes: 47 additions & 22 deletions src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,20 @@ const getFileOverwriteConfirm = (name: string) => {
};
};

const getMultipleFilesOverwriteConfirm = (files: URI[]) => {
if (files.length > 1) {
return <IConfirmation>{
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[];
}
Expand Down Expand Up @@ -1010,7 +1024,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
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
Expand Down Expand Up @@ -1263,7 +1277,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
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 } });
Expand Down Expand Up @@ -1310,7 +1324,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}

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<void> {
Expand Down Expand Up @@ -1346,36 +1360,39 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
}

private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
// Reuse duplicate action if user copies
if (isCopy) {
const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().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<void> {
// Reuse duplicate action when user copies
const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().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<void> {

// 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 ((<FileOperationError>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);
}
Expand All @@ -1388,6 +1405,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
}

private async doHandleExplorerDrop(sources: ExplorerItem[], target: ExplorerItem, isCopy: boolean): Promise<void> {
if (isCopy) {
return this.doHandleExplorerDropOnCopy(sources, target);
} else {
return this.doHandleExplorerDropOnMove(sources, target);
}
}

private static getStatsFromDragAndDropData(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, dragStartEvent?: DragEvent): ExplorerItem[] {
if (data.context) {
return data.context;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -109,7 +109,7 @@ class DeleteOperation implements IFileOperation {
}

const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('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);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/services/textfile/browser/textFileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
async create(resource: URI, value?: string | ITextSnapshot | VSBuffer, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {

// 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);
Expand Down Expand Up @@ -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);
}
Expand Down
Loading