Skip to content

Commit a41fde3

Browse files
authored
fix(amazonq): bring back the removal of extra) ] } ' " after inline completion acceptance (#7783)
## Problem https://github.com/user-attachments/assets/56c70784-f92b-43dc-8f7e-b38b873a3784 When an inline completion is accepted, there can be extra ) ] } ' " that breaks the syntax. ## Solution https://github.com/user-attachments/assets/c84f1e6a-4f80-4605-a2e1-074047ac19bf Bring back old code that was used to remove the extra ) ] } ' " . https://github.com/user-attachments/assets/35a65fce-98c1-4819-8bc5-3cb5c9aad5ab Ref: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/util/closingBracketUtil.ts --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 632a570 commit a41fde3

File tree

3 files changed

+274
-3
lines changed

3 files changed

+274
-3
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
getDiagnosticsDifferences,
3737
getDiagnosticsOfCurrentFile,
3838
toIdeDiagnostics,
39+
handleExtraBrackets,
3940
} from 'aws-core-vscode/codewhisperer'
4041
import { LineTracker } from './stateTracker/lineTracker'
4142
import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation'
@@ -106,11 +107,12 @@ export class InlineCompletionManager implements Disposable {
106107
item: InlineCompletionItemWithReferences,
107108
editor: TextEditor,
108109
requestStartTime: number,
109-
startLine: number,
110+
position: vscode.Position,
110111
firstCompletionDisplayLatency?: number
111112
) => {
112113
try {
113114
vsCodeState.isCodeWhispererEditing = true
115+
const startLine = position.line
114116
// TODO: also log the seen state for other suggestions in session
115117
// Calculate timing metrics before diagnostic delay
116118
const totalSessionDisplayTime = performance.now() - requestStartTime
@@ -119,6 +121,11 @@ export class InlineCompletionManager implements Disposable {
119121
this.sessionManager.getActiveSession()?.diagnosticsBeforeAccept,
120122
getDiagnosticsOfCurrentFile()
121123
)
124+
// try remove the extra } ) ' " if there is a new reported problem
125+
// the extra } will cause syntax error
126+
if (diagnosticDiff.added.length > 0) {
127+
await handleExtraBrackets(editor, editor.selection.active, position)
128+
}
122129
const params: LogInlineCompletionSessionResultsParams = {
123130
sessionId: sessionId,
124131
completionSessionResult: {
@@ -304,7 +311,7 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem
304311
item,
305312
editor,
306313
prevSession?.requestStartTime,
307-
position.line,
314+
position,
308315
prevSession?.firstCompletionDisplayLatency,
309316
],
310317
}
@@ -441,7 +448,7 @@ ${itemLog}
441448
item,
442449
editor,
443450
session.requestStartTime,
444-
cursorPosition.line,
451+
cursorPosition,
445452
session.firstCompletionDisplayLatency,
446453
],
447454
}

