Skip to content

Commit 3a69d64

Browse files
Merge master into feature/hybridChat-local
2 parents 2057836 + 95827a2 commit 3a69d64

File tree

8 files changed

+262
-123
lines changed

8 files changed

+262
-123
lines changed

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

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

6-
import { getContext, getLogger, setContext } from 'aws-core-vscode/shared'
6+
import { getLogger, setContext } from 'aws-core-vscode/shared'
77
import * as vscode from 'vscode'
88
import { applyPatch, diffLines } from 'diff'
99
import { BaseLanguageClient } from 'vscode-languageclient'
@@ -16,7 +16,6 @@ import { EditSuggestionState } from '../editSuggestionState'
1616
import type { AmazonQInlineCompletionItemProvider } from '../completion'
1717
import { vsCodeState } from 'aws-core-vscode/codewhisperer'
1818

19-
const autoRejectEditCursorDistance = 25
2019
const autoDiscardEditCursorDistance = 10
2120

2221
export class EditDecorationManager {
@@ -164,7 +163,10 @@ export class EditDecorationManager {
164163
/**
165164
* Clears all edit suggestion decorations
166165
*/
167-
public async clearDecorations(editor: vscode.TextEditor): Promise<void> {
166+
public async clearDecorations(editor: vscode.TextEditor, disposables: vscode.Disposable[]): Promise<void> {
167+
for (const d of disposables) {
168+
d.dispose()
169+
}
168170
editor.setDecorations(this.imageDecorationType, [])
169171
editor.setDecorations(this.removedCodeDecorationType, [])
170172
this.currentImageDecoration = undefined
@@ -311,6 +313,7 @@ export async function displaySvgDecoration(
311313
session: CodeWhispererSession,
312314
languageClient: BaseLanguageClient,
313315
item: InlineCompletionItemWithReferences,
316+
listeners: vscode.Disposable[],
314317
inlineCompletionProvider?: AmazonQInlineCompletionItemProvider
315318
) {
316319
function logSuggestionFailure(type: 'DISCARD' | 'REJECT', reason: string, suggestionContent: string) {
@@ -359,44 +362,7 @@ export async function displaySvgDecoration(
359362
logSuggestionFailure('DISCARD', 'Invalid patch', item.insertText as string)
360363
return
361364
}
362-
const documentChangeListener = vscode.workspace.onDidChangeTextDocument((e) => {
363-
if (e.contentChanges.length <= 0) {
364-
return
365-
}
366-
if (e.document !== editor.document) {
367-
return
368-
}
369-
if (vsCodeState.isCodeWhispererEditing) {
370-
return
371-
}
372-
if (getContext('aws.amazonq.editSuggestionActive') === false) {
373-
return
374-
}
375365

376-
const isPatchValid = applyPatch(e.document.getText(), item.insertText as string)
377-
if (!isPatchValid) {
378-
logSuggestionFailure('REJECT', 'Invalid patch due to document change', item.insertText as string)
379-
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
380-
}
381-
})
382-
const cursorChangeListener = vscode.window.onDidChangeTextEditorSelection((e) => {
383-
if (!EditSuggestionState.isEditSuggestionActive()) {
384-
return
385-
}
386-
if (e.textEditor !== editor) {
387-
return
388-
}
389-
const currentPosition = e.selections[0].active
390-
const distance = Math.abs(currentPosition.line - startLine)
391-
if (distance > autoRejectEditCursorDistance) {
392-
logSuggestionFailure(
393-
'REJECT',
394-
`cursor position move too far away off ${autoRejectEditCursorDistance} lines`,
395-
item.insertText as string
396-
)
397-
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
398-
}
399-
})
400366
await decorationManager.displayEditSuggestion(
401367
editor,
402368
svgImage,
@@ -417,9 +383,8 @@ export async function displaySvgDecoration(
417383
const endPosition = getEndOfEditPosition(originalCode, newCode)
418384
editor.selection = new vscode.Selection(endPosition, endPosition)
419385

420-
await decorationManager.clearDecorations(editor)
421-
documentChangeListener.dispose()
422-
cursorChangeListener.dispose()
386+
await decorationManager.clearDecorations(editor, listeners)
387+
423388
const params: LogInlineCompletionSessionResultsParams = {
424389
sessionId: session.sessionId,
425390
completionSessionResult: {
@@ -443,9 +408,8 @@ export async function displaySvgDecoration(
443408
} else {
444409
getLogger().info('Edit suggestion rejected')
445410
}
446-
await decorationManager.clearDecorations(editor)
447-
documentChangeListener.dispose()
448-
cursorChangeListener.dispose()
411+
await decorationManager.clearDecorations(editor, listeners)
412+
449413
const suggestionState = isDiscard
450414
? {
451415
seen: false,

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

Lines changed: 190 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,207 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { displaySvgDecoration } from './displayImage'
7+
import { displaySvgDecoration, decorationManager } from './displayImage'
88
import { SvgGenerationService } from './svgGenerator'
9-
import { getLogger } from 'aws-core-vscode/shared'
9+
import { getContext, getLogger } from 'aws-core-vscode/shared'
1010
import { BaseLanguageClient } from 'vscode-languageclient'
1111
import { InlineCompletionItemWithReferences } from '@aws/language-server-runtimes/protocol'
1212
import { CodeWhispererSession } from '../sessionManager'
1313
import type { AmazonQInlineCompletionItemProvider } from '../completion'
14+
import { vsCodeState } from 'aws-core-vscode/codewhisperer'
15+
import { applyPatch, createPatch } from 'diff'
16+
import { EditSuggestionState } from '../editSuggestionState'
17+
import { debounce } from 'aws-core-vscode/utils'
1418

15-
export async function showEdits(
16-
item: InlineCompletionItemWithReferences,
17-
editor: vscode.TextEditor | undefined,
18-
session: CodeWhispererSession,
19-
languageClient: BaseLanguageClient,
20-
inlineCompletionProvider?: AmazonQInlineCompletionItemProvider
21-
) {
22-
if (!editor) {
23-
return
24-
}
25-
try {
26-
const svgGenerationService = new SvgGenerationService()
27-
// Generate your SVG image with the file contents
28-
const currentFile = editor.document.uri.fsPath
29-
const { svgImage, startLine, newCode, originalCodeHighlightRange } = await svgGenerationService.generateDiffSvg(
30-
currentFile,
31-
item.insertText as string
32-
)
19+
const autoRejectEditCursorDistance = 25
20+
const maxPrefixRetryCharDiff = 5
21+
const rerenderDeboucneInMs = 500
22+
23+
enum RejectReason {
24+
DocumentChange = 'Invalid patch due to document change',
25+
NotApplicableToOriginal = 'ApplyPatch fail for original code',
26+
MaxRetry = `Already retry ${maxPrefixRetryCharDiff} times`,
27+
}
28+
29+
export class EditsSuggestionSvg {
30+
private readonly logger = getLogger('nextEditPrediction')
31+
private documentChangedListener: vscode.Disposable | undefined
32+
private cursorChangedListener: vscode.Disposable | undefined
33+
34+
private startLine = 0
35+
36+
private documentChangeTrace = {
37+
contentChanged: '',
38+
count: 0,
39+
}
40+
41+
constructor(
42+
private suggestion: InlineCompletionItemWithReferences,
43+
private readonly editor: vscode.TextEditor,
44+
private readonly languageClient: BaseLanguageClient,
45+
private readonly session: CodeWhispererSession,
46+
private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider
47+
) {}
48+
49+
async show(patchedSuggestion?: InlineCompletionItemWithReferences) {
50+
if (!this.editor) {
51+
this.logger.error(`attempting to render an edit suggestion while editor is undefined`)
52+
return
53+
}
54+
55+
const item = patchedSuggestion ? patchedSuggestion : this.suggestion
3356

34-
// TODO: To investigate why it fails and patch [generateDiffSvg]
35-
if (newCode.length === 0) {
36-
getLogger('nextEditPrediction').warn('not able to apply provided edit suggestion, skip rendering')
57+
try {
58+
const svgGenerationService = new SvgGenerationService()
59+
// Generate your SVG image with the file contents
60+
const currentFile = this.editor.document.uri.fsPath
61+
const { svgImage, startLine, newCode, originalCodeHighlightRange } =
62+
await svgGenerationService.generateDiffSvg(currentFile, this.suggestion.insertText as string)
63+
64+
// For cursorChangeListener to access
65+
this.startLine = startLine
66+
67+
if (newCode.length === 0) {
68+
this.logger.warn('not able to apply provided edit suggestion, skip rendering')
69+
return
70+
}
71+
72+
if (svgImage) {
73+
const documentChangedListener = (this.documentChangedListener ??=
74+
vscode.workspace.onDidChangeTextDocument(async (e) => {
75+
await this.onDocChange(e)
76+
}))
77+
78+
const cursorChangedListener = (this.cursorChangedListener ??=
79+
vscode.window.onDidChangeTextEditorSelection((e) => {
80+
this.onCursorChange(e)
81+
}))
82+
83+
// display the SVG image
84+
await displaySvgDecoration(
85+
this.editor,
86+
svgImage,
87+
startLine,
88+
newCode,
89+
originalCodeHighlightRange,
90+
this.session,
91+
this.languageClient,
92+
item,
93+
[documentChangedListener, cursorChangedListener],
94+
this.inlineCompletionProvider
95+
)
96+
} else {
97+
this.logger.error('SVG image generation returned an empty result.')
98+
}
99+
} catch (error) {
100+
this.logger.error(`Error generating SVG image: ${error}`)
101+
}
102+
}
103+
104+
private onCursorChange(e: vscode.TextEditorSelectionChangeEvent) {
105+
if (!EditSuggestionState.isEditSuggestionActive()) {
106+
return
107+
}
108+
if (e.textEditor !== this.editor) {
37109
return
38110
}
111+
const currentPosition = e.selections[0].active
112+
const distance = Math.abs(currentPosition.line - this.startLine)
113+
if (distance > autoRejectEditCursorDistance) {
114+
this.autoReject(`cursor position move too far away off ${autoRejectEditCursorDistance} lines`)
115+
}
116+
}
39117

40-
if (svgImage) {
41-
// display the SVG image
42-
await displaySvgDecoration(
43-
editor,
44-
svgImage,
45-
startLine,
46-
newCode,
47-
originalCodeHighlightRange,
48-
session,
49-
languageClient,
50-
item,
51-
inlineCompletionProvider
118+
private async onDocChange(e: vscode.TextDocumentChangeEvent) {
119+
if (e.contentChanges.length <= 0) {
120+
return
121+
}
122+
if (e.document !== this.editor.document) {
123+
return
124+
}
125+
if (vsCodeState.isCodeWhispererEditing) {
126+
return
127+
}
128+
if (getContext('aws.amazonq.editSuggestionActive') === false) {
129+
return
130+
}
131+
132+
// TODO: handle multi-contentChanges scenario
133+
const diff = e.contentChanges[0] ? e.contentChanges[0].text : ''
134+
this.logger.info(`docChange sessionId=${this.session.sessionId}, contentChange=${diff}`)
135+
136+
// Track document changes because we might need to hide/reject suggestions while users are typing for better UX
137+
this.documentChangeTrace.contentChanged += e.contentChanges[0].text
138+
this.documentChangeTrace.count++
139+
/**
140+
* 1. Take the diff returned by the model and apply it to the code we originally sent to the model
141+
* 2. Do a diff between the above code and what's currently in the editor
142+
* 3. Show this second diff to the user as the edit suggestion
143+
*/
144+
// Users' file content when the request fires (best guess because the actual process happens in language server)
145+
const originalCode = this.session.fileContent
146+
const appliedToOriginal = applyPatch(originalCode, this.suggestion.insertText as string)
147+
try {
148+
if (appliedToOriginal) {
149+
const updatedPatch = this.patchSuggestion(appliedToOriginal)
150+
151+
if (
152+
this.documentChangeTrace.contentChanged.length > maxPrefixRetryCharDiff ||
153+
this.documentChangeTrace.count > maxPrefixRetryCharDiff
154+
) {
155+
// Reject the suggestion if users've typed over 5 characters while the suggestion is shown
156+
this.autoReject(RejectReason.MaxRetry)
157+
} else if (applyPatch(this.editor.document.getText(), updatedPatch.insertText as string) === false) {
158+
this.autoReject(RejectReason.DocumentChange)
159+
} else {
160+
// Close the previoius popup and rerender it
161+
this.logger.debug(`calling rerender with suggestion\n ${updatedPatch.insertText as string}`)
162+
await this.debouncedRerender(updatedPatch)
163+
}
164+
} else {
165+
this.autoReject(RejectReason.NotApplicableToOriginal)
166+
}
167+
} catch (e) {
168+
this.logger.error(`encountered error while processing edit suggestion when users type ${e}`)
169+
// TODO: Maybe we should auto reject/hide suggestions in this scenario
170+
}
171+
}
172+
173+
async dispose() {
174+
this.documentChangedListener?.dispose()
175+
this.cursorChangedListener?.dispose()
176+
await decorationManager.clearDecorations(this.editor, [])
177+
}
178+
179+
debouncedRerender = debounce(
180+
async (suggestion: InlineCompletionItemWithReferences) => await this.rerender(suggestion),
181+
rerenderDeboucneInMs,
182+
true
183+
)
184+
185+
private async rerender(suggestion: InlineCompletionItemWithReferences) {
186+
await decorationManager.clearDecorations(this.editor, [])
187+
await this.show(suggestion)
188+
}
189+
190+
private autoReject(reason: string) {
191+
function logSuggestionFailure(type: 'REJECT', reason: string, suggestionContent: string) {
192+
getLogger('nextEditPrediction').debug(
193+
`Auto ${type} edit suggestion with reason=${reason}, suggetion: ${suggestionContent}`
52194
)
53-
} else {
54-
getLogger('nextEditPrediction').error('SVG image generation returned an empty result.')
55195
}
56-
} catch (error) {
57-
getLogger('nextEditPrediction').error(`Error generating SVG image: ${error}`)
196+
197+
logSuggestionFailure('REJECT', reason, this.suggestion.insertText as string)
198+
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
199+
}
200+
201+
private patchSuggestion(appliedToOriginal: string): InlineCompletionItemWithReferences {
202+
const updatedPatch = createPatch(
203+
this.editor.document.fileName,
204+
this.editor.document.getText(),
205+
appliedToOriginal
206+
)
207+
this.logger.info(`Update edit suggestion\n ${updatedPatch}`)
208+
return { ...this.suggestion, insertText: updatedPatch }
58209
}
59210
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ import {
4242
import { LineTracker } from './stateTracker/lineTracker'
4343
import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation'
4444
import { TelemetryHelper } from './telemetryHelper'
45-
import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared'
45+
import { Experiments, getContext, getLogger, sleep } from 'aws-core-vscode/shared'
4646
import { messageUtils } from 'aws-core-vscode/utils'
47-
import { showEdits } from './EditRendering/imageRenderer'
47+
import { EditsSuggestionSvg } from './EditRendering/imageRenderer'
4848
import { ICursorUpdateRecorder } from './cursorUpdateManager'
4949
import { DocumentEventListener } from './documentEventListener'
5050

@@ -215,6 +215,7 @@ export class InlineCompletionManager implements Disposable {
215215
export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider {
216216
private logger = getLogger()
217217
private pendingRequest: Promise<InlineCompletionItem[]> | undefined
218+
private lastEdit: EditsSuggestionSvg | undefined
218219

219220
constructor(
220221
private readonly languageClient: BaseLanguageClient,
@@ -350,6 +351,11 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
350351
return []
351352
}
352353

354+
// Make edit suggestion blocking
355+
if (getContext('aws.amazonq.editSuggestionActive') === true) {
356+
return []
357+
}
358+
353359
// there is a bug in VS Code, when hitting Enter, the context.triggerKind is Invoke (0)
354360
// when hitting other keystrokes, the context.triggerKind is Automatic (1)
355361
// we only mark option + C as manual trigger
@@ -531,7 +537,12 @@ ${itemLog}
531537
if (item.isInlineEdit) {
532538
// Check if Next Edit Prediction feature flag is enabled
533539
if (Experiments.instance.get('amazonqLSPNEP', true)) {
534-
await showEdits(item, editor, session, this.languageClient, this)
540+
if (this.lastEdit) {
541+
await this.lastEdit.dispose()
542+
}
543+
const e = new EditsSuggestionSvg(item, editor, this.languageClient, session, this)
544+
await e.show()
545+
this.lastEdit = e
535546
logstr += `- duration between trigger to edits suggestion is displayed: ${Date.now() - t0}ms`
536547
}
537548
return []

0 commit comments

Comments
 (0)