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: When exactly does run, is it only when an item gets highlighted in the menu? Can't tell from the docs. I only need to generate for the completion item the user ends up applying -- doing it for other items is unnecessary work that's bogging down the extension host, and I will have to implement yielding. I assume no one would need to generate before a completion item is applied? Is there any kind of completion applied event we can hook into to work around this for the time being?
That's exactly what is about . Today, there is no guarantee that is ever called because its original design is to provide data for the suggest details widget. However, we see that (amongst other things) are computed during resolve and leads to subtle bugs (because don't always call resolve or always await it) Well, is the problem if you do not generate them upfront. select suggestion synchronously inserts the string starts for additional text edits continues to type quickly and all over the place -call eventually returns but it is unclear if the edits are still valid because the state changed many types in-between Solutions: Won't work because in JS you cannot wait for another "thread" or "process", the world always keeps turning Implement a full -solution because this is two entities "typing" concurrently in the same document. However, it will feel awkward because the user doesn't expect to be such a scenario. approach that tries to apply additional edits if possible but inserts the "main edit" (insertText) always synchronously. () "if possible" is currently defined as "no edit has occurred before the first additional edit". That works well if you use additional edits for things like imports or prefixes (which is the indented use-case) but won't work if additional text edits change the main edit, like appending a call-operators with arguments etc
I mean JS can't block but ignoring typing while awaiting is possible -- I was imagining that VSCode would keep the completions menu open and ignore any typing until is resolved and all text edits applied for the accepted item. Obviously this would be bad if resolving takes a long time, but the API has cancelation tokens already, so the UI would just need to show a spinner/cancel icon, and also cancel if the user presses ESC or navigates to a different editor, for a decent UX.