packages/core/src/codewhisperer/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export * from './util/importAdderUtil'
6868
export * from './util/zipUtil'
6969
export * from './util/diagnosticsUtil'
7070
export * from './util/commonUtil'
71+
export * from './util/closingBracketUtil'
7172
export * from './util/codewhispererSettings'
7273
export * from './service/diagnosticsProvider'
7374
export * as diagnosticsProvider from './service/diagnosticsProvider'
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
* Reference: https://github.com/aws/aws-toolkit-vscode/blob/amazonq/v1.74.0/packages/core/src/codewhisperer/util/closingBracketUtil.ts
5+
*/
6+
7+
import * as vscode from 'vscode'
8+
import * as CodeWhispererConstants from '../models/constants'
9+
10+
interface bracketMapType {
11+
[k: string]: string
12+
}
13+
14+
const quotes = ["'", '"', '`']
15+
const parenthesis = ['(', '[', '{', ')', ']', '}', '<', '>']
16+
17+
const closeToOpen: bracketMapType = {
18+
')': '(',
19+
']': '[',
20+
'}': '{',
21+
'>': '<',
22+
}
23+
24+
const openToClose: bracketMapType = {
25+
'(': ')',
26+
'[': ']',
27+
'{': '}',
28+
'<': '>',
29+
}
30+
31+
/**
32+
* LeftContext | Recommendation | RightContext
33+
* This function aims to resolve symbols which are redundant and need to be removed
34+
* The high level logic is as followed
35+
* 1. Pair non-paired closing symbols(parenthesis, brackets, quotes) existing in the "recommendation" with non-paired symbols existing in the "leftContext"
36+
* 2. Remove non-paired closing symbols existing in the "rightContext"
37+
* @param endPosition: end position of the effective recommendation written by CodeWhisperer
38+
* @param startPosition: start position of the effective recommendation by CodeWhisperer
39+
*
40+
* for example given file context ('|' is where we trigger the service):
41+
* anArray.pu|
42+
* recommendation returned: "sh(element);"
43+
* typeahead: "sh("
44+
* the effective recommendation written by CodeWhisperer: "element);"
45+
*/
46+
export async function handleExtraBrackets(
47+
editor: vscode.TextEditor,
48+
endPosition: vscode.Position,
49+
startPosition: vscode.Position
50+
) {
51+
const recommendation = editor.document.getText(new vscode.Range(startPosition, endPosition))
52+
const endOffset = editor.document.offsetAt(endPosition)
53+
const startOffset = editor.document.offsetAt(startPosition)
54+
const leftContext = editor.document.getText(
55+
new vscode.Range(
56+
startPosition,
57+
editor.document.positionAt(Math.max(startOffset - CodeWhispererConstants.charactersLimit, 0))
58+
)
59+
)
60+
61+
const rightContext = editor.document.getText(
62+
new vscode.Range(
63+
editor.document.positionAt(endOffset),
64+
editor.document.positionAt(endOffset + CodeWhispererConstants.charactersLimit)
65+
)
66+
)
67+
const bracketsToRemove = getBracketsToRemove(
68+
editor,
69+
recommendation,
70+
leftContext,
71+
rightContext,
72+
endPosition,
73+
startPosition
74+
)
75+
76+
const quotesToRemove = getQuotesToRemove(
77+
editor,
78+
recommendation,
79+
leftContext,
80+
rightContext,
81+
endPosition,
82+
startPosition
83+
)
84+
85+
const symbolsToRemove = [...bracketsToRemove, ...quotesToRemove]
86+
87+
if (symbolsToRemove.length) {
88+
await removeBracketsFromRightContext(editor, symbolsToRemove, endPosition)
89+
}
90+
}
91+
92+
const removeBracketsFromRightContext = async (
93+
editor: vscode.TextEditor,
94+
idxToRemove: number[],
95+
endPosition: vscode.Position
96+
) => {
97+
const offset = editor.document.offsetAt(endPosition)
98+
99+
await editor.edit(
100+
(editBuilder) => {
101+
for (const idx of idxToRemove) {
102+
const range = new vscode.Range(
103+
editor.document.positionAt(offset + idx),
104+
editor.document.positionAt(offset + idx + 1)
105+
)
106+
editBuilder.delete(range)
107+
}
108+
},
109+
{ undoStopAfter: false, undoStopBefore: false }
110+
)
111+
}
112+
113+
function getBracketsToRemove(
114+
editor: vscode.TextEditor,
115+
recommendation: string,
116+
leftContext: string,
117+
rightContext: string,
118+
end: vscode.Position,
119+
start: vscode.Position
120+
) {
121+
const unpairedClosingsInReco = nonClosedClosingParen(recommendation)
122+
const unpairedOpeningsInLeftContext = nonClosedOpneingParen(leftContext, unpairedClosingsInReco.length)
123+
const unpairedClosingsInRightContext = nonClosedClosingParen(rightContext)
124+
125+
const toRemove: number[] = []
126+
127+
let i = 0
128+
let j = 0
129+
let k = 0
130+
while (i < unpairedOpeningsInLeftContext.length && j < unpairedClosingsInReco.length) {
131+
const opening = unpairedOpeningsInLeftContext[i]
132+
const closing = unpairedClosingsInReco[j]
133+
134+
const isPaired = closeToOpen[closing.char] === opening.char
135+
const rightContextCharToDelete = unpairedClosingsInRightContext[k]
136+
137+
if (isPaired) {
138+
if (rightContextCharToDelete && rightContextCharToDelete.char === closing.char) {
139+
const rightContextStart = editor.document.offsetAt(end) + 1
140+
const symbolPosition = editor.document.positionAt(
141+
rightContextStart + rightContextCharToDelete.strOffset
142+
)
143+
const lineCnt = recommendation.split('\n').length - 1
144+
const isSameline = symbolPosition.line - lineCnt === start.line
145+
146+
if (isSameline) {
147+
toRemove.push(rightContextCharToDelete.strOffset)
148+
}
149+
150+
k++
151+
}
152+
}
153+
154+
i++
155+
j++
156+
}
157+
158+
return toRemove
159+
}
160+
161+
function getQuotesToRemove(
162+
editor: vscode.TextEditor,
163+
recommendation: string,
164+
leftContext: string,
165+
rightContext: string,
166+
endPosition: vscode.Position,
167+
startPosition: vscode.Position
168+
) {
169+
let leftQuote: string | undefined = undefined
170+
let leftIndex: number | undefined = undefined
171+
for (let i = leftContext.length - 1; i >= 0; i--) {
172+
const char = leftContext[i]
173+
if (quotes.includes(char)) {
174+
leftQuote = char
175+
leftIndex = leftContext.length - i
176+
break
177+
}
178+
}
179+
180+
let rightQuote: string | undefined = undefined
181+
let rightIndex: number | undefined = undefined
182+
for (let i = 0; i < rightContext.length; i++) {
183+
const char = rightContext[i]
184+
if (quotes.includes(char)) {
185+
rightQuote = char
186+
rightIndex = i
187+
break
188+
}
189+
}
190+
191+
let quoteCountInReco = 0
192+
if (leftQuote && rightQuote && leftQuote === rightQuote) {
193+
for (const char of recommendation) {
194+
if (quotes.includes(char) && char === leftQuote) {
195+
quoteCountInReco++
196+
}
197+
}
198+
}
199+
200+
if (leftIndex !== undefined && rightIndex !== undefined && quoteCountInReco % 2 !== 0) {
201+
const p = editor.document.positionAt(editor.document.offsetAt(endPosition) + rightIndex)
202+
203+
if (endPosition.line === startPosition.line && endPosition.line === p.line) {
204+
return [rightIndex]
205+
}
206+
}
207+
208+
return []
209+
}
210+
211+
function nonClosedOpneingParen(str: string, cnt?: number): { char: string; strOffset: number }[] {
212+
const resultSet: { char: string; strOffset: number }[] = []
213+
const stack: string[] = []
214+
215+
for (let i = str.length - 1; i >= 0; i--) {
216+
const char = str[i]
217+
if (char! in parenthesis) {
218+
continue
219+
}
220+
221+
if (char in closeToOpen) {
222+
stack.push(char)
223+
if (cnt && cnt === resultSet.length) {
224+
return resultSet
225+
}
226+
} else if (char in openToClose) {
227+
if (stack.length !== 0 && stack[stack.length - 1] === openToClose[char]) {
228+
stack.pop()
229+
} else {
230+
resultSet.push({ char: char, strOffset: i })
231+
}
232+
}
233+
}
234+
235+
return resultSet
236+
}
237+
238+
function nonClosedClosingParen(str: string, cnt?: number): { char: string; strOffset: number }[] {
239+
const resultSet: { char: string; strOffset: number }[] = []
240+
const stack: string[] = []
241+
242+
for (let i = 0; i < str.length; i++) {
243+
const char = str[i]
244+
if (char! in parenthesis) {
245+
continue
246+
}
247+
248+
if (char in openToClose) {
249+
stack.push(char)
250+
if (cnt && cnt === resultSet.length) {
251+
return resultSet
252+
}
253+
} else if (char in closeToOpen) {
254+
if (stack.length !== 0 && stack[stack.length - 1] === closeToOpen[char]) {
255+
stack.pop()
256+
} else {
257+
resultSet.push({ char: char, strOffset: i })
258+
}
259+
}
260+
}
261+
262+
return resultSet
263+
}

0 commit comments

Comments
 (0)