Skip to content

Commit f3086d7

Browse files
fix: patch #2133 and handle more variants of FIM suggestions (#2407)
* fix: patch #2133 and handle more variants of FIM suggestions * fix: only consider FIM if the first edit line matches trigger position * fix: test --------- Co-authored-by: atontb <[email protected]>
1 parent 15d1b1f commit f3086d7

File tree

3 files changed

+155
-23
lines changed

3 files changed

+155
-23
lines changed

server/aws-lsp-codewhisperer/src/language-server/inline-completion/handler/editCompletionHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,8 @@ export class EditCompletionHandler {
400400
const processedSuggestion = processEditSuggestion(
401401
suggestion.content,
402402
session.startPosition,
403-
session.document
403+
session.document,
404+
session.requestContext.fileContext.rightFileContent
404405
)
405406
const isInlineEdit = processedSuggestion.type === SuggestionType.EDIT
406407

server/aws-lsp-codewhisperer/src/language-server/inline-completion/utils/diffUtils.test.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,27 @@ describe('categorizeUnifieddiffV2v2 should return correct type (addOnly, edit, d
123123

124124
describe('addOnly', function () {
125125
const addOnlyCases: Case[] = [
126+
{
127+
udiff: `--- a/src/main/hello/MathUtil.java
128+
+++ b/src/main/hello/MathUtil.java
129+
@@ -10,6 +10,6 @@ public class MathUtil {
130+
}
131+
132+
public static int multiply(int a, int b) {
133+
- return a * b;
134+
+ return a * b * c;
135+
}
136+
}`,
137+
},
138+
{
139+
udiff: `--- file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
140+
+++ file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
141+
@@ -6,2 +6,3 @@
142+
- return a * b;
143+
+ return a * b *
144+
+ c * d;
145+
}`,
146+
},
126147
{
127148
udiff: `--- file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
128149
+++ file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
@@ -141,12 +162,10 @@ describe('categorizeUnifieddiffV2v2 should return correct type (addOnly, edit, d
141162
}`,
142163
},
143164
{
144-
udiff: `--- file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
145-
+++ file:///Volumes/workplace/ide/sample_projects/Calculator-2/src/main/hello/MathUtil.java
146-
@@ -6,7 +6,11 @@
147-
148-
// write a function to subtract 2 numbers
149-
public static int subtract(int a, int b) {
165+
udiff: `--- a/src/main/hello/MathUtil.java
166+
+++ b/src/main/hello/MathUtil.java
167+
@@ -8,5 +8,10 @@ public class MathUtil {
168+
public static int subtract(int a, int b) {
150169
return a - b;
151170
}
152171
-
@@ -251,20 +270,14 @@ describe('categorizeUnifieddiffV2v2 should return correct type (addOnly, edit, d
251270
{
252271
udiff: `--- a/src/main/hello/MathUtil.java
253272
+++ b/src/main/hello/MathUtil.java
254-
@@ -1,11 +1,11 @@
273+
@@ -1,6 +1,6 @@
255274
public class MathUtil {
256275
// write a function to add 2 numbers
257276
- public static int add(int a, int b) {
258277
+ public static double add(double a, double b) {
259278
return a + b;
260279
}
261-
262-
// write a function to subtract 2 numbers
263-
public static int subtract(int a, int b) {
264-
public static double subtract(double a, double b) {
265-
return a - b;
266-
}
267-
}`,
280+
`,
268281
},
269282
{
270283
udiff: `--- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts
@@ -300,6 +313,17 @@ describe('categorizeUnifieddiffV2v2 should return correct type (addOnly, edit, d
300313
const languageId = getLanguageIdFromUri(uri)
301314
textDocument = TextDocument.create(uri, languageId, 0, content)`,
302315
},
316+
{
317+
udiff: `--- a/src/main/hello/MathUtil.java
318+
+++ b/src/main/hello/MathUtil.java
319+
@@ -12,4 +12,5 @@ public class MathUtil {
320+
321+
// write a function to multiply 2 numbers
322+
public static int multiply(int a, int b) {
323+
- return a * b;
324+
+ return a * b * c;
325+
+ }`,
326+
},
303327
]
304328

305329
for (let i = 0; i < cases.length; i++) {

server/aws-lsp-codewhisperer/src/language-server/inline-completion/utils/diffUtils.ts

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

6-
import * as diff from 'diff'
6+
import { parsePatch, Hunk, createTwoFilesPatch } from 'diff'
77
import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem } from '../../../shared/models/model'
88
import { trimSupplementalContexts } from '../../../shared/supplementalContextUtil/supplementalContextUtil'
99
import { Position, TextDocument, Range } from '@aws/language-server-runtimes/protocol'
1010
import { SuggestionType } from '../../../shared/codeWhispererService'
11-
import { getPrefixSuffixOverlap } from './mergeRightUtils'
11+
import { getPrefixSuffixOverlap, truncateOverlapWithRightContext } from './mergeRightUtils'
1212

1313
/**
1414
* Generates a unified diff format between old and new file contents
@@ -31,7 +31,7 @@ export function generateUnifiedDiffWithTimestamps(
3131
newTimestamp: number,
3232
contextSize: number = 3
3333
): string {
34-
const patchResult = diff.createTwoFilesPatch(
34+
const patchResult = createTwoFilesPatch(
3535
oldFilePath,
3636
newFilePath,
3737
oldContent,
@@ -204,12 +204,13 @@ export function getCharacterDifferences(addedLines: string[], deletedLines: stri
204204
export function processEditSuggestion(
205205
unifiedDiff: string,
206206
triggerPosition: Position,
207-
document: TextDocument
207+
document: TextDocument,
208+
rightContext: string
208209
): { suggestionContent: string; type: SuggestionType } {
209210
// Assume it's an edit if anything goes wrong, at the very least it will not be rendered incorrectly
210211
let diffCategory: ReturnType<typeof categorizeUnifieddiff> = 'edit'
211212
try {
212-
diffCategory = categorizeUnifieddiff(unifiedDiff)
213+
diffCategory = categorizeUnifieddiff(unifiedDiff, triggerPosition.line)
213214
} catch (e) {
214215
// We dont have logger here....
215216
diffCategory = 'edit'
@@ -228,8 +229,9 @@ export function processEditSuggestion(
228229
* if LSP returns `g('foo')` instead of `.log()` the suggestion will be discarded because prefix doesnt match
229230
*/
230231
const processedAdd = removeOverlapCodeFromSuggestion(leftContextAtTriggerLine, preprocessAdd)
232+
const mergedWithRightContext = truncateOverlapWithRightContext(rightContext, processedAdd)
231233
return {
232-
suggestionContent: processedAdd,
234+
suggestionContent: mergedWithRightContext,
233235
type: SuggestionType.COMPLETION,
234236
}
235237
} else {
@@ -247,10 +249,26 @@ interface UnifiedDiff {
247249
firstPlusIndex: number
248250
minusIndexes: number[]
249251
plusIndexes: number[]
252+
hunk: Hunk
250253
}
251254

252255
// TODO: refine
253256
export function readUdiff(unifiedDiff: string): UnifiedDiff {
257+
let hunk: Hunk | undefined
258+
try {
259+
const patches = parsePatch(unifiedDiff)
260+
if (patches.length !== 1) {
261+
throw new Error(`Provided unified diff from has 0 or more than 1 patches`)
262+
}
263+
hunk = patches[0].hunks[0]
264+
if (!hunk) {
265+
throw new Error(`Null hunk`)
266+
}
267+
} catch (e) {
268+
throw e
269+
}
270+
271+
// TODO: Should use hunk instead of parsing manually
254272
const lines = unifiedDiff.split('\n')
255273
const headerEndIndex = lines.findIndex(l => l.startsWith('@@'))
256274
if (headerEndIndex === -1) {
@@ -275,30 +293,64 @@ export function readUdiff(unifiedDiff: string): UnifiedDiff {
275293
const firstMinusIndex = relevantLines.findIndex(s => s.startsWith('-'))
276294
const firstPlusIndex = relevantLines.findIndex(s => s.startsWith('+'))
277295

296+
// TODO: Comment these out as they are used for a different version of addonly type determination logic in case the current implementation doesn't work.
297+
// Could remove later if we are sure current imple works.
298+
/**
299+
* Concatenate all contiguous added lines (i.e., unbroken sequence of "+"s).
300+
* Exclude all newlines when concatenating, so we get a single line representing the new text
301+
*/
302+
// let singleLine = ''
303+
// let prev: number | undefined
304+
// for (const idx of plusIndexes) {
305+
// if (!prev || idx === prev + 1) {
306+
// const removedPlus = relevantLines[idx].substring(1)
307+
// const removedStartNewline = trimStartNewline(removedPlus)
308+
// singleLine += removedStartNewline
309+
// } else {
310+
// break
311+
// }
312+
// }
313+
278314
return {
279315
linesWithoutHeaders: relevantLines,
280316
firstMinusIndex: firstMinusIndex,
281317
firstPlusIndex: firstPlusIndex,
282318
minusIndexes: minusIndexes,
283319
plusIndexes: plusIndexes,
320+
hunk: hunk,
284321
}
285322
}
286323

287-
export function categorizeUnifieddiff(unifiedDiff: string): 'addOnly' | 'deleteOnly' | 'edit' {
324+
// Theoretically, we should always pass userTriggerAtLine, keeping it nullable for easier testing for now
325+
export function categorizeUnifieddiff(
326+
unifiedDiff: string,
327+
userTriggerAtLine?: number
328+
): 'addOnly' | 'deleteOnly' | 'edit' {
288329
try {
289330
const d = readUdiff(unifiedDiff)
331+
const hunk = d.hunk
290332
const firstMinusIndex = d.firstMinusIndex
291333
const firstPlusIndex = d.firstPlusIndex
292334
const diffWithoutHeaders = d.linesWithoutHeaders
293335

336+
// Shouldn't be the case but if there is no - nor +, assume it's an edit
294337
if (firstMinusIndex === -1 && firstPlusIndex === -1) {
295338
return 'edit'
296339
}
297340

341+
// If first "EDIT" line is not where users trigger, it must be EDIT
342+
// Note hunk.start is 1 based index
343+
const firstLineEdited = hunk.oldStart - 1 + Math.min(...d.minusIndexes, ...d.plusIndexes)
344+
if (userTriggerAtLine !== undefined && userTriggerAtLine !== firstLineEdited) {
345+
return 'edit'
346+
}
347+
348+
// Naive case, only +
298349
if (firstMinusIndex === -1 && firstPlusIndex !== -1) {
299350
return 'addOnly'
300351
}
301352

353+
// Naive case, only -
302354
if (firstMinusIndex !== -1 && firstPlusIndex === -1) {
303355
return 'deleteOnly'
304356
}
@@ -321,12 +373,47 @@ export function categorizeUnifieddiff(unifiedDiff: string): 'addOnly' | 'deleteO
321373

322374
// If last '-' line is followed by '+' block, it could be addonly
323375
if (plusIndexes[0] === minusIndexes[minusIndexes.length - 1] + 1) {
376+
/**
377+
-------------------------------
378+
- return
379+
+ return a - b;
380+
-------------------------------
381+
commonPrefix = "return "
382+
minusLinesDelta = ""
383+
384+
--------------------------------
385+
-\t\t\t
386+
+\treturn a - b;
387+
--------------------------------
388+
commonPrefix = "\t"
389+
minusLinesDelta = "\t\t"
390+
391+
*
392+
*
393+
*
394+
*/
324395
const minusLine = diffWithoutHeaders[minusIndexes[minusIndexes.length - 1]].substring(1)
325396
const pluscode = extractAdditions(unifiedDiff)
326397

327398
// If minusLine subtract the longest common substring of minusLine and plugcode and it's empty string, it's addonly
328399
const commonPrefix = longestCommonPrefix(minusLine, pluscode)
329-
if (minusLine.substring(commonPrefix.length).trim().length === 0) {
400+
const minusLinesDelta = minusLine.substring(commonPrefix.length)
401+
if (minusLinesDelta.trim().length === 0) {
402+
return 'addOnly'
403+
}
404+
405+
/**
406+
-------------------------------
407+
- return a * b;
408+
+ return a * b * c;
409+
-------------------------------
410+
commonPrefix = "return a * b"
411+
minusLinesDelta = ";"
412+
pluscodeDelta = " * c;"
413+
*
414+
*/
415+
const pluscodeDelta = pluscode.substring(commonPrefix.length)
416+
if (pluscodeDelta.endsWith(minusLinesDelta)) {
330417
return 'addOnly'
331418
}
332419
}
@@ -400,3 +487,23 @@ export function longestCommonPrefix(str1: string, str2: string): string {
400487

401488
return prefix
402489
}
490+
491+
// TODO: They are used for a different version of addonly type determination logic in case the current implementation doesn't work.
492+
// Could remove later if we are sure current impl works.
493+
// function trimStartNewline(str: string): string {
494+
// return str.replace(/^[\n\r]+/, '')
495+
// }
496+
497+
// function hasOneContiguousInsert(original: string, changed: string) {
498+
// const delta = changed.length - original.length
499+
// if (delta <= 0) {
500+
// // Changed string must be longer
501+
// return false
502+
// }
503+
504+
// let p, s
505+
// for (p = 0; original[p] === changed[p] && p < original.length; ++p);
506+
// for (s = original.length - 1; original[s] === changed[s + delta] && s >= 0; --s);
507+
508+
// return p === s + 1
509+
// }

0 commit comments

Comments
 (0)