Skip to content

Commit

Permalink
change create, delete, copy, and rename operations so that they can h…
Browse files Browse the repository at this point in the history
…andle multiple files at once, #111867
  • Loading branch information
jrieken committed Jan 6, 2021
1 parent fa593d8 commit 16da2c5
Showing 1 changed file with 152 additions and 71 deletions.
223 changes: 152 additions & 71 deletions src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { WorkspaceFileEditOptions } from 'vs/editor/common/modes';
import { IFileService, FileSystemProviderCapabilities, IFileContent } from 'vs/platform/files/common/files';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkingCopyFileService, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService, UndoRedoGroup, UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer } from 'vs/base/common/buffer';
import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
import * as resources from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { flatten } from 'vs/base/common/arrays';

interface IFileOperation {
uris: URI[];
Expand All @@ -31,115 +31,172 @@ class Noop implements IFileOperation {
}
}

interface RenameOrCopyEdit {
readonly newUri: URI,
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions
}

class RenameOperation implements IFileOperation {

constructor(
readonly newUri: URI,
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions,
readonly undoRedoInfo: IFileOperationUndoRedoInfo,
private readonly _edits: RenameOrCopyEdit[],
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IFileService private readonly _fileService: IFileService,
) { }

get uris() {
return [this.newUri, this.oldUri];
return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri]));
}

async perform(token: CancellationToken): Promise<IFileOperation> {
// rename
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

const moves: IMoveOperation[] = [];
const undoes: RenameOrCopyEdit[] = [];
for (const edit of this._edits) {
// check: not overwriting, but ignoring, and the target file exists
const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri);
if (!skip) {
moves.push({
file: { source: edit.oldUri, target: edit.newUri },
overwrite: edit.options.overwrite
});

// reverse edit
undoes.push({
newUri: edit.oldUri,
oldUri: edit.newUri,
options: edit.options
});
}
}

await this._workingCopyFileService.move([{ file: { source: this.oldUri, target: this.newUri }, overwrite: this.options.overwrite }], this.undoRedoInfo, token);
return new RenameOperation(this.oldUri, this.newUri, this.options, { isUndoing: true }, this._workingCopyFileService, this._fileService);
if (moves.length === 0) {
return new Noop();
}

await this._workingCopyFileService.move(moves, this._undoRedoInfo, token);
return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService);
}

toString(): string {
const oldBasename = resources.basename(this.oldUri);
const newBasename = resources.basename(this.newUri);
if (oldBasename !== newBasename) {
return `(rename ${oldBasename} to ${newBasename})`;
}
return `(rename ${this.oldUri} to ${this.newUri})`;
return `(rename ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`;
}
}

class CopyOperation implements IFileOperation {

constructor(
readonly newUri: URI,
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions,
readonly undoRedoInfo: IFileOperationUndoRedoInfo,
private readonly _edits: RenameOrCopyEdit[],
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IFileService private readonly _fileService: IFileService,
@IInstantiationService private readonly _instaService: IInstantiationService
) { }

get uris() {
return [this.newUri, this.oldUri];
return flatten(this._edits.map(edit => [edit.newUri, edit.oldUri]));
}

async perform(token: CancellationToken): Promise<IFileOperation> {
// copy
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

// (1) create copy operations, remove noops
const copies: ICopyOperation[] = [];
for (const edit of this._edits) {
//check: not overwriting, but ignoring, and the target file exists
const skip = edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri);
if (!skip) {
copies.push({ file: { source: edit.oldUri, target: edit.newUri }, overwrite: edit.options.overwrite });
}
}

if (copies.length === 0) {
return new Noop();
}

const stat = await this._workingCopyFileService.copy([{ file: { source: this.oldUri, target: this.newUri }, overwrite: this.options.overwrite }], this.undoRedoInfo, token);
const folder = this.options.folder || (stat.length === 1 && stat[0].isDirectory);
return this._instaService.createInstance(DeleteOperation, this.newUri, { recursive: true, folder, ...this.options }, { isUndoing: true }, false);
// (2) perform the actual copy and use the return stats to build undo edits
const stats = await this._workingCopyFileService.copy(copies, this._undoRedoInfo, token);
const undoes: DeleteEdit[] = [];

for (let i = 0; i < stats.length; i++) {
const stat = stats[i];
const edit = this._edits[i];
undoes.push({
oldUri: stat.resource,
options: { recursive: true, folder: this._edits[i].options.folder || stat.isDirectory, ...edit.options },
undoesCreate: false
});
}

return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true });
}

toString(): string {
return `(copy ${this.oldUri} to ${this.newUri})`;
return `(copy ${this._edits.map(edit => `${edit.oldUri} to ${edit.newUri}`).join(', ')})`;
}
}

interface CreateEdit {
readonly newUri: URI;
readonly options: WorkspaceFileEditOptions,
readonly contents: VSBuffer | undefined,
}

