From 69fd7afc56b4c4f793981d6b0cfbeb06cc1ae613 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 16 Jul 2020 10:50:52 +0200 Subject: [PATCH] throttle save participants in certain cases (#102542) --- .../textfile/common/textFileEditorModel.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index b0a885ddd0657..1bb7ac6975d25 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -75,6 +75,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private bufferSavedVersionId: number | undefined; private ignoreDirtyOnModelContentChange = false; + private static readonly UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD = 500; + private lastModelContentChangeFromUndoRedo: number | undefined = undefined; + private lastResolvedFileStat: IFileStatWithMetadata | undefined; private readonly saveSequentializer = new TaskSequentializer(); @@ -450,16 +453,23 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // where `value` was captured in the content change listener closure scope. // Content Change - this._register(model.onDidChangeContent(() => this.onModelContentChanged(model))); + this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); } - private onModelContentChanged(model: ITextModel): void { + private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { this.logService.trace(`[text file model] onModelContentChanged() - enter`, this.resource.toString(true)); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; this.logService.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString(true)); + // Remember when the user changed the model through a undo/redo operation. + // We need this information to throttle save participants to fix + // https://github.com/microsoft/vscode/issues/102542 + if (isUndoingOrRedoing) { + this.lastModelContentChangeFromUndoRedo = Date.now(); + } + // We mark check for a dirty-state change upon model content change, unless: // - explicitly instructed to ignore it (e.g. from model.load()) // - the model is readonly (in that case we never assume the change was done by the user) @@ -639,7 +649,31 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save participants can also be skipped through API. if (this.isResolved() && !options.skipSaveParticipants) { try { - await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); + + // Measure the time it took from the last undo/redo operation to this save. If this + // time is below `UNDO_REDO_SAVE_PARTICIPANTS_THROTTLE_THRESHOLD`, we make sure to + // delay the save participant for the remaining time if the reason is auto save. + // + // This fixes the following issue: + // - the user has configured auto save with delay of 100ms or shorter + // - the user has a save participant enabled that modifies the file on each save + // - the user types into the file and the file gets saved + // - the user triggers undo operation + // - this will undo the save participant change but trigger the save participant right after + // - the user has no chance to undo over the save participant + // + // Reported as: https://github.com/microsoft/vscode/issues/102542 + if (options.reason === SaveReason.AUTO && typeof this.lastModelContentChangeFromUndoRedo === 'number') { + const timeFromUndoRedoToSave = Date.now() - this.lastModelContentChangeFromUndoRedo; + if (timeFromUndoRedoToSave < TextFileEditorModel.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD) { + await timeout(TextFileEditorModel.UNDO_REDO_SAVE_PARTICIPANTS_AUTO_SAVE_THROTTLE_THRESHOLD - timeFromUndoRedoToSave); + } + } + + // Run save participants unless save was cancelled meanwhile + if (!saveCancellation.token.isCancellationRequested) { + await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); + } } catch (error) { this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true)); }