Skip to content

Commit 76649f7

Browse files
authored
fix(amazonq): Correctly report suggestion state. (#7502)
## Problem Suggestion state reporting is incorrect. ## Solution 1. When receiving a suggestion, if the cursor is behind invoking position, DISCARD suggestion 2. When receiving a suggestion, if the cursor is after invoking position and the typeahead does not match DISCARD suggestion. 3. When user does not do anything with a shown suggestion, we REJECT it the next time we invoke for new suggestions. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 1d89d5d commit 76649f7

File tree

2 files changed

+114
-24
lines changed

2 files changed

+114
-24
lines changed

packages/amazonq/src/app/inline/completion.ts

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export class InlineCompletionManager implements Disposable {
139139
await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine)
140140
}
141141
this.sessionManager.incrementSuggestionCount()
142+
// clear session manager states once accepted
143+
this.sessionManager.clear()
142144
}
143145
commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance)
144146

@@ -166,6 +168,8 @@ export class InlineCompletionManager implements Disposable {
166168
},
167169
}
168170
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
171+
// clear session manager states once rejected
172+
this.sessionManager.clear()
169173
}
170174
commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection)
171175
}
@@ -179,6 +183,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
179183
private readonly inlineTutorialAnnotation: InlineTutorialAnnotation
180184
) {}
181185

186+
private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults'
182187
provideInlineCompletionItems = debounce(
183188
this._provideInlineCompletionItems.bind(this),
184189
inlineCompletionsDebounceDelay,
@@ -191,6 +196,10 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
191196
context: InlineCompletionContext,
192197
token: CancellationToken
193198
): Promise<InlineCompletionItem[]> {
199+
// prevent concurrent API calls and write to shared state variables
200+
if (vsCodeState.isRecommendationsActive) {
201+
return []
202+
}
194203
try {
195204
vsCodeState.isRecommendationsActive = true
196205
const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic
@@ -199,6 +208,24 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
199208
return []
200209
}
201210

211+
// report suggestion state for previous suggestions if they exist
212+
const prevSessionId = this.sessionManager.getActiveSession()?.sessionId
213+
const prevItemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId
214+
if (prevSessionId && prevItemId) {
215+
const params: LogInlineCompletionSessionResultsParams = {
216+
sessionId: prevSessionId,
217+
completionSessionResult: {
218+
[prevItemId]: {
219+
seen: true,
220+
accepted: false,
221+
discarded: false,
222+
},
223+
},
224+
}
225+
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
226+
this.sessionManager.clear()
227+
}
228+
202229
// tell the tutorial that completions has been triggered
203230
await this.inlineTutorialAnnotation.triggered(context.triggerKind)
204231
TelemetryHelper.instance.setInvokeSuggestionStartTime()
@@ -213,6 +240,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
213240
)
214241
// get active item from session for displaying
215242
const items = this.sessionManager.getActiveRecommendation()
243+
const itemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId
216244
const session = this.sessionManager.getActiveSession()
217245
const editor = window.activeTextEditor
218246

@@ -229,24 +257,72 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
229257
}
230258

