Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 91 additions & 15 deletions packages/amazonq/src/app/inline/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
Expand All @@ -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,
Expand All @@ -191,6 +196,10 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
context: InlineCompletionContext,
token: CancellationToken
): Promise<InlineCompletionItem[]> {
// prevent concurrent API calls and write to shared state variables
if (vsCodeState.isRecommendationsActive) {
return []
}
try {
vsCodeState.isRecommendationsActive = true
const isAutoTrigger = context.triggerKind === InlineCompletionTriggerKind.Automatic
Expand All @@ -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()
Expand All @@ -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

Expand All @@ -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 []
Expand Down
32 changes: 23 additions & 9 deletions packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 = [
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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',
},
])
Expand Down Expand Up @@ -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
)
Expand All @@ -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))
})
})
})
Expand Down
Loading