bingocode 1.1.176 → 1.1.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
areFileEditsInputsEquivalent,
|
|
74
74
|
findActualString,
|
|
75
75
|
findClosestLines,
|
|
76
|
+
findFuzzySuggestion,
|
|
76
77
|
getPatchForEdit,
|
|
77
78
|
preserveQuoteStyle,
|
|
78
79
|
visibleWhitespace,
|
|
@@ -317,11 +318,15 @@ export const FileEditTool = buildTool({
|
|
|
317
318
|
// Use findActualString to handle quote normalization
|
|
318
319
|
const actualOldString = findActualString(file, old_string)
|
|
319
320
|
if (!actualOldString) {
|
|
320
|
-
|
|
321
|
+
const BASE = 'String to replace not found.'
|
|
321
322
|
const matches = findClosestLines(file, old_string)
|
|
323
|
+
const fuzzy = findFuzzySuggestion(file, old_string)
|
|
324
|
+
const fuzzyBlock = fuzzy
|
|
325
|
+
? `\n\nDid you mean:\n line ${fuzzy.lineNumber}: ${visibleWhitespace(fuzzy.suggestion)}\n (edit distance ${fuzzy.distance})`
|
|
326
|
+
: ''
|
|
322
327
|
const msg = matches.length
|
|
323
|
-
|
|
324
|
-
: `${BASE}.\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}\n↑ check visible whitespace markers above.`
|
|
328
|
+
? `${BASE}\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}\nClosest matches:\n${matches.map(m => ` line ${m.lineNumber} (${m.diffType})\n ${visibleWhitespace(m.snippet)}`).join('\n')}${fuzzyBlock}\n↑ check visible whitespace markers above.`
|
|
329
|
+
: `${BASE}.\n→ = tab · = space\nProvided:\n${visibleWhitespace(old_string)}${fuzzyBlock}\n↑ check visible whitespace markers above.`
|
|
325
330
|
return {
|
|
326
331
|
result: false,
|
|
327
332
|
behavior: 'ask',
|
|
@@ -15,6 +15,108 @@ import {
|
|
|
15
15
|
} from '../../utils/file.js'
|
|
16
16
|
import type { EditInput, FileEdit } from './types.js'
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Computes Levenshtein distance between two strings using Wagner-Fischer DP.
|
|
20
|
+
* O(n*m) time, O(min(n,m)) space (two-row optimization).
|
|
21
|
+
*/
|
|
22
|
+
export function levenshteinDistance(a: string, b: string): number {
|
|
23
|
+
// Ensure a is the shorter string for memory efficiency
|
|
24
|
+
if (a.length > b.length) {
|
|
25
|
+
;[a, b] = [b, a]
|
|
26
|
+
}
|
|
27
|
+
const m = a.length
|
|
28
|
+
const n = b.length
|
|
29
|
+
|
|
30
|
+
let prevRow = new Uint16Array(m + 1)
|
|
31
|
+
let currRow = new Uint16Array(m + 1)
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i <= m; i++) prevRow[i] = i
|
|
34
|
+
for (let j = 1; j <= n; j++) {
|
|
35
|
+
currRow[0] = j
|
|
36
|
+
for (let i = 1; i <= m; i++) {
|
|
37
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1
|
|
38
|
+
// d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+cost)
|
|
39
|
+
// deletion: remove char from s → d[i-1][j] + 1 → currRow[i - 1] + 1
|
|
40
|
+
// insertion: add char from t → d[i][j-1] + 1 → prevRow[i] + 1
|
|
41
|
+
// substitution: swap or match → d[i-1][j-1] + cost → prevRow[i - 1] + cost
|
|
42
|
+
currRow[i] = Math.min(
|
|
43
|
+
currRow[i - 1]! + 1,
|
|
44
|
+
prevRow[i]! + 1,
|
|
45
|
+
prevRow[i - 1]! + cost,
|
|
46
|
+
}
|
|
47
|
+
;[prevRow, currRow] = [currRow, prevRow]
|
|
48
|
+
}
|
|
49
|
+
return prevRow[m]!
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fuzzy matching — finds the best match in fileContent for searchString
|
|
54
|
+
* when the edit distance is within threshold. Used as a secondary pass
|
|
55
|
+
* after exact/normalized matching fails, to suggest "Did you mean: X"
|
|
56
|
+
* when the model's old_string is only a few characters off.
|
|
57
|
+
*
|
|
58
|
+
* For multi-line search strings: compares each contiguous block of lines
|
|
59
|
+
* whose total length is within tolerance. Returns null when no match is
|
|
60
|
+
* within threshold.
|
|
61
|
+
*/
|
|
62
|
+
export function findFuzzySuggestion(
|
|
63
|
+
fileContent: string,
|
|
64
|
+
searchString: string,
|
|
65
|
+
threshold: number = 2,
|
|
66
|
+
): { suggestion: string; lineNumber: number; distance: number } | null {
|
|
67
|
+
const searchLines = searchString.split('\n')
|
|
68
|
+
const fileLines = fileContent.split('\n')
|
|
69
|
+
const searchLen = searchString.length
|
|
70
|
+
|
|
71
|
+
// For single-line searches, scan all lines with similar length
|
|
72
|
+
if (searchLines.length === 1) {
|
|
73
|
+
const lenTolerance = searchLen + threshold * 2
|
|
74
|
+
let bestLine: string | null = null
|
|
75
|
+
let bestDist = Infinity
|
|
76
|
+
let bestIdx = -1
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
79
|
+
const line = fileLines[i]!
|
|
80
|
+
if (Math.abs(line.length - searchLen) > lenTolerance) continue
|
|
81
|
+
const dist = levenshteinDistance(line, searchString)
|
|
82
|
+
if (dist < bestDist) {
|
|
83
|
+
bestDist = dist
|
|
84
|
+
bestLine = line
|
|
85
|
+
bestIdx = i
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (bestLine && bestDist > 0 && bestDist <= threshold) {
|
|
90
|
+
return {
|
|
91
|
+
suggestion: bestLine.replace(/\s+$/, ''),
|
|
92
|
+
lineNumber: bestIdx + 1,
|
|
93
|
+
distance: bestDist,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// No match within threshold (or distance 0 = exact match already present)
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Multi-line: compare contiguous blocks of lines
|
|
101
|
+
const maxBlockLen = searchLen + threshold * searchLines.length
|
|
102
|
+
let best: { suggestion: string; lineNumber: number; distance: number } | null = null
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
|
|
105
|
+
const block = fileLines.slice(i, i + searchLines.length).join('\n')
|
|
106
|
+
if (Math.abs(block.length - searchLen) > maxBlockLen) continue
|
|
107
|
+
const dist = levenshteinDistance(block.replace(/\s+$/, ''), searchString.replace(/\s+$/, ''))
|
|
108
|
+
if (dist > 0 && dist <= threshold && dist < (best?.distance ?? Infinity)) {
|
|
109
|
+
best = {
|
|
110
|
+
suggestion: block.replace(/\s+$/, ''),
|
|
111
|
+
lineNumber: i + 1,
|
|
112
|
+
distance: dist,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return best
|
|
118
|
+
}
|
|
119
|
+
|
|
18
120
|
// Claude can't output curly quotes, so we define them as constants here for Claude to use
|
|
19
121
|
// in the code. We do this because we normalize curly quotes to straight quotes
|
|
20
122
|
// when applying edits.
|
|
@@ -266,7 +368,11 @@ export class EditNotFoundError extends Error {
|
|
|
266
368
|
* tab → '→', space → '·'
|
|
267
369
|
*/
|
|
268
370
|
export function visibleWhitespace(str: string): string {
|
|
269
|
-
return str
|
|
371
|
+
return str
|
|
372
|
+
.replace(/\t/g, '→')
|
|
373
|
+
.replace(/ /g, '·')
|
|
374
|
+
.replace(/\n/g, '↵')
|
|
375
|
+
.replace(/\r/g, '␍')
|
|
270
376
|
}
|
|
271
377
|
|
|
272
378
|
/**
|
|
@@ -432,11 +538,15 @@ export function getPatchForEdits({
|
|
|
432
538
|
// If this edit didn't change anything, throw an error
|
|
433
539
|
if (updatedFile === previousContent) {
|
|
434
540
|
const closest = findClosestLines(fileContents, edit.old_string)
|
|
541
|
+
const fuzzy = findFuzzySuggestion(fileContents, edit.old_string)
|
|
542
|
+
const fuzzyBlock = fuzzy
|
|
543
|
+
? `\n\nDid you mean:\n line ${fuzzy.lineNumber}: ${visibleWhitespace(fuzzy.suggestion)}\n (edit distance ${fuzzy.distance})`
|
|
544
|
+
: ''
|
|
435
545
|
throw new EditNotFoundError(
|
|
436
546
|
closest.length
|
|
437
547
|
? `Edit failed — closest match:
|
|
438
|
-
${closest.map(m => ` line ${m.lineNumber}: ${visibleWhitespace(m.snippet)} (${m.diffType})`).join('\n')}`
|
|
439
|
-
:
|
|
548
|
+
${closest.map(m => ` line ${m.lineNumber}: ${visibleWhitespace(m.snippet)} (${m.diffType})`).join('\n')}${fuzzyBlock}`
|
|
549
|
+
: `Edit failed — string not found in file.${fuzzyBlock}`,
|
|
440
550
|
{
|
|
441
551
|
searchString: edit.old_string,
|
|
442
552
|
visibleSearch: visibleWhitespace(edit.old_string),
|