Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadSaveParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,9 @@ export class SaveParticipant implements ISaveParticipant {
this._saveParticipants.dispose();
}

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

const cts = new CancellationTokenSource();
const cts = new CancellationTokenSource(token);

return this._progressService.withProgress({
title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })),
Expand All @@ -383,7 +383,7 @@ export class SaveParticipant implements ISaveParticipant {
break;
}
try {
const promise = p.participate(model, env, progress, cts.token);
const promise = p.participate(model, context, progress, cts.token);
await raceCancellation(promise, cts.token);
} catch (err) {
this._logService.warn(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

interface IPendingSave {
versionId: number;
cancel: () => void;
promise: Promise<void>;
}

Expand Down Expand Up @@ -35,8 +36,12 @@ export class SaveSequentializer {
return this._pendingSave ? this._pendingSave.promise : undefined;
}

setPending(versionId: number, promise: Promise<void>): Promise<void> {
this._pendingSave = { versionId, promise };
cancelPending(): void {
this._pendingSave?.cancel();
}

setPending(versionId: number, promise: Promise<void>, onCancel?: () => void, ): Promise<void> {
this._pendingSave = { versionId, cancel: () => onCancel?.(), promise };

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

Expand Down
13 changes: 11 additions & 2 deletions src/vs/workbench/services/textfile/common/textFileEditorModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/w
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer';
import { ILabelService } from 'vs/platform/label/common/label';
import { CancellationTokenSource } from 'vs/base/common/cancellation';

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

// Indicate to the save sequentializer that we want to
// 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.
this.saveSequentializer.cancelPending();

// Register this as the next upcoming save and return
return this.saveSequentializer.setNext(() => this.doSave(options));
}
Expand All @@ -616,6 +624,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// In addition we update our version right after in case it changed because of a model change
//
// Save participants can also be skipped through API.
const saveParticipantCancellation = new CancellationTokenSource();
let saveParticipantPromise: Promise<number> = Promise.resolve(versionId);
if (this.isResolved() && this.textFileService.saveParticipant && !options.skipSaveParticipants) {
const onCompleteOrError = () => {
Expand All @@ -625,7 +634,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
};

this.ignoreDirtyOnModelContentChange = true;
saveParticipantPromise = this.textFileService.saveParticipant.participate(this, { reason: options.reason }).then(onCompleteOrError, onCompleteOrError);
saveParticipantPromise = this.textFileService.saveParticipant.participate(this, { reason: options.reason }, saveParticipantCancellation.token).then(onCompleteOrError, onCompleteOrError);
}

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

private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/services/textfile/common/textfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types';
import { isNative } from 'vs/base/common/platform';
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { CancellationToken } from 'vs/base/common/cancellation';

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

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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,34 @@ suite('Files - TextFileEditorModel', () => {
await model.save();
model.dispose();
});

test('Save Participant, participant cancelled when saved again', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);

let participations: boolean[] = [];

accessor.textFileService.saveParticipant = {
participate: (model) => {
return timeout(10).then(() => {
participations.push(true);
});
}
};

await model.load();

model.textEditorModel!.setValue('foo');
const p1 = model.save();

model.textEditorModel!.setValue('foo 1');
const p2 = model.save();

model.textEditorModel!.setValue('foo 2');
await model.save();

await p1;
await p2;
assert.equal(participations.length, 1);
model.dispose();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,14 @@ suite('Files - SaveSequentializer', () => {
assert.ok(!secondDone);
assert.ok(thirdDone);
});

test('SaveSequentializer - cancel pending', async function () {
const sequentializer = new SaveSequentializer();

let pendingCancelled = false;
sequentializer.setPending(1, timeout(1), () => pendingCancelled = true);
sequentializer.cancelPending();

assert.ok(pendingCancelled);
});
});