231259
const cursorPosition = document.validatePosition(position)
232-
for (const item of items) {
233-
item.command = {
234-
command: 'aws.amazonq.acceptInline',
235-
title: 'On acceptance',
236-
arguments: [
237-
session.sessionId,
238-
item,
239-
editor,
240-
session.requestStartTime,
241-
cursorPosition.line,
242-
session.firstCompletionDisplayLatency,
243-
],
260+
261+
if (position.isAfter(editor.selection.active)) {
262+
getLogger().debug(`Cursor moved behind trigger position. Discarding suggestion...`)
263+
const params: LogInlineCompletionSessionResultsParams = {
264+
sessionId: session.sessionId,
265+
completionSessionResult: {
266+
[itemId]: {
267+
seen: false,
268+
accepted: false,
269+
discarded: true,
270+
},
271+
},
244272
}
245-
item.range = new Range(cursorPosition, cursorPosition)
273+
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
274+
this.sessionManager.clear()
275+
return []
276+
}
277+
278+
// the user typed characters from invoking suggestion cursor position to receiving suggestion position
279+
const typeahead = document.getText(new Range(position, editor.selection.active))
280+
281+
const itemsMatchingTypeahead = []
282+
283+
for (const item of items) {
246284
item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value
247-
ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item)
285+
if (item.insertText.startsWith(typeahead)) {
286+
item.command = {
287+
command: 'aws.amazonq.acceptInline',
288+
title: 'On acceptance',
289+
arguments: [
290+
session.sessionId,
291+
item,
292+
editor,
293+
session.requestStartTime,
294+
cursorPosition.line,
295+
session.firstCompletionDisplayLatency,
296+
],
297+
}
298+
item.range = new Range(cursorPosition, cursorPosition)
299+
itemsMatchingTypeahead.push(item)
300+
ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item)
301+
}
302+
}
303+
304+
// report discard if none of suggestions match typeahead
305+
if (itemsMatchingTypeahead.length === 0) {
306+
getLogger().debug(
307+
`Suggestion does not match user typeahead from insertion position. Discarding suggestion...`
308+
)
309+
const params: LogInlineCompletionSessionResultsParams = {
310+
sessionId: session.sessionId,
311+
completionSessionResult: {
312+
[itemId]: {
313+
seen: false,
314+
accepted: false,
315+
discarded: true,
316+
},
317+
},
318+
}
319+
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
320+
this.sessionManager.clear()
321+
return []
248322
}
249-
return items as InlineCompletionItem[]
323+
324+
// suggestions returned here will be displayed on screen
325+
return itemsMatchingTypeahead as InlineCompletionItem[]
250326
} catch (e) {
251327
getLogger('amazonqLsp').error('Failed to provide completion items: %O', e)
252328
return []

packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '..
2020
import { RecommendationService } from '../../../../../src/app/inline/recommendationService'
2121
import { SessionManager } from '../../../../../src/app/inline/sessionManager'
2222
import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test'
23-
import { noInlineSuggestionsMsg, ReferenceHoverProvider, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer'
23+
import {
24+
noInlineSuggestionsMsg,
25+
ReferenceHoverProvider,
26+
ReferenceLogViewProvider,
27+
vsCodeState,
28+
} from 'aws-core-vscode/codewhisperer'
2429
import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage'
2530
import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker'
2631
import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation'
@@ -41,7 +46,7 @@ describe('InlineCompletionManager', () => {
4146
let hoverReferenceStub: sinon.SinonStub
4247
const mockDocument = createMockDocument()
4348
const mockEditor = createMockTextEditor()
44-
const mockPosition = { line: 0, character: 0 } as Position
49+
const mockPosition = new Position(0, 0)
4550
const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined }
4651
const mockToken = { isCancellationRequested: false } as CancellationToken
4752
const fakeReferences = [
@@ -61,6 +66,11 @@ describe('InlineCompletionManager', () => {
6166
insertText: 'test',
6267
references: fakeReferences,
6368
},
69+
{
70+
itemId: 'test-item2',
71+
insertText: 'import math\ndef two_sum(nums, target):\n',
72+
references: fakeReferences,
73+
},
6474
]
6575

6676
beforeEach(() => {
@@ -240,10 +250,11 @@ describe('InlineCompletionManager', () => {
240250
const activeStateController = new InlineGeneratingMessage(lineTracker)
241251
inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager)
242252
recommendationService = new RecommendationService(mockSessionManager, activeStateController)
243-
253+
vsCodeState.isRecommendationsActive = false
244254
mockSessionManager = {
245255
getActiveSession: getActiveSessionStub,
246256
getActiveRecommendation: getActiveRecommendationStub,
257+
clear: () => {},
247258
} as unknown as SessionManager
248259

249260
getActiveSessionStub.returns({
@@ -257,7 +268,7 @@ describe('InlineCompletionManager', () => {
257268
getAllRecommendationsStub.resolves()
258269
sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor())
259270
}),
260-
it('should call recommendation service to get new suggestions for new sessions', async () => {
271+
it('should call recommendation service to get new suggestions(matching typeahead) for new sessions', async () => {
261272
provider = new AmazonQInlineCompletionItemProvider(
262273
languageClient,
263274
recommendationService,
@@ -271,7 +282,7 @@ describe('InlineCompletionManager', () => {
271282
mockToken
272283
)
273284
assert(getAllRecommendationsStub.calledOnce)
274-
assert.deepStrictEqual(items, mockSuggestions)
285+
assert.deepStrictEqual(items, [mockSuggestions[1]])
275286
}),
276287
it('should handle reference if there is any', async () => {
277288
provider = new AmazonQInlineCompletionItemProvider(
@@ -319,10 +330,13 @@ describe('InlineCompletionManager', () => {
319330
mockSessionManager,
320331
inlineTutorialAnnotation
321332
)
322-
const expectedText = 'this is my text'
333+
const expectedText = `${mockSuggestions[1].insertText}this is my text`
323334
getActiveRecommendationStub.returns([
324335
{
325-
insertText: { kind: 'snippet', value: 'this is my text' } satisfies StringValue,
336+
insertText: {
337+
kind: 'snippet',
338+
value: `${mockSuggestions[1].insertText}this is my text`,
339+
} satisfies StringValue,
326340
itemId: 'itemId',
327341
},
328342
])
@@ -379,7 +393,7 @@ describe('InlineCompletionManager', () => {
379393
const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken)
380394
const p3 = provider.provideInlineCompletionItems(
381395
mockDocument,
382-
new Position(2, 2),
396+
new Position(1, 26),
383397
mockContext,
384398
mockToken
385399
)
@@ -394,7 +408,7 @@ describe('InlineCompletionManager', () => {
394408
const r3 = await p3
395409

396410
// calls the function with the latest provided args.
397-
assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(2, 2))
411+
assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(1, 26))
398412
})
399413
})
400414
})

0 commit comments

Comments
 (0)