Skip to content

Commit 6e82d93

Browse files
committed
Merge remote-tracking branch 'origin/master' into HEAD
2 parents e24101c + 14611c3 commit 6e82d93

File tree

18 files changed

+1159
-764
lines changed

18 files changed

+1159
-764
lines changed

packages/amazonq/src/app/inline/EditRendering/displayImage.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { getLogger, setContext } from 'aws-core-vscode/shared'
6+
import { getContext, getLogger, setContext } from 'aws-core-vscode/shared'
77
import * as vscode from 'vscode'
8-
import { diffLines } from 'diff'
8+
import { applyPatch, diffLines } from 'diff'
99
import { LanguageClient } from 'vscode-languageclient'
1010
import { CodeWhispererSession } from '../sessionManager'
1111
import { LogInlineCompletionSessionResultsParams } from '@aws/language-server-runtimes/protocol'
1212
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
1313
import path from 'path'
1414
import { imageVerticalOffset } from './svgGenerator'
15-
import { AmazonQInlineCompletionItemProvider } from '../completion'
15+
import { EditSuggestionState } from '../editSuggestionState'
16+
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1617
import { vsCodeState } from 'aws-core-vscode/codewhisperer'
1718

1819
export class EditDecorationManager {
@@ -136,6 +137,7 @@ export class EditDecorationManager {
136137
await this.clearDecorations(editor)
137138

138139
await setContext('aws.amazonq.editSuggestionActive' as any, true)
140+
EditSuggestionState.setEditSuggestionActive(true)
139141

140142
this.acceptHandler = onAccept
141143
this.rejectHandler = onReject
@@ -166,6 +168,7 @@ export class EditDecorationManager {
166168
this.acceptHandler = undefined
167169
this.rejectHandler = undefined
168170
await setContext('aws.amazonq.editSuggestionActive' as any, false)
171+
EditSuggestionState.setEditSuggestionActive(false)
169172
}
170173

171174
/**
@@ -270,6 +273,28 @@ function getEndOfEditPosition(originalCode: string, newCode: string): vscode.Pos
270273
return editor ? editor.selection.active : new vscode.Position(0, 0)
271274
}
272275

276+
/**
277+
* Helper function to create discard telemetry params
278+
*/
279+
function createDiscardTelemetryParams(
280+
session: CodeWhispererSession,
281+
item: InlineCompletionItemWithReferences
282+
): LogInlineCompletionSessionResultsParams {
283+
return {
284+
sessionId: session.sessionId,
285+
completionSessionResult: {
286+
[item.itemId]: {
287+
seen: false,
288+
accepted: false,
289+
discarded: true,
290+
},
291+
},
292+
totalSessionDisplayTime: Date.now() - session.requestStartTime,
293+
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
294+
isInlineEdit: true,
295+
}
296+
}
297+
273298
/**
274299
* Helper function to display SVG decorations
275300
*/
@@ -286,6 +311,41 @@ export async function displaySvgDecoration(
286311
) {
287312
const originalCode = editor.document.getText()
288313

314+
// Check if a completion suggestion is currently active - if so, discard edit suggestion
315+
if (inlineCompletionProvider && (await inlineCompletionProvider.isCompletionActive())) {
316+
// Emit DISCARD telemetry for edit suggestion that can't be shown due to active completion
317+
const params = createDiscardTelemetryParams(session, item)
318+
languageClient.sendNotification('aws/logInlineCompletionSessionResults', params)
319+
getLogger().info('Edit suggestion discarded due to active completion suggestion')
320+
return
321+
}
322+
323+
const isPatchValid = applyPatch(editor.document.getText(), item.insertText as string)
324+
if (!isPatchValid) {
325+
const params = createDiscardTelemetryParams(session, item)
326+
// TODO: this session is closed on flare side hence discarded is not emitted in flare
327+
languageClient.sendNotification('aws/logInlineCompletionSessionResults', params)
328+
return
329+
}
330+
const documentChangeListener = vscode.workspace.onDidChangeTextDocument((e) => {
331+
if (e.contentChanges.length <= 0) {
332+
return
333+
}
334+
if (e.document !== editor.document) {
335+
return
336+
}
337+
if (vsCodeState.isCodeWhispererEditing) {
338+
return
339+
}
340+
if (getContext('aws.amazonq.editSuggestionActive') === false) {
341+
return
342+
}
343+
344+
const isPatchValid = applyPatch(e.document.getText(), item.insertText as string)
345+
if (!isPatchValid) {
346+
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
347+
}
348+
})
289349
await decorationManager.displayEditSuggestion(
290350
editor,
291351
svgImage,
@@ -310,6 +370,7 @@ export async function displaySvgDecoration(
310370
editor.selection = new vscode.Selection(endPosition, endPosition)
311371

312372
await decorationManager.clearDecorations(editor)
373+
documentChangeListener.dispose()
313374
const params: LogInlineCompletionSessionResultsParams = {
314375
sessionId: session.sessionId,
315376
completionSessionResult: {
@@ -343,6 +404,7 @@ export async function displaySvgDecoration(
343404
// Handle reject
344405
getLogger().info('Edit suggestion rejected')
345406
await decorationManager.clearDecorations(editor)
407+
documentChangeListener.dispose()
346408
const params: LogInlineCompletionSessionResultsParams = {
347409
sessionId: session.sessionId,
348410
completionSessionResult: {

packages/amazonq/src/app/inline/EditRendering/imageRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getLogger } from 'aws-core-vscode/shared'
1010
import { LanguageClient } from 'vscode-languageclient'
1111
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
1212
import { CodeWhispererSession } from '../sessionManager'
13-
import { AmazonQInlineCompletionItemProvider } from '../completion'
13+
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1414

1515
export async function showEdits(
1616
item: InlineCompletionItemWithReferences,

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,53 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
237237
await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`)
238238
}
239239

240+
/**
241+
* Check if a completion suggestion is currently active/displayed
242+
*/
243+
public async isCompletionActive(): Promise<boolean> {
244+
const session = this.sessionManager.getActiveSession()
245+
if (session === undefined || !session.displayed || session.suggestions.some((item) => item.isInlineEdit)) {
246+
return false
247+
}
248+
249+
// Use VS Code command to check if inline suggestion is actually visible on screen
250+
// This command only executes when inlineSuggestionVisible context is true
251+
await vscode.commands.executeCommand('aws.amazonq.checkInlineSuggestionVisibility')
252+
const isInlineSuggestionVisible = performance.now() - session.lastVisibleTime < 50
253+
return isInlineSuggestionVisible
254+
}
255+
256+
/**
257+
* Batch discard telemetry for completion suggestions when edit suggestion is active
258+
*/
259+
public batchDiscardTelemetryForEditSuggestion(items: any[], session: any): void {
260+
// Emit DISCARD telemetry for completion suggestions that can't be shown due to active edit
261+
const completionSessionResult: {
262+
[key: string]: { seen: boolean; accepted: boolean; discarded: boolean }
263+
} = {}
264+
265+
for (const item of items) {
266+
if (!item.isInlineEdit && item.itemId) {
267+
completionSessionResult[item.itemId] = {
268+
seen: false,
269+
accepted: false,
270+
discarded: true,
271+
}
272+
}
273+
}
274+
275+
// Send single telemetry event for all discarded items
276+
if (Object.keys(completionSessionResult).length > 0) {
277+
const params: LogInlineCompletionSessionResultsParams = {
278+
sessionId: session.sessionId,
279+
completionSessionResult,
280+
firstCompletionDisplayLatency: session.firstCompletionDisplayLatency,
281+
totalSessionDisplayTime: performance.now() - session.requestStartTime,
282+
}
283+
this.languageClient.sendNotification(this.logSessionResultMessageName, params)
284+
}
285+
}
286+
240287
// this method is automatically invoked by VS Code as user types
241288
async provideInlineCompletionItems(
242289
document: TextDocument,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Manages the state of edit suggestions to avoid circular dependencies
8+
*/
9+
export class EditSuggestionState {
10+
private static isEditSuggestionCurrentlyActive = false
11+
12+
static setEditSuggestionActive(active: boolean): void {
13+
this.isEditSuggestionCurrentlyActive = active
14+
}
15+
16+
static isEditSuggestionActive(): boolean {
17+
return this.isEditSuggestionCurrentlyActive
18+
}
19+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getLogger } from 'aws-core-vscode/shared'
2424
import { DocumentEventListener } from './documentEventListener'
2525
import { getOpenFilesInWindow } from 'aws-core-vscode/utils'
2626
import { asyncCallWithTimeout } from '../../util/timeoutUtil'
27+
import { EditSuggestionState } from './editSuggestionState'
2728

2829
export interface GetAllRecommendationsOptions {
2930
emitTelemetry?: boolean
@@ -132,7 +133,7 @@ export class RecommendationService {
132133
* Completions use PartialResultToken with single 1 call of [getAllRecommendations].
133134
* Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block.
134135
*/
135-
if (!isTriggerByDeletion && !request.partialResultToken) {
136+
if (!isTriggerByDeletion && !request.partialResultToken && !EditSuggestionState.isEditSuggestionActive()) {
136137
const completionPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
137138
inlineCompletionWithReferencesRequestType.method,
138139
request,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface CodeWhispererSession {
2626
triggerOnAcceptance?: boolean
2727
// whether any suggestion in this session was displayed on screen
2828
displayed: boolean
29+
// timestamp when the suggestion was last visible
30+
lastVisibleTime: number
2931
}
3032

3133
export class SessionManager {
@@ -52,6 +54,7 @@ export class SessionManager {
5254
firstCompletionDisplayLatency,
5355
diagnosticsBeforeAccept,
5456
displayed: false,
57+
lastVisibleTime: 0,
5558
}
5659
this._currentSuggestionIndex = 0
5760
}
@@ -134,6 +137,7 @@ export class SessionManager {
134137
public checkInlineSuggestionVisibility() {
135138
if (this.activeSession) {
136139
this.activeSession.displayed = true
140+
this.activeSession.lastVisibleTime = performance.now()
137141
}
138142
}
139143

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CursorUpdateManager } from '../../../../../src/app/inline/cursorUpdateM
1515
import { CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer'
1616
import { globals } from 'aws-core-vscode/shared'
1717
import { DocumentEventListener } from '../../../../../src/app/inline/documentEventListener'
18+
import { EditSuggestionState } from '../../../../../src/app/inline/editSuggestionState'
1819

1920
const completionApi = 'aws/textDocument/inlineCompletionWithReferences'
2021
const editApi = 'aws/textDocument/editCompletion'
@@ -325,5 +326,77 @@ describe('RecommendationService', () => {
325326
console.error = originalConsoleError
326327
}
327328
})
329+
330+
it('should not make completion request when edit suggestion is active', async () => {
331+
// Mock EditSuggestionState to return true (edit suggestion is active)
332+
const isEditSuggestionActiveStub = sandbox.stub(EditSuggestionState, 'isEditSuggestionActive').returns(true)
333+
334+
const mockResult = {
335+
sessionId: 'test-session',
336+
items: [mockInlineCompletionItemOne],
337+
partialResultToken: undefined,
338+
}
339+
340+
sendRequestStub.resolves(mockResult)
341+
342+
await service.getAllRecommendations(
343+
languageClient,
344+
mockDocument,
345+
mockPosition,
346+
mockContext,
347+
mockToken,
348+
true,
349+
mockDocumentEventListener
350+
)
351+
352+
// Verify sendRequest was called only for edit API, not completion API
353+
const cs = sendRequestStub.getCalls()
354+
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
355+
const editCalls = cs.filter((c) => c.firstArg === editApi)
356+
357+
assert.strictEqual(cs.length, 1) // Only edit call
358+
assert.strictEqual(completionCalls.length, 0) // No completion calls
359+
assert.strictEqual(editCalls.length, 1) // One edit call
360+
361+
// Verify the stub was called
362+
sinon.assert.calledOnce(isEditSuggestionActiveStub)
363+
})
364+
365+
it('should make completion request when edit suggestion is not active', async () => {
366+
// Mock EditSuggestionState to return false (no edit suggestion active)
367+
const isEditSuggestionActiveStub = sandbox
368+
.stub(EditSuggestionState, 'isEditSuggestionActive')
369+
.returns(false)
370+
371+
const mockResult = {
372+
sessionId: 'test-session',
373+
items: [mockInlineCompletionItemOne],
374+
partialResultToken: undefined,
375+
}
376+
377+
sendRequestStub.resolves(mockResult)
378+
379+
await service.getAllRecommendations(
380+
languageClient,
381+
mockDocument,
382+
mockPosition,
383+
mockContext,
384+
mockToken,
385+
true,
386+
mockDocumentEventListener
387+
)
388+
389+
// Verify sendRequest was called for both APIs
390+
const cs = sendRequestStub.getCalls()
391+
const completionCalls = cs.filter((c) => c.firstArg === completionApi)
392+
const editCalls = cs.filter((c) => c.firstArg === editApi)
393+
394+
assert.strictEqual(cs.length, 2) // Both calls
395+
assert.strictEqual(completionCalls.length, 1) // One completion call
396+
assert.strictEqual(editCalls.length, 1) // One edit call
397+
398+
// Verify the stub was called
399+
sinon.assert.calledOnce(isEditSuggestionActiveStub)
400+
})
328401
})
329402
})

0 commit comments

Comments
 (0)