Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
39 changes: 18 additions & 21 deletions packages/amazonq/src/app/inline/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,6 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem

// yield event loop to let the document listen catch updates
await sleep(1)
// prevent user deletion invoking auto trigger
// this is a best effort estimate of deletion
if (this.documentEventListener.isLastEventDeletion(document.uri.fsPath)) {
getLogger().debug('Skip auto trigger when deleting code')
return []
}

let logstr = `GenerateCompletion metadata:\\n`
try {
Expand Down Expand Up @@ -370,8 +364,8 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
},
token,
isAutoTrigger,
getAllRecommendationsOptions,
this.documentEventListener.getLastDocumentChangeEvent(document.uri.fsPath)?.event
this.documentEventListener,
getAllRecommendationsOptions
)
// get active item from session for displaying
const items = this.sessionManager.getActiveRecommendation()
Expand Down Expand Up @@ -404,21 +398,24 @@ ${itemLog}

const cursorPosition = document.validatePosition(position)

if (position.isAfter(editor.selection.active)) {
const params: LogInlineCompletionSessionResultsParams = {
sessionId: session.sessionId,
completionSessionResult: {
[itemId]: {
seen: false,
accepted: false,
discarded: true,
// Completion will not be rendered if users cursor moves to a position which is before the position when the service is invoked
if (items.length > 0 && !items[0].isInlineEdit) {
if (position.isAfter(editor.selection.active)) {
const params: LogInlineCompletionSessionResultsParams = {
sessionId: session.sessionId,
completionSessionResult: {
[itemId]: {
seen: false,
accepted: false,
discarded: true,
},
},
},
}
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
this.sessionManager.clear()
logstr += `- cursor moved behind trigger position. Discarding completion suggestion...`
return []
}
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
this.sessionManager.clear()
logstr += `- cursor moved behind trigger position. Discarding suggestion...`
return []
}

// delay the suggestion rendeing if user is actively typing
Expand Down
55 changes: 51 additions & 4 deletions packages/amazonq/src/app/inline/recommendationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
import {
InlineCompletionListWithReferences,
InlineCompletionWithReferencesParams,
inlineCompletionWithReferencesRequestType,
TextDocumentContentChangeEvent,
editCompletionRequestType,
} from '@aws/language-server-runtimes/protocol'
import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode'
import { LanguageClient } from 'vscode-languageclient'
Expand All @@ -21,6 +21,7 @@ import {
import { TelemetryHelper } from './telemetryHelper'
import { ICursorUpdateRecorder } from './cursorUpdateManager'
import { getLogger } from 'aws-core-vscode/shared'
import { DocumentEventListener } from './documentEventListener'
import { getOpenFilesInWindow } from 'aws-core-vscode/utils'
import { asyncCallWithTimeout } from '../../util/timeoutUtil'

Expand Down Expand Up @@ -66,9 +67,11 @@ export class RecommendationService {
context: InlineCompletionContext,
token: CancellationToken,
isAutoTrigger: boolean,
options: GetAllRecommendationsOptions = { emitTelemetry: true, showUi: true },
documentChangeEvent?: vscode.TextDocumentChangeEvent
documentEventListener: DocumentEventListener,
options: GetAllRecommendationsOptions = { emitTelemetry: true, showUi: true }
) {
const documentChangeEvent = documentEventListener?.getLastDocumentChangeEvent(document.uri.fsPath)?.event

// Record that a regular request is being made
this.cursorUpdateRecorder?.recordCompletionRequest()
const documentChangeParams = documentChangeEvent
Expand Down Expand Up @@ -119,7 +122,51 @@ export class RecommendationService {
})
const t0 = performance.now()

const result = await this.getRecommendationsWithTimeout(languageClient, request, token)
// Best effort estimate of deletion
const isTriggerByDeletion = documentEventListener.isLastEventDeletion(document.uri.fsPath)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leigaol fyi, logic to handle trigger when users are deleting the code is moved here


const ps: Promise<InlineCompletionListWithReferences>[] = []
/**
* IsTriggerByDeletion is to prevent user deletion invoking Completions.
* PartialResultToken is not a hack for now since only Edits suggestion use partialResultToken across different calls of [getAllRecommendations],
* Completions use PartialResultToken with single 1 call of [getAllRecommendations].
* Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block.
*/
if (!isTriggerByDeletion && !request.partialResultToken) {
const completionPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
inlineCompletionWithReferencesRequestType.method,
request,
token
)
ps.push(completionPromise)
}

/**
* Though Edit request is sent on keystrokes everytime, the language server will execute the request in a debounced manner so that it won't be immediately executed.
*/
const editPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
editCompletionRequestType.method,
request,
token
)
ps.push(editPromise)

/**
* First come first serve, ideally we should simply return the first response returned. However there are some caviar here because either
* (1) promise might be returned early without going through service
* (2) some users are not enabled with edits suggestion, therefore service will return empty result without passing through the model
* With the scenarios listed above or others, it's possible that 1 promise will ALWAYS win the race and users will NOT get any suggestion back.
* This is the hack to return first "NON-EMPTY" response
*/
let result = await Promise.race(ps)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editPromise will likely always win the race because for most users the edit predictions are not enabled, hence its server API response will be very fast, and it will win the race.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

donee

if (ps.length > 1 && result.items.length === 0) {
for (const p of ps) {
const r = await p
if (r.items.length > 0) {
result = r
}
}
}
Comment on lines +162 to +169
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugly, but this is the best effort we can do atm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add another field suggestionType within InlineCompletionListWithReferences so that we know which suggestion type it is instead of this hack. Can follow up.


getLogger().info('Received inline completion response from LSP: %O', {
sessionId: result.sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { createMockDocument } from 'aws-core-vscode/test'
import { CursorUpdateManager } from '../../../../../src/app/inline/cursorUpdateManager'
import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer'
import { globals } from 'aws-core-vscode/shared'
import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener'

const completionApi = 'aws/textDocument/inlineCompletionWithReferences'
const editApi = 'aws/textDocument/editCompletion'

describe('RecommendationService', () => {
let languageClient: LanguageClient
Expand All @@ -28,6 +32,10 @@ describe('RecommendationService', () => {
const mockPosition = { line: 0, character: 0 } as Position
const mockContext = { triggerKind: InlineCompletionTriggerKind.Automatic, selectedCompletionInfo: undefined }
const mockToken = { isCancellationRequested: false } as CancellationToken
const mockDocumentEventListener = {
isLastEventDeletion: (filepath: string) => false,
getLastDocumentChangeEvent: (filepath: string) => undefined,
} as DocumentEventListener
const mockInlineCompletionItemOne = {
insertText: 'ItemOne',
} as InlineCompletionItem
Expand Down Expand Up @@ -134,12 +142,19 @@ describe('RecommendationService', () => {
mockPosition,
mockContext,
mockToken,
true
true,
mockDocumentEventListener
)

// Verify sendRequest was called with correct parameters
assert(sendRequestStub.calledOnce)
const requestArgs = sendRequestStub.firstCall.args[1]
const cs = sendRequestStub.getCalls()
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
const editCalls = cs.filter((c) => c.firstArg === editApi)
assert.strictEqual(cs.length, 2)
assert.strictEqual(completionCalls.length, 1)
assert.strictEqual(editCalls.length, 1)

const requestArgs = completionCalls[0].args[1]
assert.deepStrictEqual(requestArgs, {
textDocument: {
uri: 'file:///test.py',
Expand Down Expand Up @@ -177,12 +192,19 @@ describe('RecommendationService', () => {
mockPosition,
mockContext,
mockToken,
true
true,
mockDocumentEventListener
)

// Verify sendRequest was called with correct parameters
assert(sendRequestStub.calledTwice)
const firstRequestArgs = sendRequestStub.firstCall.args[1]
const cs = sendRequestStub.getCalls()
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
const editCalls = cs.filter((c) => c.firstArg === editApi)
assert.strictEqual(cs.length, 3)
assert.strictEqual(completionCalls.length, 2)
assert.strictEqual(editCalls.length, 1)

const firstRequestArgs = completionCalls[0].args[1]
const expectedRequestArgs = {
textDocument: {
uri: 'file:///test.py',
Expand All @@ -192,7 +214,7 @@ describe('RecommendationService', () => {
documentChangeParams: undefined,
openTabFilepaths: [],
}
const secondRequestArgs = sendRequestStub.secondCall.args[1]
const secondRequestArgs = completionCalls[1].args[1]
assert.deepStrictEqual(firstRequestArgs, expectedRequestArgs)
assert.deepStrictEqual(secondRequestArgs, {
...expectedRequestArgs,
Expand All @@ -218,7 +240,8 @@ describe('RecommendationService', () => {
mockPosition,
mockContext,
mockToken,
true
true,
mockDocumentEventListener
)

// Verify recordCompletionRequest was called
Expand All @@ -235,6 +258,7 @@ describe('RecommendationService', () => {
mockContext,
mockToken,
true,
mockDocumentEventListener,
{
showUi: false,
emitTelemetry: true,
Expand All @@ -254,7 +278,8 @@ describe('RecommendationService', () => {
mockPosition,
mockContext,
mockToken,
true
true,
mockDocumentEventListener
)

// Verify UI methods were called
Expand Down Expand Up @@ -286,6 +311,7 @@ describe('RecommendationService', () => {
mockContext,
mockToken,
true,
mockDocumentEventListener,
options
)

Expand Down
Loading