Merge branch 'main' into improve_diff_prompt

This commit is contained in:
Daniel
2025-01-18 17:01:21 -05:00
committed by GitHub
191 changed files with 32530 additions and 35310 deletions

View File

@@ -1,17 +1,21 @@
import type { DiffStrategy } from './types'
import { UnifiedDiffStrategy } from './strategies/unified'
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
import { NewUnifiedDiffStrategy } from './strategies/new-unified'
import type { DiffStrategy } from "./types"
import { UnifiedDiffStrategy } from "./strategies/unified"
import { SearchReplaceDiffStrategy } from "./strategies/search-replace"
import { NewUnifiedDiffStrategy } from "./strategies/new-unified"
/**
* Get the appropriate diff strategy for the given model
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
* @returns The appropriate diff strategy for the model
*/
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy: boolean = false): DiffStrategy {
if (experimentalDiffStrategy) {
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
}
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
export function getDiffStrategy(
model: string,
fuzzyMatchThreshold?: number,
experimentalDiffStrategy: boolean = false,
): DiffStrategy {
if (experimentalDiffStrategy) {
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
}
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
}
export type { DiffStrategy }

View File

@@ -1,74 +1,73 @@
import { NewUnifiedDiffStrategy } from '../new-unified';
import { NewUnifiedDiffStrategy } from "../new-unified"
describe('main', () => {
describe("main", () => {
let strategy: NewUnifiedDiffStrategy
let strategy: NewUnifiedDiffStrategy
beforeEach(() => {
strategy = new NewUnifiedDiffStrategy(0.97)
})
beforeEach(() => {
strategy = new NewUnifiedDiffStrategy(0.97)
})
describe("constructor", () => {
it("should use default confidence threshold when not provided", () => {
const defaultStrategy = new NewUnifiedDiffStrategy()
expect(defaultStrategy["confidenceThreshold"]).toBe(1)
})
describe('constructor', () => {
it('should use default confidence threshold when not provided', () => {
const defaultStrategy = new NewUnifiedDiffStrategy()
expect(defaultStrategy['confidenceThreshold']).toBe(1)
})
it("should use provided confidence threshold", () => {
const customStrategy = new NewUnifiedDiffStrategy(0.85)
expect(customStrategy["confidenceThreshold"]).toBe(0.85)
})
it('should use provided confidence threshold', () => {
const customStrategy = new NewUnifiedDiffStrategy(0.85)
expect(customStrategy['confidenceThreshold']).toBe(0.85)
})
it("should enforce minimum confidence threshold", () => {
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
expect(lowStrategy["confidenceThreshold"]).toBe(0.8)
})
})
it('should enforce minimum confidence threshold', () => {
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
expect(lowStrategy['confidenceThreshold']).toBe(0.8)
})
})
describe("getToolDescription", () => {
it("should return tool description with correct cwd", () => {
const cwd = "/test/path"
const description = strategy.getToolDescription({ cwd })
describe('getToolDescription', () => {
it('should return tool description with correct cwd', () => {
const cwd = '/test/path'
const description = strategy.getToolDescription(cwd)
expect(description).toContain('apply_diff')
expect(description).toContain(cwd)
expect(description).toContain('Parameters:')
expect(description).toContain('Format Requirements:')
})
})
expect(description).toContain("apply_diff")
expect(description).toContain(cwd)
expect(description).toContain("Parameters:")
expect(description).toContain("Format Requirements:")
})
})
it('should apply simple diff correctly', async () => {
const original = `line1
it("should apply simple diff correctly", async () => {
const original = `line1
line2
line3`;
line3`
const diff = `--- a/file.txt
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
+new line
line2
-line3
+modified line3`;
+modified line3`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if(result.success) {
expect(result.content).toBe(`line1
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
new line
line2
modified line3`);
}
});
modified line3`)
}
})
it('should handle multiple hunks', async () => {
const original = `line1
it("should handle multiple hunks", async () => {
const original = `line1
line2
line3
line4
line5`;
line5`
const diff = `--- a/file.txt
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
@@ -80,23 +79,23 @@ line5`;
line4
-line5
+modified line5
+new line at end`;
+new line at end`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(`line1
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
new line
line2
modified line3
line4
modified line5
new line at end`);
}
});
new line at end`)
}
})
it('should handle complex large', async () => {
const original = `line1
it("should handle complex large", async () => {
const original = `line1
line2
line3
line4
@@ -105,9 +104,9 @@ line6
line7
line8
line9
line10`;
line10`
const diff = `--- a/file.txt
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
@@ -130,12 +129,12 @@ line10`;
line9
-line10
+final line
+very last line`;
+very last line`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(`line1
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
header line
another header
line2
@@ -150,12 +149,12 @@ changed line8
bonus line
line9
final line
very last line`);
}
});
very last line`)
}
})
it('should handle indentation changes', async () => {
const original = `first line
it("should handle indentation changes", async () => {
const original = `first line
indented line
double indented line
back to single indent
@@ -164,9 +163,9 @@ no indent
double indent again
triple indent
back to single
last line`;
last line`
const diff = `--- original
const diff = `--- original
+++ modified
@@ ... @@
first line
@@ -181,9 +180,9 @@ last line`;
- triple indent
+ hi there mate
back to single
last line`;
last line`
const expected = `first line
const expected = `first line
indented line
tab indented line
new indented line
@@ -194,23 +193,22 @@ no indent
double indent again
hi there mate
back to single
last line`;
last line`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(expected);
}
});
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('should handle high level edits', async () => {
const original = `def factorial(n):
it("should handle high level edits", async () => {
const original = `def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)`
const diff = `@@ ... @@
const diff = `@@ ... @@
-def factorial(n):
- if n == 0:
- return 1
@@ -222,21 +220,21 @@ last line`;
+ else:
+ return number * factorial(number-1)`
const expected = `def factorial(number):
const expected = `def factorial(number):
if number == 0:
return 1
else:
return number * factorial(number-1)`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(expected);
}
});
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('it should handle very complex edits', async () => {
const original = `//Initialize the array that will hold the primes
it("it should handle very complex edits", async () => {
const original = `//Initialize the array that will hold the primes
var primeArray = [];
/*Write a function that checks for primeness and
pushes those values to t*he array*/
@@ -269,7 +267,7 @@ for (var i = 2; primeArray.length < numPrimes; i++) {
console.log(primeArray);
`
const diff = `--- test_diff.js
const diff = `--- test_diff.js
+++ test_diff.js
@@ ... @@
-//Initialize the array that will hold the primes
@@ -297,7 +295,7 @@ console.log(primeArray);
}
console.log(primeArray);`
const expected = `var primeArray = [];
const expected = `var primeArray = [];
function PrimeCheck(candidate){
isPrime = true;
for(var i = 2; i < candidate && isPrime; i++){
@@ -320,58 +318,57 @@ for (var i = 2; primeArray.length < numPrimes; i++) {
}
console.log(primeArray);
`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(expected);
}
});
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
describe('error handling and edge cases', () => {
it('should reject completely invalid diff format', async () => {
const original = 'line1\nline2\nline3';
const invalidDiff = 'this is not a diff at all';
const result = await strategy.applyDiff(original, invalidDiff);
expect(result.success).toBe(false);
});
describe("error handling and edge cases", () => {
it("should reject completely invalid diff format", async () => {
const original = "line1\nline2\nline3"
const invalidDiff = "this is not a diff at all"
it('should reject diff with invalid hunk format', async () => {
const original = 'line1\nline2\nline3';
const invalidHunkDiff = `--- a/file.txt
const result = await strategy.applyDiff(original, invalidDiff)
expect(result.success).toBe(false)
})
it("should reject diff with invalid hunk format", async () => {
const original = "line1\nline2\nline3"
const invalidHunkDiff = `--- a/file.txt
+++ b/file.txt
invalid hunk header
line1
-line2
+new line`;
const result = await strategy.applyDiff(original, invalidHunkDiff);
expect(result.success).toBe(false);
});
+new line`
it('should fail when diff tries to modify non-existent content', async () => {
const original = 'line1\nline2\nline3';
const nonMatchingDiff = `--- a/file.txt
const result = await strategy.applyDiff(original, invalidHunkDiff)
expect(result.success).toBe(false)
})
it("should fail when diff tries to modify non-existent content", async () => {
const original = "line1\nline2\nline3"
const nonMatchingDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
-nonexistent line
+new line
line3`;
const result = await strategy.applyDiff(original, nonMatchingDiff);
expect(result.success).toBe(false);
});
line3`
it('should handle overlapping hunks', async () => {
const original = `line1
const result = await strategy.applyDiff(original, nonMatchingDiff)
expect(result.success).toBe(false)
})
it("should handle overlapping hunks", async () => {
const original = `line1
line2
line3
line4
line5`;
const overlappingDiff = `--- a/file.txt
line5`
const overlappingDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
@@ -384,19 +381,19 @@ line5`;
-line3
-line4
+modified3and4
line5`;
const result = await strategy.applyDiff(original, overlappingDiff);
expect(result.success).toBe(false);
});
line5`
it('should handle empty lines modifications', async () => {
const original = `line1
const result = await strategy.applyDiff(original, overlappingDiff)
expect(result.success).toBe(false)
})
it("should handle empty lines modifications", async () => {
const original = `line1
line3
line5`;
const emptyLinesDiff = `--- a/file.txt
line5`
const emptyLinesDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
@@ -404,73 +401,73 @@ line5`;
-line3
+line3modified
line5`;
const result = await strategy.applyDiff(original, emptyLinesDiff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(`line1
line5`
const result = await strategy.applyDiff(original, emptyLinesDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
line3modified
line5`);
}
});
line5`)
}
})
it('should handle mixed line endings in diff', async () => {
const original = 'line1\r\nline2\nline3\r\n';
const mixedEndingsDiff = `--- a/file.txt
it("should handle mixed line endings in diff", async () => {
const original = "line1\r\nline2\nline3\r\n"
const mixedEndingsDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1\r
-line2
+modified2\r
line3`;
const result = await strategy.applyDiff(original, mixedEndingsDiff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe('line1\r\nmodified2\r\nline3\r\n');
}
});
line3`
it('should handle partial line modifications', async () => {
const original = 'const value = oldValue + 123;';
const partialDiff = `--- a/file.txt
const result = await strategy.applyDiff(original, mixedEndingsDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("line1\r\nmodified2\r\nline3\r\n")
}
})
it("should handle partial line modifications", async () => {
const original = "const value = oldValue + 123;"
const partialDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
-const value = oldValue + 123;
+const value = newValue + 123;`;
const result = await strategy.applyDiff(original, partialDiff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe('const value = newValue + 123;');
}
});
+const value = newValue + 123;`
it('should handle slightly malformed but recoverable diff', async () => {
const original = 'line1\nline2\nline3';
// Missing space after --- and +++
const slightlyBadDiff = `---a/file.txt
const result = await strategy.applyDiff(original, partialDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("const value = newValue + 123;")
}
})
it("should handle slightly malformed but recoverable diff", async () => {
const original = "line1\nline2\nline3"
// Missing space after --- and +++
const slightlyBadDiff = `---a/file.txt
+++b/file.txt
@@ ... @@
line1
-line2
+new line
line3`;
const result = await strategy.applyDiff(original, slightlyBadDiff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe('line1\nnew line\nline3');
}
});
});
line3`
describe('similar code sections', () => {
it('should correctly modify the right section when similar code exists', async () => {
const original = `function add(a, b) {
const result = await strategy.applyDiff(original, slightlyBadDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("line1\nnew line\nline3")
}
})
})
describe("similar code sections", () => {
it("should correctly modify the right section when similar code exists", async () => {
const original = `function add(a, b) {
return a + b;
}
@@ -480,20 +477,20 @@ function subtract(a, b) {
function multiply(a, b) {
return a + b; // Bug here
}`;
}`
const diff = `--- a/math.js
const diff = `--- a/math.js
+++ b/math.js
@@ ... @@
function multiply(a, b) {
- return a + b; // Bug here
+ return a * b;
}`;
}`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(`function add(a, b) {
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`function add(a, b) {
return a + b;
}
@@ -503,12 +500,12 @@ function subtract(a, b) {
function multiply(a, b) {
return a * b;
}`);
}
});
}`)
}
})
it('should handle multiple similar sections with correct context', async () => {
const original = `if (condition) {
it("should handle multiple similar sections with correct context", async () => {
const original = `if (condition) {
doSomething();
doSomething();
doSomething();
@@ -518,9 +515,9 @@ if (otherCondition) {
doSomething();
doSomething();
doSomething();
}`;
}`
const diff = `--- a/file.js
const diff = `--- a/file.js
+++ b/file.js
@@ ... @@
if (otherCondition) {
@@ -528,12 +525,12 @@ if (otherCondition) {
- doSomething();
+ doSomethingElse();
doSomething();
}`;
}`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(`if (condition) {
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`if (condition) {
doSomething();
doSomething();
doSomething();
@@ -543,14 +540,14 @@ if (otherCondition) {
doSomething();
doSomethingElse();
doSomething();
}`);
}
});
});
}`)
}
})
})
describe('hunk splitting', () => {
it('should handle large diffs with multiple non-contiguous changes', async () => {
const original = `import { readFile } from 'fs';
describe("hunk splitting", () => {
it("should handle large diffs with multiple non-contiguous changes", async () => {
const original = `import { readFile } from 'fs';
import { join } from 'path';
import { Logger } from './logger';
@@ -595,9 +592,9 @@ export {
validateInput,
writeOutput,
parseConfig
};`;
};`
const diff = `--- a/file.ts
const diff = `--- a/file.ts
+++ b/file.ts
@@ ... @@
-import { readFile } from 'fs';
@@ -672,9 +669,9 @@ export {
- parseConfig
+ parseConfig,
+ type Config
};`;
};`
const expected = `import { readFile, writeFile } from 'fs';
const expected = `import { readFile, writeFile } from 'fs';
import { join } from 'path';
import { Logger } from './utils/logger';
import { Config } from './types';
@@ -727,13 +724,13 @@ export {
writeOutput,
parseConfig,
type Config
};`;
};`
const result = await strategy.applyDiff(original, diff);
expect(result.success).toBe(true);
if (result.success) {
expect(result.content).toBe(expected);
}
});
});
});
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,27 @@
import { UnifiedDiffStrategy } from '../unified'
import { UnifiedDiffStrategy } from "../unified"
describe('UnifiedDiffStrategy', () => {
let strategy: UnifiedDiffStrategy
describe("UnifiedDiffStrategy", () => {
let strategy: UnifiedDiffStrategy
beforeEach(() => {
strategy = new UnifiedDiffStrategy()
})
beforeEach(() => {
strategy = new UnifiedDiffStrategy()
})
describe('getToolDescription', () => {
it('should return tool description with correct cwd', () => {
const cwd = '/test/path'
const description = strategy.getToolDescription(cwd)
expect(description).toContain('apply_diff')
expect(description).toContain(cwd)
expect(description).toContain('Parameters:')
expect(description).toContain('Format Requirements:')
})
})
describe("getToolDescription", () => {
it("should return tool description with correct cwd", () => {
const cwd = "/test/path"
const description = strategy.getToolDescription({ cwd })
describe('applyDiff', () => {
it('should successfully apply a function modification diff', async () => {
const originalContent = `import { Logger } from '../logger';
expect(description).toContain("apply_diff")
expect(description).toContain(cwd)
expect(description).toContain("Parameters:")
expect(description).toContain("Format Requirements:")
})
})
describe("applyDiff", () => {
it("should successfully apply a function modification diff", async () => {
const originalContent = `import { Logger } from '../logger';
function calculateTotal(items: number[]): number {
return items.reduce((sum, item) => {
@@ -31,7 +31,7 @@ function calculateTotal(items: number[]): number {
export { calculateTotal };`
const diffContent = `--- src/utils/helper.ts
const diffContent = `--- src/utils/helper.ts
+++ src/utils/helper.ts
@@ -1,9 +1,10 @@
import { Logger } from '../logger';
@@ -47,7 +47,7 @@ export { calculateTotal };`
export { calculateTotal };`
const expected = `import { Logger } from '../logger';
const expected = `import { Logger } from '../logger';
function calculateTotal(items: number[]): number {
const total = items.reduce((sum, item) => {
@@ -58,21 +58,21 @@ function calculateTotal(items: number[]): number {
export { calculateTotal };`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('should successfully apply a diff adding a new method', async () => {
const originalContent = `class Calculator {
it("should successfully apply a diff adding a new method", async () => {
const originalContent = `class Calculator {
add(a: number, b: number): number {
return a + b;
}
}`
const diffContent = `--- src/Calculator.ts
const diffContent = `--- src/Calculator.ts
+++ src/Calculator.ts
@@ -1,5 +1,9 @@
class Calculator {
@@ -85,7 +85,7 @@ export { calculateTotal };`
+ }
}`
const expected = `class Calculator {
const expected = `class Calculator {
add(a: number, b: number): number {
return a + b;
}
@@ -95,15 +95,15 @@ export { calculateTotal };`
}
}`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('should successfully apply a diff modifying imports', async () => {
const originalContent = `import { useState } from 'react';
it("should successfully apply a diff modifying imports", async () => {
const originalContent = `import { useState } from 'react';
import { Button } from './components';
function App() {
@@ -111,7 +111,7 @@ function App() {
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const diffContent = `--- src/App.tsx
const diffContent = `--- src/App.tsx
+++ src/App.tsx
@@ -1,7 +1,8 @@
-import { useState } from 'react';
@@ -124,7 +124,7 @@ function App() {
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const expected = `import { useState, useEffect } from 'react';
const expected = `import { useState, useEffect } from 'react';
import { Button } from './components';
function App() {
@@ -132,16 +132,16 @@ function App() {
useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('should successfully apply a diff with multiple hunks', async () => {
const originalContent = `import { readFile, writeFile } from 'fs';
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should successfully apply a diff with multiple hunks", async () => {
const originalContent = `import { readFile, writeFile } from 'fs';
function processFile(path: string) {
readFile(path, 'utf8', (err, data) => {
@@ -155,7 +155,7 @@ function processFile(path: string) {
export { processFile };`
const diffContent = `--- src/file-processor.ts
const diffContent = `--- src/file-processor.ts
+++ src/file-processor.ts
@@ -1,12 +1,14 @@
-import { readFile, writeFile } from 'fs';
@@ -182,7 +182,7 @@ export { processFile };`
export { processFile };`
const expected = `import { promises as fs } from 'fs';
const expected = `import { promises as fs } from 'fs';
import { join } from 'path';
async function processFile(path: string) {
@@ -198,32 +198,31 @@ async function processFile(path: string) {
export { processFile };`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it('should handle empty original content', async () => {
const originalContent = ''
const diffContent = `--- empty.ts
it("should handle empty original content", async () => {
const originalContent = ""
const diffContent = `--- empty.ts
+++ empty.ts
@@ -0,0 +1,3 @@
+export function greet(name: string): string {
+ return \`Hello, \${name}!\`;
+}`
const expected = `export function greet(name: string): string {
const expected = `export function greet(name: string): string {
return \`Hello, \${name}!\`;
}\n`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
})
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
})
})

View File

@@ -265,8 +265,8 @@ describe("applyGitFallback", () => {
{ type: "context", content: "line1", indent: "" },
{ type: "remove", content: "line2", indent: "" },
{ type: "add", content: "new line2", indent: "" },
{ type: "context", content: "line3", indent: "" }
]
{ type: "context", content: "line3", indent: "" },
],
} as Hunk
const content = ["line1", "line2", "line3"]
@@ -281,8 +281,8 @@ describe("applyGitFallback", () => {
const hunk = {
changes: [
{ type: "context", content: "nonexistent", indent: "" },
{ type: "add", content: "new line", indent: "" }
]
{ type: "add", content: "new line", indent: "" },
],
} as Hunk
const content = ["line1", "line2", "line3"]

View File

@@ -3,7 +3,7 @@ import { findAnchorMatch, findExactMatch, findSimilarityMatch, findLevenshteinMa
type SearchStrategy = (
searchStr: string,
content: string[],
startIndex?: number
startIndex?: number,
) => {
index: number
confidence: number
@@ -11,141 +11,141 @@ type SearchStrategy = (
}
const testCases = [
{
name: "should return no match if the search string is not found",
searchStr: "not found",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match if the search string is found",
searchStr: "line2",
content: ["line1", "line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "line3",
content: ["line1", "line2", "line3", "line4", "line3"],
startIndex: 3,
expected: { index: 4, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if there are more lines in content",
searchStr: "line2",
content: ["line1", "line2", "line3", "line4", "line5"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the beginning of the content",
searchStr: "line1",
content: ["line1", "line2", "line3"],
expected: { index: 0, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the end of the content",
searchStr: "line3",
content: ["line1", "line2", "line3"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match for a multi-line search string",
searchStr: "line2\nline3",
content: ["line1", "line2", "line3", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if a multi-line search string is not found",
searchStr: "line2\nline4",
content: ["line1", "line2", "line3", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with indentation",
searchStr: " line2",
content: ["line1", " line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with more complex indentation",
searchStr: " line3",
content: [" line1", " line2", " line3", " line4"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation",
searchStr: "\tline2",
content: [" line1", "\tline2", " line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation and multi-line",
searchStr: " line2\n\tline3",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed indentation and multi-line is not found",
searchStr: " line2\n line4",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with leading and trailing spaces",
searchStr: " line2 ",
content: ["line1", " line2 ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with leading and trailing tabs",
searchStr: "\tline2\t",
content: ["line1", "\tline2\t", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs",
searchStr: " \tline2\t ",
content: ["line1", " \tline2\t ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
searchStr: " \tline2\t \n line3 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
searchStr: " \tline2\t \n line4 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return no match if the search string is not found",
searchStr: "not found",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match if the search string is found",
searchStr: "line2",
content: ["line1", "line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "line3",
content: ["line1", "line2", "line3", "line4", "line3"],
startIndex: 3,
expected: { index: 4, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if there are more lines in content",
searchStr: "line2",
content: ["line1", "line2", "line3", "line4", "line5"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the beginning of the content",
searchStr: "line1",
content: ["line1", "line2", "line3"],
expected: { index: 0, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the end of the content",
searchStr: "line3",
content: ["line1", "line2", "line3"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match for a multi-line search string",
searchStr: "line2\nline3",
content: ["line1", "line2", "line3", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if a multi-line search string is not found",
searchStr: "line2\nline4",
content: ["line1", "line2", "line3", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with indentation",
searchStr: " line2",
content: ["line1", " line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with more complex indentation",
searchStr: " line3",
content: [" line1", " line2", " line3", " line4"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation",
searchStr: "\tline2",
content: [" line1", "\tline2", " line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation and multi-line",
searchStr: " line2\n\tline3",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed indentation and multi-line is not found",
searchStr: " line2\n line4",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with leading and trailing spaces",
searchStr: " line2 ",
content: ["line1", " line2 ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with leading and trailing tabs",
searchStr: "\tline2\t",
content: ["line1", "\tline2\t", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs",
searchStr: " \tline2\t ",
content: ["line1", " \tline2\t ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
searchStr: " \tline2\t \n line3 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
searchStr: " \tline2\t \n line4 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
]
describe("findExactMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("exact")) {
return
}
it(name, () => {
it(name, () => {
const result = findExactMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
@@ -155,16 +155,16 @@ describe("findExactMatch", () => {
})
describe("findAnchorMatch", () => {
const anchorTestCases = [
{
name: "should return no match if no anchors are found",
searchStr: " \n \n ",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
},
{
name: "should return no match if anchor positions cannot be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
const anchorTestCases = [
{
name: "should return no match if no anchors are found",
searchStr: " \n \n ",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
},
{
name: "should return no match if anchor positions cannot be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: [
"different line 1",
"different line 2",
@@ -173,24 +173,24 @@ describe("findAnchorMatch", () => {
"context line 1",
"context line 2",
],
expected: { index: -1, confidence: 0 },
},
{
name: "should return a match if anchor positions can be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
startIndex: 3,
expected: { index: 3, confidence: 1 },
},
{
name: "should return a match even if there are more lines in content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
expected: { index: -1, confidence: 0 },
},
{
name: "should return a match if anchor positions can be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
startIndex: 3,
expected: { index: 3, confidence: 1 },
},
{
name: "should return a match even if there are more lines in content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: [
"line1",
"line2",
@@ -201,30 +201,30 @@ describe("findAnchorMatch", () => {
"extra line 1",
"extra line 2",
],
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the beginning of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 0, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the end of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return no match if no valid anchor is found",
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
expected: { index: -1, confidence: 0 },
},
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the beginning of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 0, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the end of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return no match if no valid anchor is found",
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
expected: { index: -1, confidence: 0 },
},
]
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
it(name, () => {
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
it(name, () => {
const result = findAnchorMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
@@ -234,11 +234,11 @@ describe("findAnchorMatch", () => {
})
describe("findSimilarityMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("similarity")) {
return
}
it(name, () => {
it(name, () => {
const result = findSimilarityMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
@@ -248,11 +248,11 @@ describe("findSimilarityMatch", () => {
})
describe("findLevenshteinMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("levenshtein")) {
return
}
it(name, () => {
it(name, () => {
const result = findLevenshteinMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)

View File

@@ -18,7 +18,7 @@ function inferIndentation(line: string, contextLines: string[], previousIndent:
const contextLine = contextLines[0]
if (contextLine) {
const contextMatch = contextLine.match(/^(\s+)/)
if (contextMatch) {
if (contextMatch) {
return contextMatch[1]
}
}
@@ -28,19 +28,15 @@ function inferIndentation(line: string, contextLines: string[], previousIndent:
}
// Context matching edit strategy
export function applyContextMatching(
hunk: Hunk,
content: string[],
matchPosition: number,
): EditResult {
if (matchPosition === -1) {
export function applyContextMatching(hunk: Hunk, content: string[], matchPosition: number): EditResult {
if (matchPosition === -1) {
return { confidence: 0, result: content, strategy: "context" }
}
const newResult = [...content.slice(0, matchPosition)]
let sourceIndex = matchPosition
for (const change of hunk.changes) {
for (const change of hunk.changes) {
if (change.type === "context") {
// Use the original line from content if available
if (sourceIndex < content.length) {
@@ -82,20 +78,16 @@ export function applyContextMatching(
const confidence = validateEditResult(hunk, afterText)
return {
return {
confidence,
result: newResult,
strategy: "context"
strategy: "context",
}
}
// DMP edit strategy
export function applyDMP(
hunk: Hunk,
content: string[],
matchPosition: number,
): EditResult {
if (matchPosition === -1) {
export function applyDMP(hunk: Hunk, content: string[], matchPosition: number): EditResult {
if (matchPosition === -1) {
return { confidence: 0, result: content, strategy: "dmp" }
}
@@ -105,9 +97,9 @@ export function applyDMP(
const beforeLineCount = hunk.changes
.filter((change) => change.type === "context" || change.type === "remove")
.reduce((count, change) => count + change.content.split("\n").length, 0)
// Build BEFORE block (context + removals)
const beforeLines = hunk.changes
// Build BEFORE block (context + removals)
const beforeLines = hunk.changes
.filter((change) => change.type === "context" || change.type === "remove")
.map((change) => {
if (change.originalLine) {
@@ -115,9 +107,9 @@ export function applyDMP(
}
return change.indent ? change.indent + change.content : change.content
})
// Build AFTER block (context + additions)
const afterLines = hunk.changes
// Build AFTER block (context + additions)
const afterLines = hunk.changes
.filter((change) => change.type === "context" || change.type === "add")
.map((change) => {
if (change.originalLine) {
@@ -139,17 +131,17 @@ export function applyDMP(
const patchedLines = patchedText.split("\n")
// Construct final result
const newResult = [
...content.slice(0, matchPosition),
...patchedLines,
const newResult = [
...content.slice(0, matchPosition),
...patchedLines,
...content.slice(matchPosition + beforeLineCount),
]
const confidence = validateEditResult(hunk, patchedText)
return {
return {
confidence,
result: newResult,
result: newResult,
strategy: "dmp",
}
}
@@ -171,7 +163,7 @@ export async function applyGitFallback(hunk: Hunk, content: string[]): Promise<E
const searchLines = hunk.changes
.filter((change) => change.type === "context" || change.type === "remove")
.map((change) => change.originalLine || change.indent + change.content)
const replaceLines = hunk.changes
.filter((change) => change.type === "context" || change.type === "add")
.map((change) => change.originalLine || change.indent + change.content)
@@ -272,16 +264,16 @@ export async function applyGitFallback(hunk: Hunk, content: string[]): Promise<E
// Main edit function that tries strategies sequentially
export async function applyEdit(
hunk: Hunk,
content: string[],
matchPosition: number,
hunk: Hunk,
content: string[],
matchPosition: number,
confidence: number,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): Promise<EditResult> {
// Don't attempt regular edits if confidence is too low
if (confidence < confidenceThreshold) {
console.log(
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`,
)
return applyGitFallback(hunk, content)
}

View File

@@ -164,8 +164,8 @@ Generate a unified diff that can be cleanly applied to modify code files.
\`\`\`
Parameters:
- path: (required) File path relative to ${cwd}
- diff: (required) Unified diff content
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
- diff: (required) The diff content in unified format to apply to the file.
Usage:
<apply_diff>
@@ -233,7 +233,7 @@ Your diff here
originalContent: string,
diffContent: string,
startLine?: number,
endLine?: number
endLine?: number,
): Promise<DiffResult> {
const parsedDiff = this.parseUnifiedDiff(diffContent)
const originalLines = originalContent.split("\n")
@@ -271,7 +271,7 @@ Your diff here
subHunkResult,
subSearchResult.index,
subSearchResult.confidence,
this.confidenceThreshold
this.confidenceThreshold,
)
if (subEditResult.confidence >= this.confidenceThreshold) {
subHunkResult = subEditResult.result
@@ -293,12 +293,12 @@ Your diff here
const contextRatio = contextLines / totalLines
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
confidence * 100
confidence * 100,
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
errorMsg += "Debug Info:\n"
errorMsg += `- Search Strategy Used: ${strategy}\n`
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
contextRatio * 100
contextRatio * 100,
)}%)\n`
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
@@ -330,7 +330,7 @@ Your diff here
} else {
// Edit failure - likely due to content mismatch
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
editResult.confidence * 100
editResult.confidence * 100,
)}% confidence)\n\n`
errorMsg += "Debug Info:\n"
errorMsg += "- The location was found but the content didn't match exactly\n"

View File

@@ -69,26 +69,26 @@ export function getDMPSimilarity(original: string, modified: string): number {
export function validateEditResult(hunk: Hunk, result: string): number {
// Build the expected text from the hunk
const expectedText = hunk.changes
.filter(change => change.type === "context" || change.type === "add")
.map(change => change.indent ? change.indent + change.content : change.content)
.join("\n");
.filter((change) => change.type === "context" || change.type === "add")
.map((change) => (change.indent ? change.indent + change.content : change.content))
.join("\n")
// Calculate similarity between the result and expected text
const similarity = getDMPSimilarity(expectedText, result);
const similarity = getDMPSimilarity(expectedText, result)
// If the result is unchanged from original, return low confidence
const originalText = hunk.changes
.filter(change => change.type === "context" || change.type === "remove")
.map(change => change.indent ? change.indent + change.content : change.content)
.join("\n");
.filter((change) => change.type === "context" || change.type === "remove")
.map((change) => (change.indent ? change.indent + change.content : change.content))
.join("\n")
const originalSimilarity = getDMPSimilarity(originalText, result);
const originalSimilarity = getDMPSimilarity(originalText, result)
if (originalSimilarity > 0.97 && similarity !== 1) {
return 0.8 * similarity; // Some confidence since we found the right location
return 0.8 * similarity // Some confidence since we found the right location
}
// For partial matches, scale the confidence but keep it high if we're close
return similarity;
return similarity
}
// Helper function to validate context lines against original content
@@ -114,7 +114,7 @@ function validateContextLines(searchStr: string, content: string, confidenceThre
function createOverlappingWindows(
content: string[],
searchSize: number,
overlapSize: number = DEFAULT_OVERLAP_SIZE
overlapSize: number = DEFAULT_OVERLAP_SIZE,
): { window: string[]; startIndex: number }[] {
const windows: { window: string[]; startIndex: number }[] = []
@@ -140,7 +140,7 @@ function createOverlappingWindows(
// Helper function to combine overlapping matches
function combineOverlappingMatches(
matches: (SearchResult & { windowIndex: number })[],
overlapSize: number = DEFAULT_OVERLAP_SIZE
overlapSize: number = DEFAULT_OVERLAP_SIZE,
): SearchResult[] {
if (matches.length === 0) {
return []
@@ -162,7 +162,7 @@ function combineOverlappingMatches(
(m) =>
Math.abs(m.windowIndex - match.windowIndex) === 1 &&
Math.abs(m.index - match.index) <= overlapSize &&
!usedIndices.has(m.windowIndex)
!usedIndices.has(m.windowIndex),
)
if (overlapping.length > 0) {
@@ -196,7 +196,7 @@ export function findExactMatch(
searchStr: string,
content: string[],
startIndex: number = 0,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): SearchResult {
const searchLines = searchStr.split("\n")
const windows = createOverlappingWindows(content.slice(startIndex), searchLines.length)
@@ -210,7 +210,7 @@ export function findExactMatch(
const matchedContent = windowData.window
.slice(
windowStr.slice(0, exactMatch).split("\n").length - 1,
windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length
windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length,
)
.join("\n")
@@ -236,7 +236,7 @@ export function findSimilarityMatch(
searchStr: string,
content: string[],
startIndex: number = 0,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): SearchResult {
const searchLines = searchStr.split("\n")
let bestScore = 0
@@ -269,7 +269,7 @@ export function findLevenshteinMatch(
searchStr: string,
content: string[],
startIndex: number = 0,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): SearchResult {
const searchLines = searchStr.split("\n")
const candidates = []
@@ -324,7 +324,7 @@ export function findAnchorMatch(
searchStr: string,
content: string[],
startIndex: number = 0,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): SearchResult {
const searchLines = searchStr.split("\n")
const { first, last } = identifyAnchors(searchStr)
@@ -391,7 +391,7 @@ export function findBestMatch(
searchStr: string,
content: string[],
startIndex: number = 0,
confidenceThreshold: number = 0.97
confidenceThreshold: number = 0.97,
): SearchResult {
const strategies = [findExactMatch, findAnchorMatch, findSimilarityMatch, findLevenshteinMatch]

View File

@@ -1,20 +1,20 @@
export type Change = {
type: 'context' | 'add' | 'remove';
content: string;
indent: string;
originalLine?: string;
};
type: "context" | "add" | "remove"
content: string
indent: string
originalLine?: string
}
export type Hunk = {
changes: Change[];
};
changes: Change[]
}
export type Diff = {
hunks: Hunk[];
};
hunks: Hunk[]
}
export type EditResult = {
confidence: number;
result: string[];
strategy: string;
};
confidence: number
result: string[]
strategy: string
}

View File

@@ -1,72 +1,74 @@
import { DiffStrategy, DiffResult } from "../types"
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
const BUFFER_LINES = 20; // Number of extra context lines to show before and after matches
const BUFFER_LINES = 20 // Number of extra context lines to show before and after matches
function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
const matrix: number[][] = []
// Initialize matrix
for (let i = 0; i <= a.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= b.length; j++) {
matrix[0][j] = j;
}
// Initialize matrix
for (let i = 0; i <= a.length; i++) {
matrix[i] = [i]
}
for (let j = 0; j <= b.length; j++) {
matrix[0][j] = j
}
// Fill matrix
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
if (a[i-1] === b[j-1]) {
matrix[i][j] = matrix[i-1][j-1];
} else {
matrix[i][j] = Math.min(
matrix[i-1][j-1] + 1, // substitution
matrix[i][j-1] + 1, // insertion
matrix[i-1][j] + 1 // deletion
);
}
}
}
// Fill matrix
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
if (a[i - 1] === b[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1]
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1, // deletion
)
}
}
}
return matrix[a.length][b.length];
return matrix[a.length][b.length]
}
function getSimilarity(original: string, search: string): number {
if (search === '') {
return 1;
}
if (search === "") {
return 1
}
// Normalize strings by removing extra whitespace but preserve case
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
const normalizedOriginal = normalizeStr(original);
const normalizedSearch = normalizeStr(search);
if (normalizedOriginal === normalizedSearch) { return 1; }
// Calculate Levenshtein distance
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch);
// Calculate similarity ratio (0 to 1, where 1 is exact match)
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length);
return 1 - (distance / maxLength);
// Normalize strings by removing extra whitespace but preserve case
const normalizeStr = (str: string) => str.replace(/\s+/g, " ").trim()
const normalizedOriginal = normalizeStr(original)
const normalizedSearch = normalizeStr(search)
if (normalizedOriginal === normalizedSearch) {
return 1
}
// Calculate Levenshtein distance
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch)
// Calculate similarity ratio (0 to 1, where 1 is exact match)
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
return 1 - distance / maxLength
}
export class SearchReplaceDiffStrategy implements DiffStrategy {
private fuzzyThreshold: number;
private bufferLines: number;
private fuzzyThreshold: number
private bufferLines: number
constructor(fuzzyThreshold?: number, bufferLines?: number) {
// Use provided threshold or default to exact matching (1.0)
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
// so we use it directly here
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
this.bufferLines = bufferLines ?? BUFFER_LINES;
}
constructor(fuzzyThreshold?: number, bufferLines?: number) {
// Use provided threshold or default to exact matching (1.0)
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
// so we use it directly here
this.fuzzyThreshold = fuzzyThreshold ?? 1.0
this.bufferLines = bufferLines ?? BUFFER_LINES
}
getToolDescription(cwd: string): string {
return `## apply_diff
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
return `## apply_diff
Description: Request to replace existing code using a search and replace block.
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
The tool will maintain proper indentation and formatting while making changes.
@@ -76,7 +78,7 @@ If you're not confident in the exact content to search for, use the read_file to
When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
Parameters:
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd})
- diff: (required) The search/replace block defining the changes.
- start_line: (required) The line number where the search block starts.
- end_line: (required) The line number where the search block ends.
@@ -125,193 +127,204 @@ Your search/replace content here
<start_line>1</start_line>
<end_line>5</end_line>
</apply_diff>`
}
}
async applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult> {
// Extract the search and replace blocks
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
if (!match) {
return {
success: false,
error: `Invalid diff format - missing required SEARCH/REPLACE sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers`
};
}
async applyDiff(
originalContent: string,
diffContent: string,
startLine?: number,
endLine?: number,
): Promise<DiffResult> {
// Extract the search and replace blocks
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/)
if (!match) {
return {
success: false,
error: `Invalid diff format - missing required SEARCH/REPLACE sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers`,
}
}
let [_, searchContent, replaceContent] = match;
let [_, searchContent, replaceContent] = match
// Detect line ending from original content
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
// Detect line ending from original content
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
// Strip line numbers from search and replace content if every line starts with a line number
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
searchContent = stripLineNumbers(searchContent);
replaceContent = stripLineNumbers(replaceContent);
}
// Split content into lines, handling both \n and \r\n
const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
const originalLines = originalContent.split(/\r?\n/);
// Strip line numbers from search and replace content if every line starts with a line number
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
searchContent = stripLineNumbers(searchContent)
replaceContent = stripLineNumbers(replaceContent)
}
// Validate that empty search requires start line
if (searchLines.length === 0 && !startLine) {
return {
success: false,
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`
};
}
// Split content into lines, handling both \n and \r\n
const searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
const replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
const originalLines = originalContent.split(/\r?\n/)
// Validate that empty search requires same start and end line
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
return {
success: false,
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`
};
}
// Initialize search variables
let matchIndex = -1;
let bestMatchScore = 0;
let bestMatchContent = "";
const searchChunk = searchLines.join('\n');
// Validate that empty search requires start line
if (searchLines.length === 0 && !startLine) {
return {
success: false,
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`,
}
}
// Determine search bounds
let searchStartIndex = 0;
let searchEndIndex = originalLines.length;
// Validate that empty search requires same start and end line
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
return {
success: false,
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`,
}
}
// Validate and handle line range if provided
if (startLine && endLine) {
// Convert to 0-based index
const exactStartIndex = startLine - 1;
const exactEndIndex = endLine - 1;
// Initialize search variables
let matchIndex = -1
let bestMatchScore = 0
let bestMatchContent = ""
const searchChunk = searchLines.join("\n")
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
return {
success: false,
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}`,
};
}
// Determine search bounds
let searchStartIndex = 0
let searchEndIndex = originalLines.length
// Try exact match first
const originalChunk = originalLines.slice(exactStartIndex, exactEndIndex + 1).join('\n');
const similarity = getSimilarity(originalChunk, searchChunk);
if (similarity >= this.fuzzyThreshold) {
matchIndex = exactStartIndex;
bestMatchScore = similarity;
bestMatchContent = originalChunk;
} else {
// Set bounds for buffered search
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1));
searchEndIndex = Math.min(originalLines.length, endLine + this.bufferLines);
}
}
// Validate and handle line range if provided
if (startLine && endLine) {
// Convert to 0-based index
const exactStartIndex = startLine - 1
const exactEndIndex = endLine - 1
// If no match found yet, try middle-out search within bounds
if (matchIndex === -1) {
const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2);
let leftIndex = midPoint;
let rightIndex = midPoint + 1;
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
return {
success: false,
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}`,
}
}
// Search outward from the middle within bounds
while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
// Check left side if still in range
if (leftIndex >= searchStartIndex) {
const originalChunk = originalLines.slice(leftIndex, leftIndex + searchLines.length).join('\n');
const similarity = getSimilarity(originalChunk, searchChunk);
if (similarity > bestMatchScore) {
bestMatchScore = similarity;
matchIndex = leftIndex;
bestMatchContent = originalChunk;
}
leftIndex--;
}
// Try exact match first
const originalChunk = originalLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
if (similarity >= this.fuzzyThreshold) {
matchIndex = exactStartIndex
bestMatchScore = similarity
bestMatchContent = originalChunk
} else {
// Set bounds for buffered search
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
searchEndIndex = Math.min(originalLines.length, endLine + this.bufferLines)
}
}
// Check right side if still in range
if (rightIndex <= searchEndIndex - searchLines.length) {
const originalChunk = originalLines.slice(rightIndex, rightIndex + searchLines.length).join('\n');
const similarity = getSimilarity(originalChunk, searchChunk);
if (similarity > bestMatchScore) {
bestMatchScore = similarity;
matchIndex = rightIndex;
bestMatchContent = originalChunk;
}
rightIndex++;
}
}
}
// If no match found yet, try middle-out search within bounds
if (matchIndex === -1) {
const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2)
let leftIndex = midPoint
let rightIndex = midPoint + 1
// Require similarity to meet threshold
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
const searchChunk = searchLines.join('\n');
const originalContentSection = startLine !== undefined && endLine !== undefined
? `\n\nOriginal Content:\n${addLineNumbers(
originalLines.slice(
Math.max(0, startLine - 1 - this.bufferLines),
Math.min(originalLines.length, endLine + this.bufferLines)
).join('\n'),
Math.max(1, startLine - this.bufferLines)
)}`
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`;
// Search outward from the middle within bounds
while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
// Check left side if still in range
if (leftIndex >= searchStartIndex) {
const originalChunk = originalLines.slice(leftIndex, leftIndex + searchLines.length).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
if (similarity > bestMatchScore) {
bestMatchScore = similarity
matchIndex = leftIndex
bestMatchContent = originalChunk
}
leftIndex--
}
const bestMatchSection = bestMatchContent
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
: `\n\nBest Match Found:\n(no match)`;
// Check right side if still in range
if (rightIndex <= searchEndIndex - searchLines.length) {
const originalChunk = originalLines.slice(rightIndex, rightIndex + searchLines.length).join("\n")
const similarity = getSimilarity(originalChunk, searchChunk)
if (similarity > bestMatchScore) {
bestMatchScore = similarity
matchIndex = rightIndex
bestMatchContent = originalChunk
}
rightIndex++
}
}
}
const lineRange = startLine || endLine ?
` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : '';
return {
success: false,
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`
};
}
// Require similarity to meet threshold
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
const searchChunk = searchLines.join("\n")
const originalContentSection =
startLine !== undefined && endLine !== undefined
? `\n\nOriginal Content:\n${addLineNumbers(
originalLines
.slice(
Math.max(0, startLine - 1 - this.bufferLines),
Math.min(originalLines.length, endLine + this.bufferLines),
)
.join("\n"),
Math.max(1, startLine - this.bufferLines),
)}`
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join("\n"))}`
// Get the matched lines from the original content
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
// Get the exact indentation (preserving tabs/spaces) of each line
const originalIndents = matchedLines.map(line => {
const match = line.match(/^[\t ]*/);
return match ? match[0] : '';
});
const bestMatchSection = bestMatchContent
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
: `\n\nBest Match Found:\n(no match)`
// Get the exact indentation of each line in the search block
const searchIndents = searchLines.map(line => {
const match = line.match(/^[\t ]*/);
return match ? match[0] : '';
});
const lineRange =
startLine || endLine
? ` at ${startLine ? `start: ${startLine}` : "start"} to ${endLine ? `end: ${endLine}` : "end"}`
: ""
return {
success: false,
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : "start to end"}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
}
}
// Apply the replacement while preserving exact indentation
const indentedReplaceLines = replaceLines.map((line, i) => {
// Get the matched line's exact indentation
const matchedIndent = originalIndents[0] || '';
// Get the current line's indentation relative to the search content
const currentIndentMatch = line.match(/^[\t ]*/);
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
const searchBaseIndent = searchIndents[0] || '';
// Calculate the relative indentation level
const searchBaseLevel = searchBaseIndent.length;
const currentLevel = currentIndent.length;
const relativeLevel = currentLevel - searchBaseLevel;
// If relative level is negative, remove indentation from matched indent
// If positive, add to matched indent
const finalIndent = relativeLevel < 0
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
: matchedIndent + currentIndent.slice(searchBaseLevel);
return finalIndent + line.trim();
});
// Get the matched lines from the original content
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length)
// Construct the final content
const beforeMatch = originalLines.slice(0, matchIndex);
const afterMatch = originalLines.slice(matchIndex + searchLines.length);
const finalContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding);
return {
success: true,
content: finalContent
};
}
}
// Get the exact indentation (preserving tabs/spaces) of each line
const originalIndents = matchedLines.map((line) => {
const match = line.match(/^[\t ]*/)
return match ? match[0] : ""
})
// Get the exact indentation of each line in the search block
const searchIndents = searchLines.map((line) => {
const match = line.match(/^[\t ]*/)
return match ? match[0] : ""
})
// Apply the replacement while preserving exact indentation
const indentedReplaceLines = replaceLines.map((line, i) => {
// Get the matched line's exact indentation
const matchedIndent = originalIndents[0] || ""
// Get the current line's indentation relative to the search content
const currentIndentMatch = line.match(/^[\t ]*/)
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
const searchBaseIndent = searchIndents[0] || ""
// Calculate the relative indentation level
const searchBaseLevel = searchBaseIndent.length
const currentLevel = currentIndent.length
const relativeLevel = currentLevel - searchBaseLevel
// If relative level is negative, remove indentation from matched indent
// If positive, add to matched indent
const finalIndent =
relativeLevel < 0
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
: matchedIndent + currentIndent.slice(searchBaseLevel)
return finalIndent + line.trim()
})
// Construct the final content
const beforeMatch = originalLines.slice(0, matchIndex)
const afterMatch = originalLines.slice(matchIndex + searchLines.length)
const finalContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding)
return {
success: true,
content: finalContent,
}
}
}

View File

@@ -2,12 +2,12 @@ import { applyPatch } from "diff"
import { DiffStrategy, DiffResult } from "../types"
export class UnifiedDiffStrategy implements DiffStrategy {
getToolDescription(cwd: string): string {
return `## apply_diff
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
return `## apply_diff
Description: Apply a unified diff to a file at the specified path. This tool is useful when you need to make specific modifications to a file based on a set of changes provided in unified diff format (diff -U3).
Parameters:
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd})
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${args.cwd})
- diff: (required) The diff content in unified format to apply to the file.
Format Requirements:
@@ -106,32 +106,32 @@ Usage:
Your diff here
</diff>
</apply_diff>`
}
}
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
try {
const result = applyPatch(originalContent, diffContent)
if (result === false) {
return {
success: false,
error: "Failed to apply unified diff - patch rejected",
details: {
searchContent: diffContent
}
}
}
return {
success: true,
content: result
}
} catch (error) {
return {
success: false,
error: `Error applying unified diff: ${error.message}`,
details: {
searchContent: diffContent
}
}
}
}
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
try {
const result = applyPatch(originalContent, diffContent)
if (result === false) {
return {
success: false,
error: "Failed to apply unified diff - patch rejected",
details: {
searchContent: diffContent,
},
}
}
return {
success: true,
content: result,
}
} catch (error) {
return {
success: false,
error: `Error applying unified diff: ${error.message}`,
details: {
searchContent: diffContent,
},
}
}
}
}

View File

@@ -2,31 +2,35 @@
* Interface for implementing different diff strategies
*/
export type DiffResult =
| { success: true; content: string }
| { success: false; error: string; details?: {
similarity?: number;
threshold?: number;
matchedRange?: { start: number; end: number };
searchContent?: string;
bestMatch?: string;
}};
export type DiffResult =
| { success: true; content: string }
| {
success: false
error: string
details?: {
similarity?: number
threshold?: number
matchedRange?: { start: number; end: number }
searchContent?: string
bestMatch?: string
}
}
export interface DiffStrategy {
/**
* Get the tool description for this diff strategy
* @param cwd The current working directory
* @returns The complete tool description including format requirements and examples
*/
getToolDescription(cwd: string): string
/**
* Get the tool description for this diff strategy
* @param args The tool arguments including cwd and toolOptions
* @returns The complete tool description including format requirements and examples
*/
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string
/**
* Apply a diff to the original content
* @param originalContent The original file content
* @param diffContent The diff content in the strategy's format
* @param startLine Optional line number where the search block starts. If not provided, searches the entire file.
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
* @returns A DiffResult object containing either the successful result or error details
*/
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult>
}
/**
* Apply a diff to the original content
* @param originalContent The original file content
* @param diffContent The diff content in the strategy's format
* @param startLine Optional line number where the search block starts. If not provided, searches the entire file.
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
* @returns A DiffResult object containing either the successful result or error details
*/
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult>
}