diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 95107d4db85..03fb0d84bc4 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -942,7 +942,7 @@ export class TaskSequentializer { } setPending(taskId: number, promise: Promise, onCancel?: () => void,): Promise { - this._pending = { taskId: taskId, cancel: () => onCancel?.(), promise }; + this._pending = { taskId, cancel: () => onCancel?.(), promise }; promise.then(() => this.donePending(taskId), () => this.donePending(taskId)); diff --git a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index 2eaff5936cc..26368501bdc 100644 --- a/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -204,7 +204,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // Backup does not exist else { const backup = await workingCopy.backup(token); - await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta); + await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta, token); backups.push(workingCopy); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index e15f322a56c..54266f1c5c6 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -76,7 +76,7 @@ class RenameOperation implements IFileOperation { return new Noop(); } - await this._workingCopyFileService.move(moves, this._undoRedoInfo, token); + await this._workingCopyFileService.move(moves, token, this._undoRedoInfo); return new RenameOperation(undoes, { isUndoing: true }, this._workingCopyFileService, this._fileService); } @@ -125,7 +125,7 @@ class CopyOperation implements IFileOperation { } // (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 stats = await this._workingCopyFileService.copy(copies, token, this._undoRedoInfo); const undoes: DeleteEdit[] = []; for (let i = 0; i < stats.length; i++) { @@ -190,8 +190,8 @@ class CreateOperation implements IFileOperation { return new Noop(); } - await this._workingCopyFileService.createFolder(folderCreates, this._undoRedoInfo, token); - await this._workingCopyFileService.create(fileCreates, this._undoRedoInfo, token); + await this._workingCopyFileService.createFolder(folderCreates, token, this._undoRedoInfo); + await this._workingCopyFileService.create(fileCreates, token, this._undoRedoInfo); return this._instaService.createInstance(DeleteOperation, undoes, { isUndoing: true }); } @@ -265,7 +265,7 @@ class DeleteOperation implements IFileOperation { return new Noop(); } - await this._workingCopyFileService.delete(deletes, this._undoRedoInfo, token); + await this._workingCopyFileService.delete(deletes, token, this._undoRedoInfo); if (undoes.length === 0) { return new Noop(); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 610c623c9cc..79b0cbc55c8 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -149,7 +149,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return [bufferStream, decoder]; } - async create(operations: { resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions }[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { + async create(operations: { resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions }[], undoInfo?: IFileOperationUndoRedoInfo): Promise { const operationsWithContents: ICreateFileOperation[] = await Promise.all(operations.map(async o => { const contents = await this.getEncodedReadable(o.resource, o.value); return { @@ -159,7 +159,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex }; })); - return this.workingCopyFileService.create(operationsWithContents, undoInfo, token); + return this.workingCopyFileService.create(operationsWithContents, CancellationToken.None, undoInfo); } async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { @@ -251,7 +251,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([{ file: { source, target } }]); + await this.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); // At this point we don't know whether we have a // model for the source or the target URI so we diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index a63ba33ffea..132dcfac80e 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -95,7 +95,7 @@ export interface ITextFileService extends IDisposable { * Create files. If the file exists it will be overwritten with the contents if * the options enable to overwrite. */ - create(operations: { resource: URI, value?: string | ITextSnapshot, options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + create(operations: { resource: URI, value?: string | ITextSnapshot, options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo): Promise; /** * Returns the readable that uses the appropriate encoding. diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 1eb6cbb3ed5..8fc4a3d398a 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -10,7 +10,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ETAG_DISABLED, FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { TaskSequentializer, timeout } from 'vs/base/common/async'; +import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { DefaultEndOfLine, ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model'; import { assertIsDefined } from 'vs/base/common/types'; @@ -914,7 +914,9 @@ export class FileWorkingCopy extends Disposable // cancel the pending operation so that ours can run // before the pending one finishes. // Currently this will try to cancel pending save - // participants but never a pending save. + // participants and pending snapshots from the + // save operation, but not the actual save which does + // not support cancellation yet. this.saveSequentializer.cancelPending(); // Register this as the next upcoming save and return @@ -970,15 +972,9 @@ export class FileWorkingCopy extends Disposable } // It is possible that a subsequent save is cancelling this - // running save. As such we return early when we detect that - // However, we do not pass the token into the file service - // because that is an atomic operation currently without - // cancellation support, so we dispose the cancellation if - // it was not cancelled yet. + // running save. As such we return early when we detect that. if (saveCancellation.token.isCancellationRequested) { return; - } else { - saveCancellation.dispose(); } // We have to protect against being disposed at this point. It could be that the save() operation @@ -1012,10 +1008,22 @@ export class FileWorkingCopy extends Disposable try { // Snapshot working copy model contents - const snapshot = await resolvedFileWorkingCopy.model.snapshot(CancellationToken.None); + const snapshot = await raceCancellation(resolvedFileWorkingCopy.model.snapshot(saveCancellation.token), saveCancellation.token); + + // It is possible that a subsequent save is cancelling this + // running save. As such we return early when we detect that + // However, we do not pass the token into the file service + // because that is an atomic operation currently without + // cancellation support, so we dispose the cancellation if + // it was not cancelled yet. + if (saveCancellation.token.isCancellationRequested) { + return; + } else { + saveCancellation.dispose(); + } // Write them to disk - const stat = await this.fileService.writeFile(lastResolvedFileStat.resource, snapshot, { + const stat = await this.fileService.writeFile(lastResolvedFileStat.resource, assertIsDefined(snapshot), { mtime: lastResolvedFileStat.mtime, etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag, unlock: options.writeUnlock @@ -1025,7 +1033,7 @@ export class FileWorkingCopy extends Disposable } catch (error) { this.handleSaveError(error, versionId, options); } - })()); + })(), () => saveCancellation.cancel()); })(), () => saveCancellation.cancel()); } @@ -1248,8 +1256,12 @@ export class FileWorkingCopy extends Disposable return undefined; } - const snapshot = await this.model.snapshot(token); - const contents = await streamToBuffer(snapshot); + const snapshot = await raceCancellation(this.model.snapshot(token), token); + if (token.isCancellationRequested) { + return undefined; + } + + const contents = await streamToBuffer(assertIsDefined(snapshot)); return stringToSnapshot(contents.toString()); } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 542e84d9f5f..8f8e2784f2c 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -626,7 +626,7 @@ export class FileWorkingCopyManager extends Dis // Create target file adhoc if it does not exist yet if (!targetExists) { - await this.workingCopyFileService.create([{ resource: target }]); + await this.workingCopyFileService.create([{ resource: target }], CancellationToken.None); } // At this point we need to resolve the target working copy diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts index 87d5005df8a..5c6cc819459 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts @@ -27,7 +27,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable { return toDisposable(() => remove()); } - async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken | undefined): Promise { + async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken): Promise { const timeout = this.configurationService.getValue('files.participants.timeout'); if (timeout <= 0) { return; // disabled @@ -36,7 +36,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable { // For each participant for (const participant of this.participants) { try { - await participant.participate(files, operation, undoInfo, timeout, token ?? CancellationToken.None); + await participant.participate(files, operation, undoInfo, timeout, token); } 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 97865f08d17..e546748bbbc 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts @@ -165,7 +165,7 @@ export interface IWorkingCopyFileService { * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and * `onDidRunWorkingCopyFileOperation` events to participate. */ - create(operations: ICreateFileOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise; /** * Will create a folder and any parent folder that needs to be created. @@ -176,7 +176,7 @@ export interface IWorkingCopyFileService { * Note: events will only be emitted for the provided resource, but not any * parent folders that are being created as part of the operation. */ - createFolder(operations: ICreateOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise; /** * Will move working copies matching the provided resources and corresponding children @@ -185,7 +185,7 @@ export interface IWorkingCopyFileService { * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and * `onDidRunWorkingCopyFileOperation` events to participate. */ - move(operations: IMoveOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise; /** * Will copy working copies matching the provided resources and corresponding children @@ -194,7 +194,7 @@ export interface IWorkingCopyFileService { * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and * `onDidRunWorkingCopyFileOperation` events to participate. */ - copy(operations: ICopyOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise; /** * Will delete working copies matching the provided resources and children @@ -203,7 +203,7 @@ export interface IWorkingCopyFileService { * Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and * `onDidRunWorkingCopyFileOperation` events to participate. */ - delete(operations: IDeleteOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise; + delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise; //#endregion @@ -272,15 +272,15 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi //#region File operations - create(operations: ICreateFileOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { - return this.doCreateFileOrFolder(operations, true, undoInfo, token); + create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { + return this.doCreateFileOrFolder(operations, true, token, undoInfo); } - createFolder(operations: ICreateOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { - return this.doCreateFileOrFolder(operations, false, undoInfo, token); + createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { + return this.doCreateFileOrFolder(operations, false, token, undoInfo); } - async doCreateFileOrFolder(operations: (ICreateFileOperation | ICreateOperation)[], isFile: boolean, undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { + async doCreateFileOrFolder(operations: (ICreateFileOperation | ICreateOperation)[], isFile: boolean, token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { if (operations.length === 0) { return []; } @@ -300,7 +300,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi // before events const event = { correlationId: this.correlationIds++, operation: FileOperation.CREATE, files }; - await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); // now actually create on disk let stats: IFileStatWithMetadata[]; @@ -313,26 +313,26 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi } catch (error) { // error event - await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); throw error; } // after event - await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); return stats; } - async move(operations: IMoveOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { - return this.doMoveOrCopy(operations, true, undoInfo, token); + async move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { + return this.doMoveOrCopy(operations, true, token, undoInfo); } - async copy(operations: ICopyOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { - return this.doMoveOrCopy(operations, false, undoInfo, token); + async copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { + return this.doMoveOrCopy(operations, false, token, undoInfo); } - private async doMoveOrCopy(operations: IMoveOperation[] | ICopyOperation[], move: boolean, undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { + private async doMoveOrCopy(operations: IMoveOperation[] | ICopyOperation[], move: boolean, token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { const stats: IFileStatWithMetadata[] = []; // validate move/copy operation before starting @@ -349,7 +349,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi // before event const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, files }; - await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); try { for (const { file: { source, target }, overwrite } of operations) { @@ -372,18 +372,18 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi } catch (error) { // error event - await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); throw error; } // after event - await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); return stats; } - async delete(operations: IDeleteOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { + async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { // validate delete operation before starting for (const operation of operations) { @@ -399,7 +399,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi // before events const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files }; - await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); // check for any existing dirty working copies for the resource // and do a soft revert before deleting to be able to close @@ -417,13 +417,13 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi } catch (error) { // error event - await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); throw error; } // after event - await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); + await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None /* intentional: we currently only forward cancellation to participants */); } //#endregion @@ -437,7 +437,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi return this.fileOperationParticipants.addFileOperationParticipant(participant); } - private runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken | undefined): Promise { + private runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken): Promise { return this.fileOperationParticipants.participate(files, operation, undoInfo, token); } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 20ea9ca70c5..263e0350d6e 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -404,26 +404,27 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.isDirty(), false); // multiple saves in parallel are fine and result - // in individual save operations when content changes + // in just one save operation (the second one + // cancels the first) workingCopy.model?.updateContents('hello save'); const firstSave = workingCopy.save(); workingCopy.model?.updateContents('hello save more'); const secondSave = workingCopy.save(); await Promises.settled([firstSave, secondSave]); - assert.strictEqual(savedCounter, 5); + assert.strictEqual(savedCounter, 4); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); // no save when not forced and not dirty await workingCopy.save(); - assert.strictEqual(savedCounter, 5); + assert.strictEqual(savedCounter, 4); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); // save when forced even when not dirty await workingCopy.save({ force: true }); - assert.strictEqual(savedCounter, 6); + assert.strictEqual(savedCounter, 5); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); @@ -437,7 +438,7 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); await workingCopy.save({ force: true }); - assert.strictEqual(savedCounter, 7); + assert.strictEqual(savedCounter, 6); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), false); diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 19b66526180..b15b4bb83b0 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -13,6 +13,7 @@ import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { timeout } from 'vs/base/common/async'; import { TestFileWorkingCopyModel, TestFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test'; +import { CancellationToken } from 'vs/base/common/cancellation'; suite('FileWorkingCopyManager', () => { @@ -248,7 +249,7 @@ suite('FileWorkingCopyManager', () => { workingCopy.model?.updateContents('hello create'); assert.strictEqual(workingCopy.isDirty(), true); - await accessor.workingCopyFileService.create([{ resource }]); + await accessor.workingCopyFileService.create([{ resource }], CancellationToken.None); assert.strictEqual(workingCopy.isDirty(), false); }); @@ -269,9 +270,9 @@ suite('FileWorkingCopyManager', () => { assert.strictEqual(sourceWorkingCopy.isDirty(), true); if (move) { - await accessor.workingCopyFileService.move([{ file: { source, target } }]); + await accessor.workingCopyFileService.move([{ file: { source, target } }], CancellationToken.None); } else { - await accessor.workingCopyFileService.copy([{ file: { source, target } }]); + await accessor.workingCopyFileService.copy([{ file: { source, target } }], CancellationToken.None); } const targetWorkingCopy = await manager.resolve(target); @@ -286,7 +287,7 @@ suite('FileWorkingCopyManager', () => { workingCopy.model?.updateContents('hello delete'); assert.strictEqual(workingCopy.isDirty(), true); - await accessor.workingCopyFileService.delete([{ resource }]); + await accessor.workingCopyFileService.delete([{ resource }], CancellationToken.None); assert.strictEqual(workingCopy.isDirty(), false); }); @@ -297,7 +298,7 @@ suite('FileWorkingCopyManager', () => { sourceWorkingCopy.model?.updateContents('hello move'); assert.strictEqual(sourceWorkingCopy.isDirty(), true); - await accessor.workingCopyFileService.move([{ file: { source, target: source } }]); + await accessor.workingCopyFileService.move([{ file: { source, target: source } }], CancellationToken.None); assert.strictEqual(sourceWorkingCopy.isDirty(), true); assert.strictEqual(sourceWorkingCopy.model?.contents, 'hello move'); 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 09fe8d064e2..5e82f847f07 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -14,6 +14,8 @@ import { FileOperation } from 'vs/platform/files/common/files'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { VSBuffer } from 'vs/base/common/buffer'; import { ICopyOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { timeout } from 'vs/base/common/async'; suite('WorkingCopyFileService', () => { @@ -224,7 +226,7 @@ suite('WorkingCopyFileService', () => { eventCounter++; }); - await accessor.workingCopyFileService.createFolder([{ resource }]); + await accessor.workingCopyFileService.createFolder([{ resource }], CancellationToken.None); assert.strictEqual(eventCounter, 3); @@ -233,6 +235,60 @@ suite('WorkingCopyFileService', () => { listener2.dispose(); }); + test('cancellation of participants', async function () { + const resource = toResource.call(this, '/path/folder'); + + let canceled = false; + const participant = accessor.workingCopyFileService.addFileOperationParticipant({ + participate: async (files, operation, info, t, token) => { + await timeout(0); + canceled = token.isCancellationRequested; + } + }); + + // Create + let cts = new CancellationTokenSource(); + let promise: Promise = accessor.workingCopyFileService.create([{ resource }], cts.token); + cts.cancel(); + await promise; + assert.strictEqual(canceled, true); + canceled = false; + + // Create Folder + cts = new CancellationTokenSource(); + promise = accessor.workingCopyFileService.createFolder([{ resource }], cts.token); + cts.cancel(); + await promise; + assert.strictEqual(canceled, true); + canceled = false; + + // Move + cts = new CancellationTokenSource(); + promise = accessor.workingCopyFileService.move([{ file: { source: resource, target: resource } }], cts.token); + cts.cancel(); + await promise; + assert.strictEqual(canceled, true); + canceled = false; + + // Copy + cts = new CancellationTokenSource(); + promise = accessor.workingCopyFileService.copy([{ file: { source: resource, target: resource } }], cts.token); + cts.cancel(); + await promise; + assert.strictEqual(canceled, true); + canceled = false; + + // Delete + cts = new CancellationTokenSource(); + promise = accessor.workingCopyFileService.delete([{ resource }], cts.token); + cts.cancel(); + await promise; + assert.strictEqual(canceled, true); + canceled = false; + + participant.dispose(); + }); + async function testEventsMoveOrCopy(files: ICopyOperation[], move?: boolean): Promise { let eventCounter = 0; @@ -251,9 +307,9 @@ suite('WorkingCopyFileService', () => { }); if (move) { - await accessor.workingCopyFileService.move(files); + await accessor.workingCopyFileService.move(files, CancellationToken.None); } else { - await accessor.workingCopyFileService.copy(files); + await accessor.workingCopyFileService.copy(files, CancellationToken.None); } participant.dispose(); @@ -331,9 +387,9 @@ suite('WorkingCopyFileService', () => { }); if (move) { - await accessor.workingCopyFileService.move(models.map(model => ({ file: { source: model.sourceModel.resource, target: model.targetModel.resource }, options: { overwrite: true } }))); + await accessor.workingCopyFileService.move(models.map(model => ({ file: { source: model.sourceModel.resource, target: model.targetModel.resource }, options: { overwrite: true } })), CancellationToken.None); } else { - await accessor.workingCopyFileService.copy(models.map(model => ({ file: { source: model.sourceModel.resource, target: model.targetModel.resource }, options: { overwrite: true } }))); + await accessor.workingCopyFileService.copy(models.map(model => ({ file: { source: model.sourceModel.resource, target: model.targetModel.resource }, options: { overwrite: true } })), CancellationToken.None); } for (let i = 0; i < models.length; i++) { @@ -407,7 +463,7 @@ suite('WorkingCopyFileService', () => { eventCounter++; }); - await accessor.workingCopyFileService.delete(models.map(m => ({ resource: m.resource }))); + await accessor.workingCopyFileService.delete(models.map(m => ({ resource: m.resource })), CancellationToken.None); for (const model of models) { assert.ok(!accessor.workingCopyService.isDirty(model.resource)); model.dispose(); @@ -459,7 +515,7 @@ suite('WorkingCopyFileService', () => { eventCounter++; }); - await accessor.workingCopyFileService.create([{ resource, contents }]); + await accessor.workingCopyFileService.create([{ resource, contents }], CancellationToken.None); assert.ok(!accessor.workingCopyService.isDirty(model.resource)); model.dispose(); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index e6bd135a3e0..2ac7cf5c29d 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -186,18 +186,18 @@ export class TestWorkingCopyFileService implements IWorkingCopyFileService { addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable { return Disposable.None; } - async delete(operations: IDeleteOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { } + async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { } registerWorkingCopyProvider(provider: (resourceOrFolder: URI) => IWorkingCopy[]): IDisposable { return Disposable.None; } getDirty(resource: URI): IWorkingCopy[] { return []; } - create(operations: ICreateFileOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { throw new Error('Method not implemented.'); } - createFolder(operations: ICreateOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { throw new Error('Method not implemented.'); } + create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { throw new Error('Method not implemented.'); } + createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { throw new Error('Method not implemented.'); } - move(operations: IMoveOperation[], undoInfo?: IFileOperationUndoRedoInfo): Promise { throw new Error('Method not implemented.'); } + move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { throw new Error('Method not implemented.'); } - copy(operations: ICopyOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise { throw new Error('Method not implemented.'); } + copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise { throw new Error('Method not implemented.'); } } export function mock(): Ctor {