33 * SPDX-License-Identifier: Apache-2.0
44 */
55
6- import * as diff from 'diff'
6+ import { parsePatch , Hunk , createTwoFilesPatch } from 'diff'
77import { CodeWhispererSupplementalContext , CodeWhispererSupplementalContextItem } from '../../../shared/models/model'
88import { trimSupplementalContexts } from '../../../shared/supplementalContextUtil/supplementalContextUtil'
99import { Position , TextDocument , Range } from '@aws/language-server-runtimes/protocol'
1010import { 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
204204export 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
253256export 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