diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index 78e0fa9cd13..a4297cb4f53 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -139,6 +139,8 @@ export class InlineCompletionManager implements Disposable { await ImportAdderProvider.instance.onAcceptRecommendation(editor, item, startLine) } this.sessionManager.incrementSuggestionCount() + // clear session manager states once accepted + this.sessionManager.clear() } commands.registerCommand('aws.amazonq.acceptInline', onInlineAcceptance) @@ -166,6 +168,8 @@ export class InlineCompletionManager implements Disposable { }, } this.languageClient.sendNotification(this.logSessionResultMessageName, params) + // clear session manager states once rejected + this.sessionManager.clear() } commands.registerCommand('aws.amazonq.rejectCodeSuggestion', onInlineRejection) } @@ -179,6 +183,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem private readonly inlineTutorialAnnotation: InlineTutorialAnnotation ) {} + private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' provideInlineCompletionItems = debounce( this._provideInlineCompletionItems.bind(this), inlineCompletionsDebounceDelay, @@ -191,6 +196,10 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem context: InlineCompletionContext, token: CancellationToken ): Promise { + // prevent concurrent API calls and write to shared state variables + if (vsCodeState.isRecommendationsActive) { + return [] + } try { vsCodeState.isRecommendationsActive = true const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic @@ -199,6 +208,24 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem return [] } + // report suggestion state for previous suggestions if they exist + const prevSessionId = this.sessionManager.getActiveSession()?.sessionId + const prevItemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId + if (prevSessionId && prevItemId) { + const params: LogInlineCompletionSessionResultsParams = { + sessionId: prevSessionId, + completionSessionResult: { + [prevItemId]: { + seen: true, + accepted: false, + discarded: false, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + } + // tell the tutorial that completions has been triggered await this.inlineTutorialAnnotation.triggered(context.triggerKind) TelemetryHelper.instance.setInvokeSuggestionStartTime() @@ -213,6 +240,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem ) // get active item from session for displaying const items = this.sessionManager.getActiveRecommendation() + const itemId = this.sessionManager.getActiveRecommendation()?.[0]?.itemId const session = this.sessionManager.getActiveSession() const editor = window.activeTextEditor @@ -229,24 +257,72 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem } const cursorPosition = document.validatePosition(position) - for (const item of items) { - item.command = { - command: 'aws.amazonq.acceptInline', - title: 'On acceptance', - arguments: [ - session.sessionId, - item, - editor, - session.requestStartTime, - cursorPosition.line, - session.firstCompletionDisplayLatency, - ], + + if (position.isAfter(editor.selection.active)) { + getLogger().debug(`Cursor moved behind trigger position. Discarding suggestion...`) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, } - item.range = new Range(cursorPosition, cursorPosition) + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + return [] + } + + // the user typed characters from invoking suggestion cursor position to receiving suggestion position + const typeahead = document.getText(new Range(position, editor.selection.active)) + + const itemsMatchingTypeahead = [] + + for (const item of items) { item.insertText = typeof item.insertText === 'string' ? item.insertText : item.insertText.value - ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) + if (item.insertText.startsWith(typeahead)) { + item.command = { + command: 'aws.amazonq.acceptInline', + title: 'On acceptance', + arguments: [ + session.sessionId, + item, + editor, + session.requestStartTime, + cursorPosition.line, + session.firstCompletionDisplayLatency, + ], + } + item.range = new Range(cursorPosition, cursorPosition) + itemsMatchingTypeahead.push(item) + ImportAdderProvider.instance.onShowRecommendation(document, cursorPosition.line, item) + } + } + + // report discard if none of suggestions match typeahead + if (itemsMatchingTypeahead.length === 0) { + getLogger().debug( + `Suggestion does not match user typeahead from insertion position. Discarding suggestion...` + ) + const params: LogInlineCompletionSessionResultsParams = { + sessionId: session.sessionId, + completionSessionResult: { + [itemId]: { + seen: false, + accepted: false, + discarded: true, + }, + }, + } + this.languageClient.sendNotification(this.logSessionResultMessageName, params) + this.sessionManager.clear() + return [] } - return items as InlineCompletionItem[] + + // suggestions returned here will be displayed on screen + return itemsMatchingTypeahead as InlineCompletionItem[] } catch (e) { getLogger('amazonqLsp').error('Failed to provide completion items: %O', e) return [] diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index a8bc854c97f..fbc28feefbb 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -20,7 +20,12 @@ import { AmazonQInlineCompletionItemProvider, InlineCompletionManager } from '.. import { RecommendationService } from '../../../../../src/app/inline/recommendationService' import { SessionManager } from '../../../../../src/app/inline/sessionManager' import { createMockDocument, createMockTextEditor, getTestWindow, installFakeClock } from 'aws-core-vscode/test' -import { noInlineSuggestionsMsg, ReferenceHoverProvider, ReferenceLogViewProvider } from 'aws-core-vscode/codewhisperer' +import { + noInlineSuggestionsMsg, + ReferenceHoverProvider, + ReferenceLogViewProvider, + vsCodeState, +} from 'aws-core-vscode/codewhisperer' import { InlineGeneratingMessage } from '../../../../../src/app/inline/inlineGeneratingMessage' import { LineTracker } from '../../../../../src/app/inline/stateTracker/lineTracker' import { InlineTutorialAnnotation } from '../../../../../src/app/inline/tutorials/inlineTutorialAnnotation' @@ -41,7 +46,7 @@ describe('InlineCompletionManager', () => { let hoverReferenceStub: sinon.SinonStub const mockDocument = createMockDocument() const mockEditor = createMockTextEditor() - const mockPosition = { line: 0, character: 0 } as Position + const mockPosition = new Position(0, 0) const mockContext = { triggerKind: 1, selectedCompletionInfo: undefined } const mockToken = { isCancellationRequested: false } as CancellationToken const fakeReferences = [ @@ -61,6 +66,11 @@ describe('InlineCompletionManager', () => { insertText: 'test', references: fakeReferences, }, + { + itemId: 'test-item2', + insertText: 'import math\ndef two_sum(nums, target):\n', + references: fakeReferences, + }, ] beforeEach(() => { @@ -240,10 +250,11 @@ describe('InlineCompletionManager', () => { const activeStateController = new InlineGeneratingMessage(lineTracker) inlineTutorialAnnotation = new InlineTutorialAnnotation(lineTracker, mockSessionManager) recommendationService = new RecommendationService(mockSessionManager, activeStateController) - + vsCodeState.isRecommendationsActive = false mockSessionManager = { getActiveSession: getActiveSessionStub, getActiveRecommendation: getActiveRecommendationStub, + clear: () => {}, } as unknown as SessionManager getActiveSessionStub.returns({ @@ -257,7 +268,7 @@ describe('InlineCompletionManager', () => { getAllRecommendationsStub.resolves() sandbox.stub(window, 'activeTextEditor').value(createMockTextEditor()) }), - it('should call recommendation service to get new suggestions for new sessions', async () => { + it('should call recommendation service to get new suggestions(matching typeahead) for new sessions', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, @@ -271,7 +282,7 @@ describe('InlineCompletionManager', () => { mockToken ) assert(getAllRecommendationsStub.calledOnce) - assert.deepStrictEqual(items, mockSuggestions) + assert.deepStrictEqual(items, [mockSuggestions[1]]) }), it('should handle reference if there is any', async () => { provider = new AmazonQInlineCompletionItemProvider( @@ -319,10 +330,13 @@ describe('InlineCompletionManager', () => { mockSessionManager, inlineTutorialAnnotation ) - const expectedText = 'this is my text' + const expectedText = `${mockSuggestions[1].insertText}this is my text` getActiveRecommendationStub.returns([ { - insertText: { kind: 'snippet', value: 'this is my text' } satisfies StringValue, + insertText: { + kind: 'snippet', + value: `${mockSuggestions[1].insertText}this is my text`, + } satisfies StringValue, itemId: 'itemId', }, ]) @@ -379,7 +393,7 @@ describe('InlineCompletionManager', () => { const p2 = provider.provideInlineCompletionItems(mockDocument, mockPosition, mockContext, mockToken) const p3 = provider.provideInlineCompletionItems( mockDocument, - new Position(2, 2), + new Position(1, 26), mockContext, mockToken ) @@ -394,7 +408,7 @@ describe('InlineCompletionManager', () => { const r3 = await p3 // calls the function with the latest provided args. - assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(2, 2)) + assert.deepStrictEqual((r3 as InlineCompletionItem[])[0].range?.end, new Position(1, 26)) }) }) })