Skip to content

Commit b989a6a

Browse files
authored
Investigate to cancel a running save participant when saving again (#90590)
* Investigate to cancel a running save participant when saving again (fix #90509) * keep token source
1 parent df59697 commit b989a6a

File tree

6 files changed

+63
-8
lines changed

6 files changed

+63
-8
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,9 @@ export class SaveParticipant implements ISaveParticipant {
365365
this._saveParticipants.dispose();
366366
}
367367

368-
async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise<void> {
368+
async participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
369369

370-
const cts = new CancellationTokenSource();
370+
const cts = new CancellationTokenSource(token);
371371

372372
return this._progressService.withProgress({
373373
title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })),
@@ -383,7 +383,7 @@ export class SaveParticipant implements ISaveParticipant {
383383
break;
384384
}
385385
try {
386-
const promise = p.participate(model, env, progress, cts.token);
386+
const promise = p.participate(model, context, progress, cts.token);
387387
await raceCancellation(promise, cts.token);
388388
} catch (err) {
389389
this._logService.warn(err);

src/vs/workbench/services/textfile/common/saveSequenzializer.ts

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

66
interface IPendingSave {
77
versionId: number;
8+
cancel: () => void;
89
promise: Promise<void>;
910
}
1011

@@ -35,8 +36,12 @@ export class SaveSequentializer {
3536
return this._pendingSave ? this._pendingSave.promise : undefined;
3637
}
3738

38-
setPending(versionId: number, promise: Promise<void>): Promise<void> {
39-
this._pendingSave = { versionId, promise };
39+
cancelPending(): void {
40+
this._pendingSave?.cancel();
41+
}
42+
43+
setPending(versionId: number, promise: Promise<void>, onCancel?: () => void, ): Promise<void> {
44+
this._pendingSave = { versionId, cancel: () => onCancel?.(), promise };
4045

4146
promise.then(() => this.donePending(versionId), () => this.donePending(versionId));
4247

src/vs/workbench/services/textfile/common/textFileEditorModel.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/w
2424
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
2525
import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer';
2626
import { ILabelService } from 'vs/platform/label/common/label';
27+
import { CancellationTokenSource } from 'vs/base/common/cancellation';
2728

2829
interface IBackupMetaData {
2930
mtime: number;
@@ -601,6 +602,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
601602
if (this.saveSequentializer.hasPendingSave()) {
602603
this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString());
603604

605+
// Indicate to the save sequentializer that we want to
606+
// cancel the pending operation so that ours can run
607+
// before the pending one finishes.
608+
// Currently this will try to cancel pending save
609+
// participants but never a pending save.
610+
this.saveSequentializer.cancelPending();
611+
604612
// Register this as the next upcoming save and return
605613
return this.saveSequentializer.setNext(() => this.doSave(options));
606614
}
@@ -616,6 +624,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
616624
// In addition we update our version right after in case it changed because of a model change
617625
//
618626
// Save participants can also be skipped through API.
627+
const saveParticipantCancellation = new CancellationTokenSource();
619628
let saveParticipantPromise: Promise<number> = Promise.resolve(versionId);
620629
if (this.isResolved() && this.textFileService.saveParticipant && !options.skipSaveParticipants) {
621630
const onCompleteOrError = () => {
@@ -625,7 +634,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
625634
};
626635

627636
this.ignoreDirtyOnModelContentChange = true;
628-
saveParticipantPromise = this.textFileService.saveParticipant.participate(this, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
637+
saveParticipantPromise = this.textFileService.saveParticipant.participate(this, { reason: options.reason }, saveParticipantCancellation.token).then(onCompleteOrError, onCompleteOrError);
629638
}
630639

631640
// mark the save participant as current pending save operation
@@ -678,7 +687,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
678687
etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, this.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag,
679688
writeElevated: options.writeElevated
680689
}).then(stat => this.handleSaveSuccess(stat, versionId, options), error => this.handleSaveError(error, versionId, options)));
681-
}));
690+
}), () => saveParticipantCancellation.cancel());
682691
}
683692

684693
private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void {

src/vs/workbench/services/textfile/common/textfiles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types';
1616
import { isNative } from 'vs/base/common/platform';
1717
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
1818
import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
19+
import { CancellationToken } from 'vs/base/common/cancellation';
1920

2021
export const ITextFileService = createDecorator<ITextFileService>('textFileService');
2122

@@ -230,7 +231,7 @@ export interface ISaveParticipant {
230231
/**
231232
* Participate in a save of a model. Allows to change the model before it is being saved to disk.
232233
*/
233-
participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }): Promise<void>;
234+
participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason }, token: CancellationToken): Promise<void>;
234235
}
235236

236237
/**

src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,34 @@ suite('Files - TextFileEditorModel', () => {
556556
await model.save();
557557
model.dispose();
558558
});
559+
560+
test('Save Participant, participant cancelled when saved again', async function () {
561+
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
562+
563+
let participations: boolean[] = [];
564+
565+
accessor.textFileService.saveParticipant = {
566+
participate: (model) => {
567+
return timeout(10).then(() => {
568+
participations.push(true);
569+
});
570+
}
571+
};
572+
573+
await model.load();
574+
575+
model.textEditorModel!.setValue('foo');
576+
const p1 = model.save();
577+
578+
model.textEditorModel!.setValue('foo 1');
579+
const p2 = model.save();
580+
581+
model.textEditorModel!.setValue('foo 2');
582+
await model.save();
583+
584+
await p1;
585+
await p2;
586+
assert.equal(participations.length, 1);
587+
model.dispose();
588+
});
559589
});

src/vs/workbench/services/textfile/test/common/saveSequenzializer.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,14 @@ suite('Files - SaveSequentializer', () => {
8787
assert.ok(!secondDone);
8888
assert.ok(thirdDone);
8989
});
90+
91+
test('SaveSequentializer - cancel pending', async function () {
92+
const sequentializer = new SaveSequentializer();
93+
94+
let pendingCancelled = false;
95+
sequentializer.setPending(1, timeout(1), () => pendingCancelled = true);
96+
sequentializer.cancelPending();
97+
98+
assert.ok(pendingCancelled);
99+
});
90100
});

0 commit comments

Comments
 (0)