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: Seems unlikely that people would want to get the main edit without the additional edits By making the operation cancelable, you can still go back to typing immediately if you so desire. I certainly wouldn't want to block the user from typing without recourse (as you know, I feel frustrated that happens currently when using Vim extensions) On the other hand, I guess it's unlikely that the additional text edits would overlap whatever the user continues typing...
I hadn't thought about auto accept characters though, that's a good point. That's something I don't have experience with. Also considering that VSCode could pop up a warning if it has to bail on inserting additional edits, it's not a big deal if it ever happens
I remember that we have some new data points regarding the time consumption in different stages of code completion in Can you share that data and see how we can tune it with this change?
For Java language server, below is a sample of call tree when triggering completion. (time value is larger than expected because of extra overhead from JProfiler) ! Total time of completion stage mainly consists of three parts: competion candidates in JDT to CompletionItems (calc textEdits, additionalTextEdits, etc.) --- calc an incomplete list of only 50 items by default (the full list can contain thousands) --- here calculating import statements costs a lot of time server-side code snippets (not much related to this topic) I did a simple experiment on java language server, with vscode-insider built on 2020-5-15. I was using spring-petclinic, which is not a big one, but with some dependencies. Type a initial letter, e.g. "O", and observe perf of completion and resolve. Do multiple times for statistics. Calculating additionalTextEdits async in resolve stage does decrease the time cost a lot. textEdits and additionalTextEdits are calculated during completion stage. completion: ~150 ms resolve(avg of top 10 items): ~10 ms Now I simply move a bunch of textedit calculation from "completion" stage into "resolve" stage. completion: ~100 ms resolve(avg of top 10 items): ~17 ms !
I just realized that can return a thenable, but I assume that would delay the appearance of the entire menu?
Closing as we haven't seen issues with the new pragmatic approach and the plan is to stick with it.