Code change:
// keep item in memory this._memoryService.memorize(model, this.editor.getPosition(), item); <del> const scrollState = StableEditorScrollState.capture(this.editor); </del> if (Array.isArray(item.completion.additionalTextEdits)) { <del> this.editor.executeEdits('suggestController.additionalTextEdits', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); </del> <ins> // sync additional edits const scrollState = StableEditorScrollState.capture(this.editor); this.editor.executeEdits( 'suggestController.additionalTextEdits.sync', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)) ); scrollState.restoreRelativeVerticalPositionOfCursor(this.editor); } else if (!item.isResolved) { // async additional edits let position: IPosition | undefined; const docListener = model.onDidChangeContent(e => { if (e.isFlush) { cts.cancel(); docListener.dispose(); return; } for (let change of e.changes) { const thisPosition = Range.getEndPosition(change.range); if (!position || Position.isBefore(thisPosition, position)) { position = thisPosition; } } }); let oldFlags = flags; flags |= InsertFlags.NoAfterUndoStop; let didType = false; let typeListener = this.editor.onWillType(() => { typeListener.dispose(); didType = true; if (!(oldFlags & InsertFlags.NoAfterUndoStop)) { this.editor.pushUndoStop(); } }); tasks.push(item.resolve(cts.token).then(() => { if (!item.completion.additionalTextEdits || cts.token.isCancellationRequested) { return; } if (position && item.completion.additionalTextEdits.some(edit => Position.isBefore(position!, Range.getStartPosition(edit.range)))) { return; } if (didType) { this.editor.pushUndoStop(); } this.editor.executeEdits( 'suggestController.additionalTextEdits.async', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)) ); if (didType || !(oldFlags & InsertFlags.NoAfterUndoStop)) { this.editor.pushUndoStop(); } }).finally(() => { docListener.dispose(); typeListener.dispose(); })); </ins> } let { insertText } = item.completion;

Related discussion:
text: I think this would be less confusing for users too; it's the only way it would be clear when VSCode is completely done applying the completion. I think many users would be confused if imports usually get but not when they resume typing too soon. FWIW I can syncronously generate the additional text edits I need, so I'd be happy if there were an API to synchronously generate them when the completion item is accepted. I know not everyone would be able to generate them sync though.
Also I know my questions may have seemed redundant, but I hadn't seen anyone on this thread point out that it would be ideal to avoid ever computing additional text edits (or even the primary insert text) for unselected completions. Since can get called before any item is accepted (I assume, since it seems to be for showing additional details in the menu), I'd think any solution via could still result in unnecessarily generating additional edits that are ultimately discarded.
The golden rule is that "you can always type inside the editor". Ignoring typing is not a good solution and will bug users. Just think of quick suggest and auto accept characters - you don't want the editor to stop typing just because an extension is taking a little longer (+50ms). To be precise you should have written "typing at or before the offset at which additional edits are going to happen" I honestly think the majority can generate them synchronously but this happens in the extension host process or the language server process, and all of that might happen on a different machine and there is no way to synchronously transport that into the UI.
re with the current changes you should be able to have a "simple insert text", say , and compute an additional text edit, like , that goes in front of it so that you eventually have . Right? Or am I missing something?
that might actually be a good idea ;-) In practice though, we might need to revisit the way the principal TextEdit is computed (i.e rewrite a bunch of stuff from upstream JDT)
I get the desire to be able to always be able to type inside the editor and agree that's an important principle. The only reason I think this case is an exception is that: The user would probably want to wait for all edits to get applied anyway.