class CreateOperation implements IFileOperation {

constructor(
readonly newUri: URI,
readonly options: WorkspaceFileEditOptions,
readonly undoRedoInfo: IFileOperationUndoRedoInfo,
readonly contents: VSBuffer | undefined,
private readonly _edits: CreateEdit[],
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
@IFileService private readonly _fileService: IFileService,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IInstantiationService private readonly _instaService: IInstantiationService,
) { }

get uris() {
return [this.newUri];
return this._edits.map(edit => edit.newUri);
}

async perform(token: CancellationToken): Promise<IFileOperation> {
// create file
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
}
if (this.options.folder) {
await this._workingCopyFileService.createFolder({ resource: this.newUri }, this.undoRedoInfo, token);
} else {
await this._workingCopyFileService.create({ resource: this.newUri, contents: this.contents, overwrite: this.options.overwrite }, this.undoRedoInfo, token);

const undoes: DeleteEdit[] = [];

for (const edit of this._edits) {
if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
continue; // not overwriting, but ignoring, and the target file exists
}
if (edit.options.folder) {
await this._workingCopyFileService.createFolder({ resource: edit.newUri }, this._undoRedoInfo, token);
} else {
await this._workingCopyFileService.create({ resource: edit.newUri, contents: edit.contents, overwrite: edit.options.overwrite }, this._undoRedoInfo, token);
}

undoes.push({
oldUri: edit.newUri,
options: edit.options,
undoesCreate: !edit.options.folder && !edit.contents
});
}
return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, !this.options.folder && !this.contents);

return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true });
}

toString(): string {
return this.options.folder ? `create ${resources.basename(this.newUri)} folder`
: `(create ${resources.basename(this.newUri)} with ${this.contents?.byteLength || 0} bytes)`;
return `(create ${this._edits.map(edit => edit.options.folder ? `folder ${edit.newUri}` : `file ${edit.newUri} with ${edit.contents?.byteLength || 0} bytes`).join(', ')})`;
}
}

interface DeleteEdit {
readonly oldUri: URI;
readonly options: WorkspaceFileEditOptions;
readonly undoesCreate: boolean;
}

class DeleteOperation implements IFileOperation {

constructor(
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions,
readonly undoRedoInfo: IFileOperationUndoRedoInfo,
private readonly _undoesCreateOperation: boolean,
private _edits: DeleteEdit[],
private readonly _undoRedoInfo: IFileOperationUndoRedoInfo,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IFileService private readonly _fileService: IFileService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
Expand All @@ -148,38 +205,62 @@ class DeleteOperation implements IFileOperation {
) { }

get uris() {
return [this.oldUri];
return this._edits.map(edit => edit.oldUri);
}

async perform(token: CancellationToken): Promise<IFileOperation> {
// delete file
if (!await this._fileService.exists(this.oldUri)) {
if (!this.options.ignoreIfNotExists) {
throw new Error(`${this.oldUri} does not exist and can not be deleted`);

const deletes: IDeleteOperation[] = [];
const undoes: CreateEdit[] = [];

for (const edit of this._edits) {
if (!await this._fileService.exists(edit.oldUri)) {
if (!edit.options.ignoreIfNotExists) {
throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
}
continue;
}
return new Noop();
}

let fileContent: IFileContent | undefined;
if (!this._undoesCreateOperation && !this.options.folder) {
try {
fileContent = await this._fileService.readFile(this.oldUri);
} catch (err) {
this._logService.critical(err);
deletes.push({
resource: edit.oldUri,
recursive: edit.options.recursive,
useTrash: !edit.options.skipTrashBin && this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash')
});


// read file contents for undo operation. when a file is too large it won't be restored
let fileContent: IFileContent | undefined;
if (!edit.undoesCreate && !edit.options.folder) {
try {
fileContent = await this._fileService.readFile(edit.oldUri);
} catch (err) {
this._logService.critical(err);
}
}
if (!(typeof edit.options.maxSize === 'number' && fileContent && (fileContent?.size > edit.options.maxSize))) {
undoes.push({
newUri: edit.oldUri,
options: edit.options,
contents: fileContent?.value,
});
}
}

if (deletes.length === 0) {
return new Noop();
}

const useTrash = !this.options.skipTrashBin && this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash');
await this._workingCopyFileService.delete([{ resource: this.oldUri, useTrash, recursive: this.options.recursive }], this.undoRedoInfo, token);
await this._workingCopyFileService.delete(deletes, this._undoRedoInfo, token);

if (typeof this.options.maxSize === 'number' && fileContent && (fileContent?.size > this.options.maxSize)) {
if (undoes.length === 0) {
return new Noop();
}
return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, { isUndoing: true }, fileContent?.value);
return this._instaService.createInstance(CreateOperation, undoes, { isUndoing: true });
}

toString(): string {
return `(delete ${resources.basename(this.oldUri)})`;
return `(delete ${this._edits.map(edit => edit.oldUri).join(', ')})`;
}
}

Expand Down Expand Up @@ -243,15 +324,15 @@ export class BulkFileEdits {
let op: IFileOperation | undefined;
if (edit.newResource && edit.oldResource && !options.copy) {
// rename
op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options, undoRedoInfo);
op = this._instaService.createInstance(RenameOperation, [{ newUri: edit.newResource, oldUri: edit.oldResource, options }], undoRedoInfo);
} else if (edit.newResource && edit.oldResource && options.copy) {
op = this._instaService.createInstance(CopyOperation, edit.newResource, edit.oldResource, options, undoRedoInfo);
op = this._instaService.createInstance(CopyOperation, [{ newUri: edit.newResource, oldUri: edit.oldResource, options }], undoRedoInfo);
} else if (!edit.newResource && edit.oldResource) {
// delete file
op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options, undoRedoInfo, false);
op = this._instaService.createInstance(DeleteOperation, [{ oldUri: edit.oldResource, options, undoesCreate: false }], undoRedoInfo);
} else if (edit.newResource && !edit.oldResource) {
// create file
op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undoRedoInfo, undefined);
op = this._instaService.createInstance(CreateOperation, [{ newUri: edit.newResource, options, contents: undefined }], undoRedoInfo,);
}
if (op) {
const undoOp = await op.perform(this._token);
Expand Down

0 comments on commit 16da2c5

Please sign in to comment.