Code change:
assert.equal(editor.getValue(), '    let name = foo'); }); <ins> test('use additionalTextEdits sync when possible', async function () { disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos), additionalTextEdits: [{ text: 'I came sync', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } }] }] }; }, async resolveCompletionItem(item) { return item; } })); editor.setValue('hellonhallo'); editor.setSelection(new Selection(2, 6, 2, 6)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(false, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'I came synchellonhallohello'); }); test('resolve additionalTextEdits async when needed', async function () { let resolveCallCount = 0; disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos) }] }; }, async resolveCompletionItem(item) { resolveCallCount += 1; await timeout(10); item.additionalTextEdits = [{ text: 'I came late', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } }]; return item; } })); editor.setValue('hellonhallo'); editor.setSelection(new Selection(2, 6, 2, 6)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(false, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'hellonhallohello'); assert.equal(resolveCallCount, 1); // additional edits happened after a litte wait await timeout(20); assert.equal(editor.getValue(), 'I came latehellonhallohello'); // single undo stop editor.getModel()?.undo(); assert.equal(editor.getValue(), 'hellonhallo'); }); test('resolve additionalTextEdits async when needed (typing)', async function () { let resolveCallCount = 0; let resolve: Function = () => { }; disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos) }] }; }, async resolveCompletionItem(item) { resolveCallCount += 1; await new Promise(_resolve => resolve = _resolve); item.additionalTextEdits = [{ text: 'I came late', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } }]; return item; } })); editor.setValue('hellonhallo'); editor.setSelection(new Selection(2, 6, 2, 6)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(false, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'hellonhallohello'); assert.equal(resolveCallCount, 1); // additional edits happened after a litte wait assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 11, 2, 11))); editor.trigger('test', 'type', { text: 'TYPING' }); assert.equal(editor.getValue(), 'hellonhallohelloTYPING'); resolve(); await timeout(10); assert.equal(editor.getValue(), 'I came latehellonhallohelloTYPING'); assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 17, 2, 17))); }); // additional edit come late and are AFTER the selection -> cancel test('resolve additionalTextEdits async when needed (simple conflict)', async function () { let resolveCallCount = 0; let resolve: Function = () => { }; disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos) }] }; }, async resolveCompletionItem(item) { resolveCallCount += 1; await new Promise(_resolve => resolve = _resolve); item.additionalTextEdits = [{ text: 'I came late', range: { startLineNumber: 1, startColumn: 6, endLineNumber: 1, endColumn: 6 } }]; return item; } })); editor.setValue(''); editor.setSelection(new Selection(1, 1, 1, 1)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(false, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'hello'); assert.equal(resolveCallCount, 1); resolve(); await timeout(10); assert.equal(editor.getValue(), 'hello'); }); // additional edit come late and are AFTER the position at which the user typed -> cancelled test('resolve additionalTextEdits async when needed (conflict)', async function () { let resolveCallCount = 0; let resolve: Function = () => { }; disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos) }] }; }, async resolveCompletionItem(item) { resolveCallCount += 1; await new Promise(_resolve => resolve = _resolve); item.additionalTextEdits = [{ text: 'I came late', range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 } }]; return item; } })); editor.setValue('hellonhallo'); editor.setSelection(new Selection(2, 6, 2, 6)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(false, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'hellonhallohello'); assert.equal(resolveCallCount, 1); // additional edits happened after a litte wait editor.setSelection(new Selection(1, 1, 1, 1)); editor.trigger('test', 'type', { text: 'TYPING' }); assert.equal(editor.getValue(), 'TYPINGhellonhallohello'); resolve(); await timeout(10); assert.equal(editor.getValue(), 'TYPINGhellonhallohello'); assert.ok(editor.getSelection()?.equalsSelection(new Selection(1, 7, 1, 7))); }); test('resolve additionalTextEdits async when needed (cancel)', async function () { let resolve: Function[] = []; disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { provideCompletionItems(doc, pos) { return { suggestions: [{ kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hello', range: Range.fromPositions(pos) }, { kind: CompletionItemKind.Snippet, label: 'let', insertText: 'hallo', range: Range.fromPositions(pos) }] }; }, async resolveCompletionItem(item) { await new Promise(_resolve => resolve.push(_resolve)); item.additionalTextEdits = [{ text: 'additionalTextEdits', range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 } }]; return item; } })); editor.setValue('abc'); editor.setSelection(new Selection(1, 1, 1, 1)); // trigger let p1 = Event.toPromise(controller.model.onDidSuggest); controller.triggerSuggest(); await p1; // let p2 = Event.toPromise(controller.model.onDidCancel); controller.acceptSelectedSuggestion(true, false); await p2; // insertText happens sync! assert.equal(editor.getValue(), 'helloabc'); // next controller.acceptNextSuggestion(); // resolve additional edits (MUST be cancelled) resolve.forEach(fn => fn); resolve.length = 0; await timeout(10); // next suggestion used assert.equal(editor.getValue(), 'halloabc'); }); </ins> });

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.