Skip to content

Commit 3b2e28f

Browse files
committed
Edit prefix match
1 parent 2e1a219 commit 3b2e28f

File tree

8 files changed

+197
-93
lines changed

8 files changed

+197
-93
lines changed

aws-toolkit-vscode.code-workspace

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
{
1313
"path": "packages/amazonq",
1414
},
15+
{
16+
"path": "../language-servers",
17+
},
1518
],
1619
"settings": {
1720
"typescript.tsdk": "node_modules/typescript/lib",

packages/amazonq/.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
1414
"env": {
1515
"SSMDOCUMENT_LANGUAGESERVER_PORT": "6010",
16-
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080"
16+
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080",
1717
// Below allows for overrides used during development
18-
// "__AMAZONQLSP_PATH": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/agent-standalone.js",
18+
"__AMAZONQLSP_PATH": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/agent-standalone.js"
1919
// "__AMAZONQLSP_UI": "${workspaceFolder}/../../../language-servers/chat-client/build/amazonq-ui.js"
2020
},
2121
"envFile": "${workspaceFolder}/.local.env",

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

Lines changed: 14 additions & 43 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 { LanguageClient } 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 {
@@ -311,6 +310,7 @@ export async function displaySvgDecoration(
311310
session: CodeWhispererSession,
312311
languageClient: LanguageClient,
313312
item: InlineCompletionItemWithReferences,
313+
listeners: vscode.Disposable[],
314314
inlineCompletionProvider?: AmazonQInlineCompletionItemProvider
315315
) {
316316
function logSuggestionFailure(type: 'DISCARD' | 'REJECT', reason: string, suggestionContent: string) {
@@ -359,44 +359,7 @@ export async function displaySvgDecoration(
359359
logSuggestionFailure('DISCARD', 'Invalid patch', item.insertText as string)
360360
return
361361
}
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-
}
375362

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-
})
400363
await decorationManager.displayEditSuggestion(
401364
editor,
402365
svgImage,
@@ -418,8 +381,12 @@ export async function displaySvgDecoration(
418381
editor.selection = new vscode.Selection(endPosition, endPosition)
419382

420383
await decorationManager.clearDecorations(editor)
421-
documentChangeListener.dispose()
422-
cursorChangeListener.dispose()
384+
385+
// Dispose registered listeners on popup close
386+
for (const listener of listeners) {
387+
listener.dispose()
388+
}
389+
423390
const params: LogInlineCompletionSessionResultsParams = {
424391
sessionId: session.sessionId,
425392
completionSessionResult: {
@@ -444,8 +411,12 @@ export async function displaySvgDecoration(
444411
getLogger().info('Edit suggestion rejected')
445412
}
446413
await decorationManager.clearDecorations(editor)
447-
documentChangeListener.dispose()
448-
cursorChangeListener.dispose()
414+
415+
// Dispose registered listeners on popup close
416+
for (const listener of listeners) {
417+
listener.dispose()
418+
}
419+
449420
const suggestionState = isDiscard
450421
? {
451422
seen: false,

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

Lines changed: 160 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,177 @@
66
import * as vscode from 'vscode'
77
import { displaySvgDecoration } 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 { LanguageClient } 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'
1417

15-
export async function showEdits(
16-
item: InlineCompletionItemWithReferences,
17-
editor: vscode.TextEditor | undefined,
18-
session: CodeWhispererSession,
19-
languageClient: LanguageClient,
20-
inlineCompletionProvider?: AmazonQInlineCompletionItemProvider
21-
) {
22-
if (!editor) {
23-
return
18+
function logSuggestionFailure(type: 'DISCARD' | 'REJECT', reason: string, suggestionContent: string) {
19+
getLogger('nextEditPrediction').debug(
20+
`Auto ${type} edit suggestion with reason=${reason}, suggetion: ${suggestionContent}`
21+
)
22+
}
23+
24+
const autoRejectEditCursorDistance = 25
25+
const maxPrefixRetryCount = 5
26+
27+
enum RejectReason {
28+
DocumentChange = 'Invalid patch due to document change',
29+
NotApplicableToOriginal = 'ApplyPatch fail for original code',
30+
MaxRetry = 'Already retry 10 times',
31+
}
32+
33+
export class EditsSuggestionSvg {
34+
private readonly logger = getLogger('nextEditPrediction')
35+
private readonly documentChangedListener: vscode.Disposable
36+
private readonly cursorChangedListener: vscode.Disposable
37+
private readonly updatedSuggestions: InlineCompletionItemWithReferences[] = []
38+
private startLine = 0
39+
40+
constructor(
41+
private suggestion: InlineCompletionItemWithReferences,
42+
private readonly editor: vscode.TextEditor,
43+
private readonly languageClient: LanguageClient,
44+
private readonly session: CodeWhispererSession,
45+
private readonly inlineCompletionProvider?: AmazonQInlineCompletionItemProvider // why nullable?
46+
) {
47+
this.documentChangedListener = vscode.workspace.onDidChangeTextDocument(async (e) => {
48+
await this.onDocChange(e)
49+
})
50+
51+
this.cursorChangedListener = vscode.window.onDidChangeTextEditorSelection((e) => {
52+
this.onCursorChange(e)
53+
})
2454
}
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-
)
3355

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')
56+
async show() {
57+
if (!this.editor) {
3758
return
3859
}
3960

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
61+
const item =
62+
this.updatedSuggestions.length > 0
63+
? this.updatedSuggestions[this.updatedSuggestions.length - 1]
64+
: this.suggestion
65+
66+
try {
67+
const svgGenerationService = new SvgGenerationService()
68+
// Generate your SVG image with the file contents
69+
const currentFile = this.editor.document.uri.fsPath
70+
const { svgImage, startLine, newCode, originalCodeHighlightRange } =
71+
await svgGenerationService.generateDiffSvg(currentFile, this.suggestion.insertText as string)
72+
73+
// For cursorChangeListener to access
74+
this.startLine = startLine
75+
76+
// TODO: To investigate why it fails and patch [generateDiffSvg]
77+
if (newCode.length === 0) {
78+
this.logger.warn('not able to apply provided edit suggestion, skip rendering')
79+
return
80+
}
81+
82+
if (svgImage) {
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+
[this.documentChangedListener, this.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) {
109+
return
110+
}
111+
const currentPosition = e.selections[0].active
112+
const distance = Math.abs(currentPosition.line - this.startLine)
113+
if (distance > autoRejectEditCursorDistance) {
114+
logSuggestionFailure(
115+
'REJECT',
116+
`cursor position move too far away off ${autoRejectEditCursorDistance} lines`,
117+
this.suggestion.insertText as string
52118
)
53-
} else {
54-
getLogger('nextEditPrediction').error('SVG image generation returned an empty result.')
119+
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
55120
}
56-
} catch (error) {
57-
getLogger('nextEditPrediction').error(`Error generating SVG image: ${error}`)
121+
}
122+
123+
private async onDocChange(e: vscode.TextDocumentChangeEvent) {
124+
if (e.contentChanges.length <= 0) {
125+
return
126+
}
127+
if (e.document !== this.editor.document) {
128+
return
129+
}
130+
if (vsCodeState.isCodeWhispererEditing) {
131+
return
132+
}
133+
if (getContext('aws.amazonq.editSuggestionActive') === false) {
134+
return
135+
}
136+
137+
/**
138+
* 1. Take the diff returned by the model and apply it to the code we originally sent to the model
139+
* 2. Do a diff between the above code and what's currently in the editor
140+
* 3. Show this second diff to the user as the edit suggestion
141+
*/
142+
// Users' file content when the request fires (best guess because the actual process happens in language server)
143+
const originalCode = this.session.fileContent
144+
const appliedToOriginal = applyPatch(originalCode, this.suggestion.insertText as string)
145+
try {
146+
if (appliedToOriginal) {
147+
const updatedPatch = this.patchSuggestion(appliedToOriginal)
148+
149+
if (this.updatedSuggestions.length > maxPrefixRetryCount) {
150+
this.autoReject(RejectReason.MaxRetry)
151+
} else if (applyPatch(this.editor.document.getText(), updatedPatch) === false) {
152+
this.autoReject(RejectReason.DocumentChange)
153+
}
154+
155+
await this.show()
156+
} else {
157+
this.autoReject(RejectReason.NotApplicableToOriginal)
158+
}
159+
} catch (e) {
160+
// TODO: format
161+
this.logger.error(`${e}`)
162+
}
163+
}
164+
165+
private autoReject(reason: string) {
166+
logSuggestionFailure('REJECT', reason, this.suggestion.insertText as string)
167+
void vscode.commands.executeCommand('aws.amazonq.inline.rejectEdit')
168+
}
169+
170+
private patchSuggestion(appliedToOriginal: string): string {
171+
const updatedPatch = createPatch(
172+
this.editor.document.fileName,
173+
this.editor.document.getText(),
174+
appliedToOriginal
175+
)
176+
177+
this.logger.info(`Update edit suggestion\n ${updatedPatch}`)
178+
const updated: InlineCompletionItemWithReferences = { ...this.suggestion, insertText: updatedPatch }
179+
this.updatedSuggestions.push(updated)
180+
return updatedPatch
58181
}
59182
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation'
4343
import { TelemetryHelper } from './telemetryHelper'
4444
import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared'
4545
import { messageUtils } from 'aws-core-vscode/utils'
46-
import { showEdits } from './EditRendering/imageRenderer'
46+
import { EditsSuggestionSvg } from './EditRendering/imageRenderer'
4747
import { ICursorUpdateRecorder } from './cursorUpdateManager'
4848
import { DocumentEventListener } from './documentEventListener'
4949

@@ -529,7 +529,7 @@ ${itemLog}
529529
if (item.isInlineEdit) {
530530
// Check if Next Edit Prediction feature flag is enabled
531531
if (Experiments.instance.get('amazonqLSPNEP', true)) {
532-
await showEdits(item, editor, session, this.languageClient, this)
532+
await new EditsSuggestionSvg(item, editor, this.languageClient, session, this).show()
533533
logstr += `- duration between trigger to edits suggestion is displayed: ${Date.now() - t0}ms`
534534
}
535535
return []

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,12 @@ export class RecommendationService {
141141
* Edits leverage partialResultToken to achieve EditStreak such that clients can pull all continuous suggestions generated by the model within 1 EOS block.
142142
*/
143143
if (!isTriggerByDeletion && !request.partialResultToken && !EditSuggestionState.isEditSuggestionActive()) {
144-
const completionPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
145-
inlineCompletionWithReferencesRequestType.method,
146-
request,
147-
token
148-
)
149-
ps.push(completionPromise)
144+
// const completionPromise: Promise<InlineCompletionListWithReferences> = languageClient.sendRequest(
145+
// inlineCompletionWithReferencesRequestType.method,
146+
// request,
147+
// token
148+
// )
149+
// ps.push(completionPromise)
150150
}
151151

152152
/**
@@ -241,6 +241,7 @@ export class RecommendationService {
241241
result.items,
242242
requestStartTime,
243243
position,
244+
document,
244245
firstCompletionDisplayLatency
245246
)
246247

0 commit comments

Comments
 (0)