diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index a82b809d8fa..c3920223c2d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -376,16 +376,18 @@ class InlineCompletionsState extends Disposable { return new InlineCompletionsState(newInlineCompletions, this.request); } - public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserve: InlineSuggestionIdentity | undefined): InlineCompletionsState { - let updatedItems: InlineSuggestionItem[] = []; - + public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState { let itemToPreserve: InlineSuggestionItem | undefined = undefined; - if (itemIdToPreserve) { - const preserveCandidate = this._findById(itemIdToPreserve); - if (preserveCandidate) { - const updatedSuggestionsHasItemToPreserve = updatedSuggestions.some(i => i.hash === preserveCandidate.hash); - if (!updatedSuggestionsHasItemToPreserve && preserveCandidate.canBeReused(textModel, request.position)) { - itemToPreserve = preserveCandidate; + if (itemIdToPreserveAtTop) { + const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop); + if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) { + itemToPreserve = itemToPreserveCandidate; + + const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash); + if (updatedItemToPreserve) { + updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions); + } else { + updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions]; } } } @@ -396,22 +398,20 @@ class InlineCompletionsState extends Disposable { // Otherwise: prefer inline completion if there is a visible one : updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition)); + const updatedItems: InlineSuggestionItem[] = []; for (const i of updatedSuggestions) { const oldItem = this._findByHash(i.hash); - if (oldItem) { - updatedItems.push(i.withIdentity(oldItem.identity)); + let item; + if (oldItem && oldItem !== i) { + item = i.withIdentity(oldItem.identity); oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() }); } else { - updatedItems.push(i); + item = i; + } + if (preferInlineCompletions !== item.isInlineEdit) { + updatedItems.push(item); } } - - if (itemToPreserve) { - updatedItems.unshift(itemToPreserve); - } - - updatedItems = preferInlineCompletions ? updatedItems.filter(i => !i.isInlineEdit) : updatedItems.filter(i => i.isInlineEdit); - return new InlineCompletionsState(updatedItems, request); } @@ -419,3 +419,11 @@ class InlineCompletionsState extends Disposable { return new InlineCompletionsState(this.inlineCompletions, this.request); } } + +function moveToFront(item: T, items: T[]): T[] { + const index = items.indexOf(item); + if (index > -1) { + return [item, ...items.slice(0, index), ...items.slice(index + 1)]; + } + return items; +} diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts index e1266b26613..1e4f0f7c934 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts @@ -420,6 +420,48 @@ suite('Inline Completions', () => { } ); }); + + test('Push item to preserve to front', async function () { + const provider = new MockInlineCompletionsProvider(true); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider }, + async ({ editor, editorViewModel, model, context }) => { + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); + context.keyboardType('foo'); + await timeout(1000); + + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([ + { + position: "(1,4)", + triggerKind: 0, + text: "foo" + } + ])); + assert.deepStrictEqual(context.getAndClearViewStates(), + ([ + "", + "foo[bar]" + ]) + ); + + provider.setReturnValues([{ insertText: 'foobar1', range: new Range(1, 1, 1, 4) }, { insertText: 'foobar', range: new Range(1, 1, 1, 4) }]); + + await model.triggerExplicitly(); + await timeout(1000); + + assert.deepStrictEqual(provider.getAndClearCallHistory(), ([ + { + position: "(1,4)", + triggerKind: 1, + text: "foo" + } + ])); + assert.deepStrictEqual(context.getAndClearViewStates(), + ([]) + ); + } + ); + }); }); test('No race conditions', async function () { diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index 02b29e4b16c..7659ab5223a 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -32,6 +32,10 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider private callHistory = new Array(); private calledTwiceIn50Ms = false; + constructor( + public readonly enableForwardStability = false, + ) { } + public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void { this.returnValue = value ? [value] : []; this.delayMs = delayMs; @@ -56,7 +60,7 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider private lastTimeMs: number | undefined = undefined; - async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken) { + async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise { const currentTimeMs = new Date().getTime(); if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) { this.calledTwiceIn50Ms = true; @@ -81,7 +85,7 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider await timeout(this.delayMs); } - return { items: result }; + return { items: result, enableForwardStability: this.enableForwardStability }; } disposeInlineCompletions() { } handleItemDidShow() { }