file working copy - better cancellation support

This commit is contained in:
Benjamin Pasero 2021-03-29 08:26:55 +02:00
parent 5e3df4ce30
commit 7f9835ac43
No known key found for this signature in database
GPG key ID: E6380CC4C8219E65
13 changed files with 146 additions and 76 deletions

View file

@ -942,7 +942,7 @@ export class TaskSequentializer {
}
setPending(taskId: number, promise: Promise<void>, onCancel?: () => void,): Promise<void> {
this._pending = { taskId: taskId, cancel: () => onCancel?.(), promise };
this._pending = { taskId, cancel: () => onCancel?.(), promise };
promise.then(() => this.donePending(taskId), () => this.donePending(taskId));

View file

@ -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);
}

View file

@ -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();

View file

@ -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<IFileStatWithMetadata[]> {
async create(operations: { resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions }[], undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
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<IFileStatWithMetadata> {
@ -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

View file

@ -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<IFileStatWithMetadata[]>;
create(operations: { resource: URI, value?: string | ITextSnapshot, options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]>;
/**
* Returns the readable that uses the appropriate encoding.

View file

@ -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<T extends IFileWorkingCopyModel> 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<T extends IFileWorkingCopyModel> 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<T extends IFileWorkingCopyModel> 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<T extends IFileWorkingCopyModel> extends Disposable
} catch (error) {
this.handleSaveError(error, versionId, options);
}
})());
})(), () => saveCancellation.cancel());
})(), () => saveCancellation.cancel());
}
@ -1248,8 +1256,12 @@ export class FileWorkingCopy<T extends IFileWorkingCopyModel> 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());
}

View file

@ -626,7 +626,7 @@ export class FileWorkingCopyManager<T extends IFileWorkingCopyModel> 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

View file

@ -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<void> {
async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken): Promise<void> {
const timeout = this.configurationService.getValue<number>('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);
}

View file

@ -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<IFileStatWithMetadata[]>;
create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]>;
/**
* 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<IFileStatWithMetadata[]>;
createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]>;
/**
* 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<IFileStatWithMetadata[]>;
move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]>;
/**
* 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<IFileStatWithMetadata[]>;
copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]>;
/**
* 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<void>;
delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<void>;
//#endregion
@ -272,15 +272,15 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
//#region File operations
create(operations: ICreateFileOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> {
return this.doCreateFileOrFolder(operations, true, undoInfo, token);
create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
return this.doCreateFileOrFolder(operations, true, token, undoInfo);
}
createFolder(operations: ICreateOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> {
return this.doCreateFileOrFolder(operations, false, undoInfo, token);
createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
return this.doCreateFileOrFolder(operations, false, token, undoInfo);
}
async doCreateFileOrFolder(operations: (ICreateFileOperation | ICreateOperation)[], isFile: boolean, undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> {
async doCreateFileOrFolder(operations: (ICreateFileOperation | ICreateOperation)[], isFile: boolean, token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
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<IFileStatWithMetadata[]> {
return this.doMoveOrCopy(operations, true, undoInfo, token);
async move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
return this.doMoveOrCopy(operations, true, token, undoInfo);
}
async copy(operations: ICopyOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> {
return this.doMoveOrCopy(operations, false, undoInfo, token);
async copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
return this.doMoveOrCopy(operations, false, token, undoInfo);
}
private async doMoveOrCopy(operations: IMoveOperation[] | ICopyOperation[], move: boolean, undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> {
private async doMoveOrCopy(operations: IMoveOperation[] | ICopyOperation[], move: boolean, token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> {
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<void> {
async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<void> {
// 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<void> {
private runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, token: CancellationToken): Promise<void> {
return this.fileOperationParticipants.participate(files, operation, undoInfo, token);
}

View file

@ -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);

View file

@ -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');

View file

@ -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<unknown> = 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<number> {
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();

View file

@ -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<void> { }
async delete(operations: IDeleteOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<void> { }
registerWorkingCopyProvider(provider: (resourceOrFolder: URI) => IWorkingCopy[]): IDisposable { return Disposable.None; }
getDirty(resource: URI): IWorkingCopy[] { return []; }
create(operations: ICreateFileOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
createFolder(operations: ICreateOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
create(operations: ICreateFileOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
createFolder(operations: ICreateOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
move(operations: IMoveOperation[], undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
move(operations: IMoveOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
copy(operations: ICopyOperation[], undoInfo?: IFileOperationUndoRedoInfo, token?: CancellationToken): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
copy(operations: ICopyOperation[], token: CancellationToken, undoInfo?: IFileOperationUndoRedoInfo): Promise<IFileStatWithMetadata[]> { throw new Error('Method not implemented.'); }
}
export function mock<T>(): Ctor<T> {