From 69bfd89c79dc6ec8abc3a0ac37f572c15cde7ee6 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 3 Apr 2025 11:01:30 -0400 Subject: [PATCH] Remove code for old diff strategies --- src/core/diff/DiffStrategy.ts | 5 +- .../strategies/__tests__/new-unified.test.ts | 738 ------------------ .../diff/strategies/__tests__/unified.test.ts | 228 ------ .../__tests__/edit-strategies.test.ts | 295 ------- .../__tests__/search-strategies.test.ts | 262 ------- .../strategies/new-unified/edit-strategies.ts | 306 -------- src/core/diff/strategies/new-unified/index.ts | 354 --------- .../new-unified/search-strategies.ts | 408 ---------- src/core/diff/strategies/new-unified/types.ts | 20 - src/core/diff/strategies/unified.ts | 140 ---- 10 files changed, 2 insertions(+), 2754 deletions(-) delete mode 100644 src/core/diff/strategies/__tests__/new-unified.test.ts delete mode 100644 src/core/diff/strategies/__tests__/unified.test.ts delete mode 100644 src/core/diff/strategies/new-unified/__tests__/edit-strategies.test.ts delete mode 100644 src/core/diff/strategies/new-unified/__tests__/search-strategies.test.ts delete mode 100644 src/core/diff/strategies/new-unified/edit-strategies.ts delete mode 100644 src/core/diff/strategies/new-unified/index.ts delete mode 100644 src/core/diff/strategies/new-unified/search-strategies.ts delete mode 100644 src/core/diff/strategies/new-unified/types.ts delete mode 100644 src/core/diff/strategies/unified.ts diff --git a/src/core/diff/DiffStrategy.ts b/src/core/diff/DiffStrategy.ts index abd89ffb67a..1202068ad2d 100644 --- a/src/core/diff/DiffStrategy.ts +++ b/src/core/diff/DiffStrategy.ts @@ -1,7 +1,6 @@ import type { DiffStrategy } from "./types" -import { NewUnifiedDiffStrategy } from "./strategies/new-unified" import { MultiSearchReplaceDiffStrategy } from "./strategies/multi-search-replace" -import { EXPERIMENT_IDS, ExperimentId } from "../../shared/experiments" +import { ExperimentId } from "../../shared/experiments" export type { DiffStrategy } @@ -11,7 +10,7 @@ export type { DiffStrategy } * @returns The appropriate diff strategy for the model */ -export type DiffStrategyName = "unified" | "multi-search-and-replace" +export type DiffStrategyName = "multi-search-and-replace" type GetDiffStrategyOptions = { model: string diff --git a/src/core/diff/strategies/__tests__/new-unified.test.ts b/src/core/diff/strategies/__tests__/new-unified.test.ts deleted file mode 100644 index 8832f9e7c08..00000000000 --- a/src/core/diff/strategies/__tests__/new-unified.test.ts +++ /dev/null @@ -1,738 +0,0 @@ -import { NewUnifiedDiffStrategy } from "../new-unified" - -describe("main", () => { - let strategy: NewUnifiedDiffStrategy - - 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) - }) - - 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) - }) - }) - - describe("getToolDescription", () => { - it("should return tool description with correct cwd", () => { - const cwd = "/test/path" - const description = strategy.getToolDescription({ cwd }) - - expect(description).toContain("apply_diff Tool - Generate Precise Code Changes") - expect(description).toContain(cwd) - expect(description).toContain("Step-by-Step Instructions") - expect(description).toContain("Requirements") - expect(description).toContain("Examples") - expect(description).toContain("Parameters:") - }) - }) - - it("should apply simple diff correctly", async () => { - const original = `line1 -line2 -line3` - - const diff = `--- a/file.txt -+++ b/file.txt -@@ ... @@ - line1 -+new line - line2 --line3 -+modified line3` - - 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`) - } - }) - - it("should handle multiple hunks", async () => { - const original = `line1 -line2 -line3 -line4 -line5` - - const diff = `--- a/file.txt -+++ b/file.txt -@@ ... @@ - line1 -+new line - line2 --line3 -+modified line3 -@@ ... @@ - line4 --line5 -+modified line5 -+new line at end` - - 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`) - } - }) - - it("should handle complex large", async () => { - const original = `line1 -line2 -line3 -line4 -line5 -line6 -line7 -line8 -line9 -line10` - - const diff = `--- a/file.txt -+++ b/file.txt -@@ ... @@ - line1 -+header line -+another header - line2 --line3 --line4 -+modified line3 -+modified line4 -+extra line -@@ ... @@ - line6 -+middle section - line7 --line8 -+changed line8 -+bonus line -@@ ... @@ - line9 --line10 -+final line -+very last line` - - 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 -modified line3 -modified line4 -extra line -line5 -line6 -middle section -line7 -changed line8 -bonus line -line9 -final line -very last line`) - } - }) - - it("should handle indentation changes", async () => { - const original = `first line - indented line - double indented line - back to single indent -no indent - indented again - double indent again - triple indent - back to single -last line` - - const diff = `--- original -+++ modified -@@ ... @@ - first line - indented line -+ tab indented line -+ new indented line - double indented line - back to single indent - no indent - indented again - double indent again -- triple indent -+ hi there mate - back to single - last line` - - const expected = `first line - indented line - tab indented line - new indented line - double indented line - back to single indent -no indent - indented again - double indent again - hi there mate - back to single -last line` - - 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): - if n == 0: - return 1 - else: - return n * factorial(n-1)` - const diff = `@@ ... @@ --def factorial(n): -- if n == 0: -- return 1 -- else: -- return n * factorial(n-1) -+def factorial(number): -+ if number == 0: -+ return 1 -+ else: -+ return number * factorial(number-1)` - - 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) - } - }) - - 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*/ -function PrimeCheck(candidate){ - isPrime = true; - for(var i = 2; i < candidate && isPrime; i++){ - if(candidate%i === 0){ - isPrime = false; - } else { - isPrime = true; - } - } - if(isPrime){ - primeArray.push(candidate); - } - return primeArray; -} -/*Write the code that runs the above until the - l ength of the array equa*ls the number of primes - desired*/ - -var numPrimes = prompt("How many primes?"); - -//Display the finished array of primes - -//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested -for (var i = 2; primeArray.length < numPrimes; i++) { - PrimeCheck(i); // -} -console.log(primeArray); -` - - const diff = `--- test_diff.js -+++ test_diff.js -@@ ... @@ --//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*/ - function PrimeCheck(candidate){ - isPrime = true; - for(var i = 2; i < candidate && isPrime; i++){ -@@ ... @@ - return primeArray; - } --/*Write the code that runs the above until the -- l ength of the array equa*ls the number of primes -- desired*/ - - var numPrimes = prompt("How many primes?"); - --//Display the finished array of primes -- --//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested - for (var i = 2; primeArray.length < numPrimes; i++) { -- PrimeCheck(i); // -+ PrimeCheck(i); - } - console.log(primeArray);` - - const expected = `var primeArray = []; -function PrimeCheck(candidate){ - isPrime = true; - for(var i = 2; i < candidate && isPrime; i++){ - if(candidate%i === 0){ - isPrime = false; - } else { - isPrime = true; - } - } - if(isPrime){ - primeArray.push(candidate); - } - return primeArray; -} - -var numPrimes = prompt("How many primes?"); - -for (var i = 2; primeArray.length < numPrimes; i++) { - PrimeCheck(i); -} -console.log(primeArray); -` - - 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) - }) - - 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) - }) - - 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) - }) - - it("should handle overlapping hunks", async () => { - const original = `line1 -line2 -line3 -line4 -line5` - const overlappingDiff = `--- a/file.txt -+++ b/file.txt -@@ ... @@ - line1 - line2 --line3 -+modified3 - line4 -@@ ... @@ - line2 --line3 --line4 -+modified3and4 - line5` - - 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 -+++ b/file.txt -@@ ... @@ - line1 - --line3 -+line3modified - - line5` - - const result = await strategy.applyDiff(original, emptyLinesDiff) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`line1 - -line3modified - -line5`) - } - }) - - 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") - } - }) - - 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;") - } - }) - - 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") - } - }) - }) - - 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; -} - -function subtract(a, b) { - return a - b; -} - -function multiply(a, b) { - return a + b; // Bug here -}` - - 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) { - return a + b; -} - -function subtract(a, b) { - return a - b; -} - -function multiply(a, b) { - return a * b; -}`) - } - }) - - it("should handle multiple similar sections with correct context", async () => { - const original = `if (condition) { - doSomething(); - doSomething(); - doSomething(); -} - -if (otherCondition) { - doSomething(); - doSomething(); - doSomething(); -}` - - const diff = `--- a/file.js -+++ b/file.js -@@ ... @@ - if (otherCondition) { - doSomething(); -- doSomething(); -+ doSomethingElse(); - doSomething(); - }` - - const result = await strategy.applyDiff(original, diff) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`if (condition) { - doSomething(); - doSomething(); - doSomething(); -} - -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'; -import { join } from 'path'; -import { Logger } from './logger'; - -const logger = new Logger(); - -async function processFile(filePath: string) { - try { - const data = await readFile(filePath, 'utf8'); - logger.info('File read successfully'); - return data; - } catch (error) { - logger.error('Failed to read file:', error); - throw error; - } -} - -function validateInput(input: string): boolean { - if (!input) { - logger.warn('Empty input received'); - return false; - } - return input.length > 0; -} - -async function writeOutput(data: string) { - logger.info('Processing output'); - // TODO: Implement output writing - return Promise.resolve(); -} - -function parseConfig(configPath: string) { - logger.debug('Reading config from:', configPath); - // Basic config parsing - return { - enabled: true, - maxRetries: 3 - }; -} - -export { - processFile, - validateInput, - writeOutput, - parseConfig -};` - - const diff = `--- a/file.ts -+++ b/file.ts -@@ ... @@ --import { readFile } from 'fs'; -+import { readFile, writeFile } from 'fs'; - import { join } from 'path'; --import { Logger } from './logger'; -+import { Logger } from './utils/logger'; -+import { Config } from './types'; - --const logger = new Logger(); -+const logger = new Logger('FileProcessor'); - - async function processFile(filePath: string) { - try { - const data = await readFile(filePath, 'utf8'); -- logger.info('File read successfully'); -+ logger.info(\`File \${filePath} read successfully\`); - return data; - } catch (error) { -- logger.error('Failed to read file:', error); -+ logger.error(\`Failed to read file \${filePath}:\`, error); - throw error; - } - } - - function validateInput(input: string): boolean { - if (!input) { -- logger.warn('Empty input received'); -+ logger.warn('Validation failed: Empty input received'); - return false; - } -- return input.length > 0; -+ return input.trim().length > 0; - } - --async function writeOutput(data: string) { -- logger.info('Processing output'); -- // TODO: Implement output writing -- return Promise.resolve(); -+async function writeOutput(data: string, outputPath: string) { -+ try { -+ await writeFile(outputPath, data, 'utf8'); -+ logger.info(\`Output written to \${outputPath}\`); -+ } catch (error) { -+ logger.error(\`Failed to write output to \${outputPath}:\`, error); -+ throw error; -+ } - } - --function parseConfig(configPath: string) { -- logger.debug('Reading config from:', configPath); -- // Basic config parsing -- return { -- enabled: true, -- maxRetries: 3 -- }; -+async function parseConfig(configPath: string): Promise { -+ try { -+ const configData = await readFile(configPath, 'utf8'); -+ logger.debug(\`Reading config from \${configPath}\`); -+ return JSON.parse(configData); -+ } catch (error) { -+ logger.error(\`Failed to parse config from \${configPath}:\`, error); -+ throw error; -+ } - } - - export { - processFile, - validateInput, - writeOutput, -- parseConfig -+ parseConfig, -+ type Config - };` - - const expected = `import { readFile, writeFile } from 'fs'; -import { join } from 'path'; -import { Logger } from './utils/logger'; -import { Config } from './types'; - -const logger = new Logger('FileProcessor'); - -async function processFile(filePath: string) { - try { - const data = await readFile(filePath, 'utf8'); - logger.info(\`File \${filePath} read successfully\`); - return data; - } catch (error) { - logger.error(\`Failed to read file \${filePath}:\`, error); - throw error; - } -} - -function validateInput(input: string): boolean { - if (!input) { - logger.warn('Validation failed: Empty input received'); - return false; - } - return input.trim().length > 0; -} - -async function writeOutput(data: string, outputPath: string) { - try { - await writeFile(outputPath, data, 'utf8'); - logger.info(\`Output written to \${outputPath}\`); - } catch (error) { - logger.error(\`Failed to write output to \${outputPath}:\`, error); - throw error; - } -} - -async function parseConfig(configPath: string): Promise { - try { - const configData = await readFile(configPath, 'utf8'); - logger.debug(\`Reading config from \${configPath}\`); - return JSON.parse(configData); - } catch (error) { - logger.error(\`Failed to parse config from \${configPath}:\`, error); - throw error; - } -} - -export { - processFile, - validateInput, - writeOutput, - parseConfig, - type Config -};` - - const result = await strategy.applyDiff(original, diff) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(expected) - } - }) - }) -}) diff --git a/src/core/diff/strategies/__tests__/unified.test.ts b/src/core/diff/strategies/__tests__/unified.test.ts deleted file mode 100644 index 1d9847b3c51..00000000000 --- a/src/core/diff/strategies/__tests__/unified.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { UnifiedDiffStrategy } from "../unified" - -describe("UnifiedDiffStrategy", () => { - let strategy: 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("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) => { - return sum + item; - }, 0); -} - -export { calculateTotal };` - - const diffContent = `--- src/utils/helper.ts -+++ src/utils/helper.ts -@@ -1,9 +1,10 @@ - import { Logger } from '../logger'; - - function calculateTotal(items: number[]): number { -- return items.reduce((sum, item) => { -- return sum + item; -+ const total = items.reduce((sum, item) => { -+ return sum + item * 1.1; // Add 10% markup - }, 0); -+ return Math.round(total * 100) / 100; // Round to 2 decimal places - } - - export { calculateTotal };` - - const expected = `import { Logger } from '../logger'; - -function calculateTotal(items: number[]): number { - const total = items.reduce((sum, item) => { - return sum + item * 1.1; // Add 10% markup - }, 0); - return Math.round(total * 100) / 100; // Round to 2 decimal places -} - -export { calculateTotal };` - - 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 { - add(a: number, b: number): number { - return a + b; - } -}` - - const diffContent = `--- src/Calculator.ts -+++ src/Calculator.ts -@@ -1,5 +1,9 @@ - class Calculator { - add(a: number, b: number): number { - return a + b; - } -+ -+ multiply(a: number, b: number): number { -+ return a * b; -+ } - }` - - const expected = `class Calculator { - add(a: number, b: number): number { - return a + b; - } - - multiply(a: number, b: number): number { - return a * b; - } -}` - - 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'; -import { Button } from './components'; - -function App() { - const [count, setCount] = useState(0); - return ; -}` - - const diffContent = `--- src/App.tsx -+++ src/App.tsx -@@ -1,7 +1,8 @@ --import { useState } from 'react'; -+import { useState, useEffect } from 'react'; - import { Button } from './components'; - - function App() { - const [count, setCount] = useState(0); -+ useEffect(() => { document.title = \`Count: \${count}\` }, [count]); - return ; - }` - - const expected = `import { useState, useEffect } from 'react'; -import { Button } from './components'; - -function App() { - const [count, setCount] = useState(0); - useEffect(() => { document.title = \`Count: \${count}\` }, [count]); - return ; -}` - - 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) => { - if (err) throw err; - const processed = data.toUpperCase(); - writeFile(path, processed, (err) => { - if (err) throw err; - }); - }); -} - -export { processFile };` - - const diffContent = `--- src/file-processor.ts -+++ src/file-processor.ts -@@ -1,12 +1,14 @@ --import { readFile, writeFile } from 'fs'; -+import { promises as fs } from 'fs'; -+import { join } from 'path'; - --function processFile(path: string) { -- readFile(path, 'utf8', (err, data) => { -- if (err) throw err; -+async function processFile(path: string) { -+ try { -+ const data = await fs.readFile(join(__dirname, path), 'utf8'); - const processed = data.toUpperCase(); -- writeFile(path, processed, (err) => { -- if (err) throw err; -- }); -- }); -+ await fs.writeFile(join(__dirname, path), processed); -+ } catch (error) { -+ console.error('Failed to process file:', error); -+ throw error; -+ } - } - - export { processFile };` - - const expected = `import { promises as fs } from 'fs'; -import { join } from 'path'; - -async function processFile(path: string) { - try { - const data = await fs.readFile(join(__dirname, path), 'utf8'); - const processed = data.toUpperCase(); - await fs.writeFile(join(__dirname, path), processed); - } catch (error) { - console.error('Failed to process file:', error); - throw error; - } -} - -export { processFile };` - - 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 -+++ empty.ts -@@ -0,0 +1,3 @@ -+export function greet(name: string): string { -+ return \`Hello, \${name}!\`; -+}` - - 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) - } - }) - }) -}) diff --git a/src/core/diff/strategies/new-unified/__tests__/edit-strategies.test.ts b/src/core/diff/strategies/new-unified/__tests__/edit-strategies.test.ts deleted file mode 100644 index 2bc35540baf..00000000000 --- a/src/core/diff/strategies/new-unified/__tests__/edit-strategies.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { applyContextMatching, applyDMP, applyGitFallback } from "../edit-strategies" -import { Hunk } from "../types" - -const testCases = [ - { - name: "should return original content if no match is found", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "add", content: "line2" }, - ], - } as Hunk, - content: ["line1", "line3"], - matchPosition: -1, - expected: { - confidence: 0, - result: ["line1", "line3"], - }, - expectedResult: "line1\nline3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a simple add change", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "add", content: "line2" }, - ], - } as Hunk, - content: ["line1", "line3"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line2", "line3"], - }, - expectedResult: "line1\nline2\nline3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a simple remove change", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "remove", content: "line2" }, - ], - } as Hunk, - content: ["line1", "line2", "line3"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line3"], - }, - expectedResult: "line1\nline3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a simple context change", - hunk: { - changes: [{ type: "context", content: "line1" }], - } as Hunk, - content: ["line1", "line2", "line3"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line2", "line3"], - }, - expectedResult: "line1\nline2\nline3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a multi-line add change", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "add", content: "line2\nline3" }, - ], - } as Hunk, - content: ["line1", "line4"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line2\nline3", "line4"], - }, - expectedResult: "line1\nline2\nline3\nline4", - strategies: ["context", "dmp"], - }, - { - name: "should apply a multi-line remove change", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "remove", content: "line2\nline3" }, - ], - } as Hunk, - content: ["line1", "line2", "line3", "line4"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line4"], - }, - expectedResult: "line1\nline4", - strategies: ["context", "dmp"], - }, - { - name: "should apply a multi-line context change", - hunk: { - changes: [ - { type: "context", content: "line1" }, - { type: "context", content: "line2\nline3" }, - ], - } as Hunk, - content: ["line1", "line2", "line3", "line4"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["line1", "line2\nline3", "line4"], - }, - expectedResult: "line1\nline2\nline3\nline4", - strategies: ["context", "dmp"], - }, - { - name: "should apply a change with indentation", - hunk: { - changes: [ - { type: "context", content: " line1" }, - { type: "add", content: " line2" }, - ], - } as Hunk, - content: [" line1", " line3"], - matchPosition: 0, - expected: { - confidence: 1, - result: [" line1", " line2", " line3"], - }, - expectedResult: " line1\n line2\n line3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a change with mixed indentation", - hunk: { - changes: [ - { type: "context", content: "\tline1" }, - { type: "add", content: " line2" }, - ], - } as Hunk, - content: ["\tline1", " line3"], - matchPosition: 0, - expected: { - confidence: 1, - result: ["\tline1", " line2", " line3"], - }, - expectedResult: "\tline1\n line2\n line3", - strategies: ["context", "dmp"], - }, - { - name: "should apply a change with mixed indentation and multi-line", - hunk: { - changes: [ - { type: "context", content: " line1" }, - { type: "add", content: "\tline2\n line3" }, - ], - } as Hunk, - content: [" line1", " line4"], - matchPosition: 0, - expected: { - confidence: 1, - result: [" line1", "\tline2\n line3", " line4"], - }, - expectedResult: " line1\n\tline2\n line3\n line4", - strategies: ["context", "dmp"], - }, - { - name: "should apply a complex change with mixed indentation and multi-line", - hunk: { - changes: [ - { type: "context", content: " line1" }, - { type: "remove", content: " line2" }, - { type: "add", content: "\tline3\n line4" }, - { type: "context", content: " line5" }, - ], - } as Hunk, - content: [" line1", " line2", " line5", " line6"], - matchPosition: 0, - expected: { - confidence: 1, - result: [" line1", "\tline3\n line4", " line5", " line6"], - }, - expectedResult: " line1\n\tline3\n line4\n line5\n line6", - strategies: ["context", "dmp"], - }, - { - name: "should apply a complex change with mixed indentation and multi-line and context", - hunk: { - changes: [ - { type: "context", content: " line1" }, - { type: "remove", content: " line2" }, - { type: "add", content: "\tline3\n line4" }, - { type: "context", content: " line5" }, - { type: "context", content: " line6" }, - ], - } as Hunk, - content: [" line1", " line2", " line5", " line6", " line7"], - matchPosition: 0, - expected: { - confidence: 1, - result: [" line1", "\tline3\n line4", " line5", " line6", " line7"], - }, - expectedResult: " line1\n\tline3\n line4\n line5\n line6\n line7", - strategies: ["context", "dmp"], - }, - { - name: "should apply a complex change with mixed indentation and multi-line and context and a different match position", - hunk: { - changes: [ - { type: "context", content: " line1" }, - { type: "remove", content: " line2" }, - { type: "add", content: "\tline3\n line4" }, - { type: "context", content: " line5" }, - { type: "context", content: " line6" }, - ], - } as Hunk, - content: [" line0", " line1", " line2", " line5", " line6", " line7"], - matchPosition: 1, - expected: { - confidence: 1, - result: [" line0", " line1", "\tline3\n line4", " line5", " line6", " line7"], - }, - expectedResult: " line0\n line1\n\tline3\n line4\n line5\n line6\n line7", - strategies: ["context", "dmp"], - }, -] - -describe("applyContextMatching", () => { - testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => { - if (!strategies?.includes("context")) { - return - } - it(name, () => { - const result = applyContextMatching(hunk, content, matchPosition) - expect(result.result.join("\n")).toEqual(expectedResult) - expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence) - expect(result.strategy).toBe("context") - }) - }) -}) - -describe("applyDMP", () => { - testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => { - if (!strategies?.includes("dmp")) { - return - } - it(name, () => { - const result = applyDMP(hunk, content, matchPosition) - expect(result.result.join("\n")).toEqual(expectedResult) - expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence) - expect(result.strategy).toBe("dmp") - }) - }) -}) - -describe("applyGitFallback", () => { - it("should successfully apply changes using git operations", async () => { - const hunk = { - changes: [ - { type: "context", content: "line1", indent: "" }, - { type: "remove", content: "line2", indent: "" }, - { type: "add", content: "new line2", indent: "" }, - { type: "context", content: "line3", indent: "" }, - ], - } as Hunk - - const content = ["line1", "line2", "line3"] - const result = await applyGitFallback(hunk, content) - - expect(result.result.join("\n")).toEqual("line1\nnew line2\nline3") - expect(result.confidence).toBe(1) - expect(result.strategy).toBe("git-fallback") - }) - - it("should return original content with 0 confidence when changes cannot be applied", async () => { - const hunk = { - changes: [ - { type: "context", content: "nonexistent", indent: "" }, - { type: "add", content: "new line", indent: "" }, - ], - } as Hunk - - const content = ["line1", "line2", "line3"] - const result = await applyGitFallback(hunk, content) - - expect(result.result).toEqual(content) - expect(result.confidence).toBe(0) - expect(result.strategy).toBe("git-fallback") - }) -}) diff --git a/src/core/diff/strategies/new-unified/__tests__/search-strategies.test.ts b/src/core/diff/strategies/new-unified/__tests__/search-strategies.test.ts deleted file mode 100644 index 5bee5371321..00000000000 --- a/src/core/diff/strategies/new-unified/__tests__/search-strategies.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { findAnchorMatch, findExactMatch, findSimilarityMatch, findLevenshteinMatch } from "../search-strategies" - -type SearchStrategy = ( - searchStr: string, - content: string[], - startIndex?: number, -) => { - index: number - confidence: number - strategy: string -} - -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"], - }, -] - -describe("findExactMatch", () => { - testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => { - if (!strategies?.includes("exact")) { - return - } - it(name, () => { - const result = findExactMatch(searchStr, content, startIndex) - expect(result.index).toBe(expected.index) - expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence) - expect(result.strategy).toMatch(/exact(-overlapping)?/) - }) - }) -}) - -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", - content: [ - "different line 1", - "different line 2", - "different line 3", - "another unique line", - "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", - content: [ - "line1", - "line2", - "unique line", - "context line 1", - "context line 2", - "line 6", - "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 }, - }, - ] - - 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) - expect(result.strategy).toBe("anchor") - }) - }) -}) - -describe("findSimilarityMatch", () => { - testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => { - if (!strategies?.includes("similarity")) { - return - } - it(name, () => { - const result = findSimilarityMatch(searchStr, content, startIndex) - expect(result.index).toBe(expected.index) - expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence) - expect(result.strategy).toBe("similarity") - }) - }) -}) - -describe("findLevenshteinMatch", () => { - testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => { - if (!strategies?.includes("levenshtein")) { - return - } - it(name, () => { - const result = findLevenshteinMatch(searchStr, content, startIndex) - expect(result.index).toBe(expected.index) - expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence) - expect(result.strategy).toBe("levenshtein") - }) - }) -}) diff --git a/src/core/diff/strategies/new-unified/edit-strategies.ts b/src/core/diff/strategies/new-unified/edit-strategies.ts deleted file mode 100644 index 8998132a821..00000000000 --- a/src/core/diff/strategies/new-unified/edit-strategies.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { diff_match_patch } from "diff-match-patch" -import { EditResult, Hunk } from "./types" -import { getDMPSimilarity, validateEditResult } from "./search-strategies" -import * as path from "path" -import simpleGit, { SimpleGit } from "simple-git" -import * as tmp from "tmp" -import * as fs from "fs" - -// Helper function to infer indentation - simplified version -function inferIndentation(line: string, contextLines: string[], previousIndent: string = ""): string { - // If the line has explicit indentation in the change, use it exactly - const lineMatch = line.match(/^(\s+)/) - if (lineMatch) { - return lineMatch[1] - } - - // If we have context lines, use the indentation from the first context line - const contextLine = contextLines[0] - if (contextLine) { - const contextMatch = contextLine.match(/^(\s+)/) - if (contextMatch) { - return contextMatch[1] - } - } - - // Fallback to previous indent - return previousIndent -} - -// Context matching edit strategy -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) { - if (change.type === "context") { - // Use the original line from content if available - if (sourceIndex < content.length) { - newResult.push(content[sourceIndex]) - } else { - const line = change.indent ? change.indent + change.content : change.content - newResult.push(line) - } - sourceIndex++ - } else if (change.type === "add") { - // Use exactly the indentation from the change - const baseIndent = change.indent || "" - - // Handle multi-line additions - const lines = change.content.split("\n").map((line) => { - // If the line already has indentation, preserve it relative to the base indent - const lineIndentMatch = line.match(/^(\s*)(.*)/) - if (lineIndentMatch) { - const [, lineIndent, content] = lineIndentMatch - // Only add base indent if the line doesn't already have it - return lineIndent ? line : baseIndent + content - } - return baseIndent + line - }) - - newResult.push(...lines) - } else if (change.type === "remove") { - // Handle multi-line removes by incrementing sourceIndex for each line - const removedLines = change.content.split("\n").length - sourceIndex += removedLines - } - } - - // Append remaining content - newResult.push(...content.slice(sourceIndex)) - - // Calculate confidence based on the actual changes - const afterText = newResult.slice(matchPosition, newResult.length - (content.length - sourceIndex)).join("\n") - - const confidence = validateEditResult(hunk, afterText) - - return { - confidence, - result: newResult, - strategy: "context", - } -} - -// DMP edit strategy -export function applyDMP(hunk: Hunk, content: string[], matchPosition: number): EditResult { - if (matchPosition === -1) { - return { confidence: 0, result: content, strategy: "dmp" } - } - - const dmp = new diff_match_patch() - - // Calculate total lines in before block accounting for multi-line content - 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 - .filter((change) => change.type === "context" || change.type === "remove") - .map((change) => { - if (change.originalLine) { - return change.originalLine - } - return change.indent ? change.indent + change.content : change.content - }) - - // Build AFTER block (context + additions) - const afterLines = hunk.changes - .filter((change) => change.type === "context" || change.type === "add") - .map((change) => { - if (change.originalLine) { - return change.originalLine - } - return change.indent ? change.indent + change.content : change.content - }) - - // Convert to text with proper line endings - const beforeText = beforeLines.join("\n") - const afterText = afterLines.join("\n") - - // Create and apply patch - const patch = dmp.patch_make(beforeText, afterText) - const targetText = content.slice(matchPosition, matchPosition + beforeLineCount).join("\n") - const [patchedText] = dmp.patch_apply(patch, targetText) - - // Split result and preserve line endings - const patchedLines = patchedText.split("\n") - - // Construct final result - const newResult = [ - ...content.slice(0, matchPosition), - ...patchedLines, - ...content.slice(matchPosition + beforeLineCount), - ] - - const confidence = validateEditResult(hunk, patchedText) - - return { - confidence, - result: newResult, - strategy: "dmp", - } -} - -// Git fallback strategy that works with full content -export async function applyGitFallback(hunk: Hunk, content: string[]): Promise { - let tmpDir: tmp.DirResult | undefined - - try { - tmpDir = tmp.dirSync({ unsafeCleanup: true }) - const git: SimpleGit = simpleGit(tmpDir.name) - - await git.init() - await git.addConfig("user.name", "Temp") - await git.addConfig("user.email", "temp@example.com") - // Prevent Git from automatically converting line endings - await git.addConfig("core.autocrlf", "false") - - const filePath = path.join(tmpDir.name, "file.txt") - - 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) - - // Ensure consistent line endings (LF only) in all text operations - const searchText = searchLines.join("\n") - const replaceText = replaceLines.join("\n") - const originalText = content.join("\n") - - try { - fs.writeFileSync(filePath, originalText) - await git.add("file.txt") - const originalCommit = await git.commit("original") - console.log("Strategy 1 - Original commit:", originalCommit.commit) - - fs.writeFileSync(filePath, searchText) - await git.add("file.txt") - const searchCommit1 = await git.commit("search") - console.log("Strategy 1 - Search commit:", searchCommit1.commit) - - fs.writeFileSync(filePath, replaceText) - await git.add("file.txt") - const replaceCommit = await git.commit("replace") - console.log("Strategy 1 - Replace commit:", replaceCommit.commit) - - console.log("Strategy 1 - Attempting checkout of:", originalCommit.commit) - await git.raw(["checkout", originalCommit.commit]) - try { - console.log("Strategy 1 - Attempting cherry-pick of:", replaceCommit.commit) - await git.raw(["cherry-pick", "--minimal", replaceCommit.commit]) - - const newText = fs.readFileSync(filePath, "utf-8") - // Normalize line endings to LF before splitting - const normalizedText = newText.replace(/\r\n/g, "\n") - const newLines = normalizedText.split("\n") - return { - confidence: 1, - result: newLines, - strategy: "git-fallback", - } - } catch (cherryPickError) { - console.error("Strategy 1 failed with merge conflict") - } - } catch (error) { - console.error("Strategy 1 failed:", error) - } - - try { - await git.init() - await git.addConfig("user.name", "Temp") - await git.addConfig("user.email", "temp@example.com") - // Prevent Git from automatically converting line endings - await git.addConfig("core.autocrlf", "false") - - fs.writeFileSync(filePath, searchText) - await git.add("file.txt") - const searchCommit = await git.commit("search") - const searchHash = searchCommit.commit.replace(/^HEAD /, "") - console.log("Strategy 2 - Search commit:", searchHash) - - fs.writeFileSync(filePath, replaceText) - await git.add("file.txt") - const replaceCommit = await git.commit("replace") - const replaceHash = replaceCommit.commit.replace(/^HEAD /, "") - console.log("Strategy 2 - Replace commit:", replaceHash) - - console.log("Strategy 2 - Attempting checkout of:", searchHash) - await git.raw(["checkout", searchHash]) - fs.writeFileSync(filePath, originalText) - await git.add("file.txt") - const originalCommit2 = await git.commit("original") - console.log("Strategy 2 - Original commit:", originalCommit2.commit) - - try { - console.log("Strategy 2 - Attempting cherry-pick of:", replaceHash) - await git.raw(["cherry-pick", "--minimal", replaceHash]) - - const newText = fs.readFileSync(filePath, "utf-8") - // Normalize line endings to LF before splitting - const normalizedText = newText.replace(/\r\n/g, "\n") - const newLines = normalizedText.split("\n") - return { - confidence: 1, - result: newLines, - strategy: "git-fallback", - } - } catch (cherryPickError) { - console.error("Strategy 2 failed with merge conflict") - } - } catch (error) { - console.error("Strategy 2 failed:", error) - } - - console.error("Git fallback failed") - return { confidence: 0, result: content, strategy: "git-fallback" } - } catch (error) { - console.error("Git fallback strategy failed:", error) - return { confidence: 0, result: content, strategy: "git-fallback" } - } finally { - if (tmpDir) { - tmpDir.removeCallback() - } - } -} - -// Main edit function that tries strategies sequentially -export async function applyEdit( - hunk: Hunk, - content: string[], - matchPosition: number, - confidence: number, - confidenceThreshold: number = 0.97, -): Promise { - // 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...`, - ) - return applyGitFallback(hunk, content) - } - - // Try each strategy in sequence until one succeeds - const strategies = [ - { name: "dmp", apply: () => applyDMP(hunk, content, matchPosition) }, - { name: "context", apply: () => applyContextMatching(hunk, content, matchPosition) }, - { name: "git-fallback", apply: () => applyGitFallback(hunk, content) }, - ] - - // Try strategies sequentially until one succeeds - for (const strategy of strategies) { - const result = await strategy.apply() - if (result.confidence >= confidenceThreshold) { - return result - } - } - - return { confidence: 0, result: content, strategy: "none" } -} diff --git a/src/core/diff/strategies/new-unified/index.ts b/src/core/diff/strategies/new-unified/index.ts deleted file mode 100644 index 5b385616f6f..00000000000 --- a/src/core/diff/strategies/new-unified/index.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { Diff, Hunk, Change } from "./types" -import { findBestMatch, prepareSearchString } from "./search-strategies" -import { applyEdit } from "./edit-strategies" -import { DiffResult, DiffStrategy } from "../../types" - -export class NewUnifiedDiffStrategy implements DiffStrategy { - private readonly confidenceThreshold: number - - getName(): string { - return "NewUnified" - } - - constructor(confidenceThreshold: number = 1) { - this.confidenceThreshold = Math.max(confidenceThreshold, 0.8) - } - - private parseUnifiedDiff(diff: string): Diff { - const MAX_CONTEXT_LINES = 6 // Number of context lines to keep before/after changes - const lines = diff.split("\n") - const hunks: Hunk[] = [] - let currentHunk: Hunk | null = null - - let i = 0 - while (i < lines.length && !lines[i].startsWith("@@")) { - i++ - } - - for (; i < lines.length; i++) { - const line = lines[i] - - if (line.startsWith("@@")) { - if ( - currentHunk && - currentHunk.changes.length > 0 && - currentHunk.changes.some((change) => change.type === "add" || change.type === "remove") - ) { - const changes = currentHunk.changes - let startIdx = 0 - let endIdx = changes.length - 1 - - for (let j = 0; j < changes.length; j++) { - if (changes[j].type !== "context") { - startIdx = Math.max(0, j - MAX_CONTEXT_LINES) - break - } - } - - for (let j = changes.length - 1; j >= 0; j--) { - if (changes[j].type !== "context") { - endIdx = Math.min(changes.length - 1, j + MAX_CONTEXT_LINES) - break - } - } - - currentHunk.changes = changes.slice(startIdx, endIdx + 1) - hunks.push(currentHunk) - } - currentHunk = { changes: [] } - continue - } - - if (!currentHunk) { - continue - } - - const content = line.slice(1) - const indentMatch = content.match(/^(\s*)/) - const indent = indentMatch ? indentMatch[0] : "" - const trimmedContent = content.slice(indent.length) - - if (line.startsWith(" ")) { - currentHunk.changes.push({ - type: "context", - content: trimmedContent, - indent, - originalLine: content, - }) - } else if (line.startsWith("+")) { - currentHunk.changes.push({ - type: "add", - content: trimmedContent, - indent, - originalLine: content, - }) - } else if (line.startsWith("-")) { - currentHunk.changes.push({ - type: "remove", - content: trimmedContent, - indent, - originalLine: content, - }) - } else { - const finalContent = trimmedContent ? " " + trimmedContent : " " - currentHunk.changes.push({ - type: "context", - content: finalContent, - indent, - originalLine: content, - }) - } - } - - if ( - currentHunk && - currentHunk.changes.length > 0 && - currentHunk.changes.some((change) => change.type === "add" || change.type === "remove") - ) { - hunks.push(currentHunk) - } - - return { hunks } - } - - getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string { - return `# apply_diff Tool - Generate Precise Code Changes - -Generate a unified diff that can be cleanly applied to modify code files. - -## Step-by-Step Instructions: - -1. Start with file headers: - - First line: "--- {original_file_path}" - - Second line: "+++ {new_file_path}" - -2. For each change section: - - Begin with "@@ ... @@" separator line without line numbers - - Include 2-3 lines of context before and after changes - - Mark removed lines with "-" - - Mark added lines with "+" - - Preserve exact indentation - -3. Group related changes: - - Keep related modifications in the same hunk - - Start new hunks for logically separate changes - - When modifying functions/methods, include the entire block - -## Requirements: - -1. MUST include exact indentation -2. MUST include sufficient context for unique matching -3. MUST group related changes together -4. MUST use proper unified diff format -5. MUST NOT include timestamps in file headers -6. MUST NOT include line numbers in the @@ header - -## Examples: - -✅ Good diff (follows all requirements): -\`\`\`diff ---- src/utils.ts -+++ src/utils.ts -@@ ... @@ - def calculate_total(items): -- total = 0 -- for item in items: -- total += item.price -+ return sum(item.price for item in items) -\`\`\` - -❌ Bad diff (violates requirements #1 and #2): -\`\`\`diff ---- src/utils.ts -+++ src/utils.ts -@@ ... @@ --total = 0 --for item in items: -+return sum(item.price for item in items) -\`\`\` - -Parameters: -- path: (required) File path relative to ${args.cwd} -- diff: (required) Unified diff content in unified format to apply to the file. - -Usage: - -path/to/file.ext - -Your diff here - -` - } - - // Helper function to split a hunk into smaller hunks based on contiguous changes - private splitHunk(hunk: Hunk): Hunk[] { - const result: Hunk[] = [] - let currentHunk: Hunk | null = null - let contextBefore: Change[] = [] - let contextAfter: Change[] = [] - const MAX_CONTEXT_LINES = 3 // Keep 3 lines of context before/after changes - - for (let i = 0; i < hunk.changes.length; i++) { - const change = hunk.changes[i] - - if (change.type === "context") { - if (!currentHunk) { - contextBefore.push(change) - if (contextBefore.length > MAX_CONTEXT_LINES) { - contextBefore.shift() - } - } else { - contextAfter.push(change) - if (contextAfter.length > MAX_CONTEXT_LINES) { - // We've collected enough context after changes, create a new hunk - currentHunk.changes.push(...contextAfter) - result.push(currentHunk) - currentHunk = null - // Keep the last few context lines for the next hunk - contextBefore = contextAfter - contextAfter = [] - } - } - } else { - if (!currentHunk) { - currentHunk = { changes: [...contextBefore] } - contextAfter = [] - } else if (contextAfter.length > 0) { - // Add accumulated context to current hunk - currentHunk.changes.push(...contextAfter) - contextAfter = [] - } - currentHunk.changes.push(change) - } - } - - // Add any remaining changes - if (currentHunk) { - if (contextAfter.length > 0) { - currentHunk.changes.push(...contextAfter) - } - result.push(currentHunk) - } - - return result - } - - async applyDiff( - originalContent: string, - diffContent: string, - startLine?: number, - endLine?: number, - ): Promise { - const parsedDiff = this.parseUnifiedDiff(diffContent) - const originalLines = originalContent.split("\n") - let result = [...originalLines] - - if (!parsedDiff.hunks.length) { - return { - success: false, - error: "No hunks found in diff. Please ensure your diff includes actual changes and follows the unified diff format.", - } - } - - for (const hunk of parsedDiff.hunks) { - const contextStr = prepareSearchString(hunk.changes) - const { - index: matchPosition, - confidence, - strategy, - } = findBestMatch(contextStr, result, 0, this.confidenceThreshold) - - if (confidence < this.confidenceThreshold) { - console.log("Full hunk application failed, trying sub-hunks strategy") - // Try splitting the hunk into smaller hunks - const subHunks = this.splitHunk(hunk) - let subHunkSuccess = true - let subHunkResult = [...result] - - for (const subHunk of subHunks) { - const subContextStr = prepareSearchString(subHunk.changes) - const subSearchResult = findBestMatch(subContextStr, subHunkResult, 0, this.confidenceThreshold) - - if (subSearchResult.confidence >= this.confidenceThreshold) { - const subEditResult = await applyEdit( - subHunk, - subHunkResult, - subSearchResult.index, - subSearchResult.confidence, - this.confidenceThreshold, - ) - if (subEditResult.confidence >= this.confidenceThreshold) { - subHunkResult = subEditResult.result - continue - } - } - subHunkSuccess = false - break - } - - if (subHunkSuccess) { - result = subHunkResult - continue - } - - // If sub-hunks also failed, return the original error - const contextLines = hunk.changes.filter((c) => c.type === "context").length - const totalLines = hunk.changes.length - const contextRatio = contextLines / totalLines - - let errorMsg = `Failed to find a matching location in the file (${Math.floor( - 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, - )}%)\n` - errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n` - - if (contextRatio < 0.2) { - errorMsg += "\nPossible Issues:\n" - errorMsg += "- Not enough context lines to uniquely identify the location\n" - errorMsg += "- Add a few more lines of unchanged code around your changes\n" - } else if (contextRatio > 0.5) { - errorMsg += "\nPossible Issues:\n" - errorMsg += "- Too many context lines may reduce search accuracy\n" - errorMsg += "- Try to keep only 2-3 lines of context before and after changes\n" - } else { - errorMsg += "\nPossible Issues:\n" - errorMsg += "- The diff may be targeting a different version of the file\n" - errorMsg += - "- There may be too many changes in a single hunk, try splitting the changes into multiple hunks\n" - } - - if (startLine && endLine) { - errorMsg += `\nSearch Range: lines ${startLine}-${endLine}\n` - } - - return { success: false, error: errorMsg } - } - - const editResult = await applyEdit(hunk, result, matchPosition, confidence, this.confidenceThreshold) - if (editResult.confidence >= this.confidenceThreshold) { - result = editResult.result - } else { - // Edit failure - likely due to content mismatch - let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor( - editResult.confidence * 100, - )}% confidence)\n\n` - errorMsg += "Debug Info:\n" - errorMsg += "- The location was found but the content didn't match exactly\n" - errorMsg += "- This usually means the file has been modified since the diff was created\n" - errorMsg += "- Or the diff may be targeting a different version of the file\n" - errorMsg += "\nPossible Solutions:\n" - errorMsg += "1. Refresh your view of the file and create a new diff\n" - errorMsg += "2. Double-check that the removed lines (-) match the current file content\n" - errorMsg += "3. Ensure your diff targets the correct version of the file" - - return { success: false, error: errorMsg } - } - } - - return { success: true, content: result.join("\n") } - } -} diff --git a/src/core/diff/strategies/new-unified/search-strategies.ts b/src/core/diff/strategies/new-unified/search-strategies.ts deleted file mode 100644 index 97fd4991c40..00000000000 --- a/src/core/diff/strategies/new-unified/search-strategies.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { compareTwoStrings } from "string-similarity" -import { closest } from "fastest-levenshtein" -import { diff_match_patch } from "diff-match-patch" -import { Change, Hunk } from "./types" - -export type SearchResult = { - index: number - confidence: number - strategy: string -} - -const LARGE_FILE_THRESHOLD = 1000 // lines -const UNIQUE_CONTENT_BOOST = 0.05 -const DEFAULT_OVERLAP_SIZE = 3 // lines of overlap between windows -const MAX_WINDOW_SIZE = 500 // maximum lines in a window - -// Helper function to calculate adaptive confidence threshold based on file size -function getAdaptiveThreshold(contentLength: number, baseThreshold: number): number { - if (contentLength <= LARGE_FILE_THRESHOLD) { - return baseThreshold - } - return Math.max(baseThreshold - 0.07, 0.8) // Reduce threshold for large files but keep minimum at 80% -} - -// Helper function to evaluate content uniqueness -function evaluateContentUniqueness(searchStr: string, content: string[]): number { - const searchLines = searchStr.split("\n") - const uniqueLines = new Set(searchLines) - const contentStr = content.join("\n") - - // Calculate how many search lines are relatively unique in the content - let uniqueCount = 0 - for (const line of uniqueLines) { - const regex = new RegExp(line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g") - const matches = contentStr.match(regex) - if (matches && matches.length <= 2) { - // Line appears at most twice - uniqueCount++ - } - } - - return uniqueCount / uniqueLines.size -} - -// Helper function to prepare search string from context -export function prepareSearchString(changes: Change[]): string { - const lines = changes.filter((c) => c.type === "context" || c.type === "remove").map((c) => c.originalLine) - return lines.join("\n") -} - -// Helper function to evaluate similarity between two texts -export function evaluateSimilarity(original: string, modified: string): number { - return compareTwoStrings(original, modified) -} - -// Helper function to validate using diff-match-patch -export function getDMPSimilarity(original: string, modified: string): number { - const dmp = new diff_match_patch() - const diffs = dmp.diff_main(original, modified) - dmp.diff_cleanupSemantic(diffs) - const patches = dmp.patch_make(original, diffs) - const [expectedText] = dmp.patch_apply(patches, original) - - const similarity = evaluateSimilarity(expectedText, modified) - return similarity -} - -// Helper function to validate edit results using hunk information -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") - - // Calculate similarity between the result and expected text - 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") - - const originalSimilarity = getDMPSimilarity(originalText, result) - if (originalSimilarity > 0.97 && similarity !== 1) { - 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 -} - -// Helper function to validate context lines against original content -function validateContextLines(searchStr: string, content: string, confidenceThreshold: number): number { - // Extract just the context lines from the search string - const contextLines = searchStr.split("\n").filter((line) => !line.startsWith("-")) // Exclude removed lines - - // Compare context lines with content - const similarity = evaluateSimilarity(contextLines.join("\n"), content) - - // Get adaptive threshold based on content size - const threshold = getAdaptiveThreshold(content.split("\n").length, confidenceThreshold) - - // Calculate uniqueness boost - const uniquenessScore = evaluateContentUniqueness(searchStr, content.split("\n")) - const uniquenessBoost = uniquenessScore * UNIQUE_CONTENT_BOOST - - // Adjust confidence based on threshold and uniqueness - return similarity < threshold ? similarity * 0.3 + uniquenessBoost : similarity + uniquenessBoost -} - -// Helper function to create overlapping windows -function createOverlappingWindows( - content: string[], - searchSize: number, - overlapSize: number = DEFAULT_OVERLAP_SIZE, -): { window: string[]; startIndex: number }[] { - const windows: { window: string[]; startIndex: number }[] = [] - - // Ensure minimum window size is at least searchSize - const effectiveWindowSize = Math.max(searchSize, Math.min(searchSize * 2, MAX_WINDOW_SIZE)) - - // Ensure overlap size doesn't exceed window size - const effectiveOverlapSize = Math.min(overlapSize, effectiveWindowSize - 1) - - // Calculate step size, ensure it's at least 1 - const stepSize = Math.max(1, effectiveWindowSize - effectiveOverlapSize) - - for (let i = 0; i < content.length; i += stepSize) { - const windowContent = content.slice(i, i + effectiveWindowSize) - if (windowContent.length >= searchSize) { - windows.push({ window: windowContent, startIndex: i }) - } - } - - return windows -} - -// Helper function to combine overlapping matches -function combineOverlappingMatches( - matches: (SearchResult & { windowIndex: number })[], - overlapSize: number = DEFAULT_OVERLAP_SIZE, -): SearchResult[] { - if (matches.length === 0) { - return [] - } - - // Sort matches by confidence - matches.sort((a, b) => b.confidence - a.confidence) - - const combinedMatches: SearchResult[] = [] - const usedIndices = new Set() - - for (const match of matches) { - if (usedIndices.has(match.windowIndex)) { - continue - } - - // Find overlapping matches - const overlapping = matches.filter( - (m) => - Math.abs(m.windowIndex - match.windowIndex) === 1 && - Math.abs(m.index - match.index) <= overlapSize && - !usedIndices.has(m.windowIndex), - ) - - if (overlapping.length > 0) { - // Boost confidence if we find same match in overlapping windows - const avgConfidence = - (match.confidence + overlapping.reduce((sum, m) => sum + m.confidence, 0)) / (overlapping.length + 1) - const boost = Math.min(0.05 * overlapping.length, 0.1) // Max 10% boost - - combinedMatches.push({ - index: match.index, - confidence: Math.min(1, avgConfidence + boost), - strategy: `${match.strategy}-overlapping`, - }) - - usedIndices.add(match.windowIndex) - overlapping.forEach((m) => usedIndices.add(m.windowIndex)) - } else { - combinedMatches.push({ - index: match.index, - confidence: match.confidence, - strategy: match.strategy, - }) - usedIndices.add(match.windowIndex) - } - } - - return combinedMatches -} - -export function findExactMatch( - searchStr: string, - content: string[], - startIndex: number = 0, - confidenceThreshold: number = 0.97, -): SearchResult { - const searchLines = searchStr.split("\n") - const windows = createOverlappingWindows(content.slice(startIndex), searchLines.length) - const matches: (SearchResult & { windowIndex: number })[] = [] - - windows.forEach((windowData, windowIndex) => { - const windowStr = windowData.window.join("\n") - const exactMatch = windowStr.indexOf(searchStr) - - if (exactMatch !== -1) { - const matchedContent = windowData.window - .slice( - windowStr.slice(0, exactMatch).split("\n").length - 1, - windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length, - ) - .join("\n") - - const similarity = getDMPSimilarity(searchStr, matchedContent) - const contextSimilarity = validateContextLines(searchStr, matchedContent, confidenceThreshold) - const confidence = Math.min(similarity, contextSimilarity) - - matches.push({ - index: startIndex + windowData.startIndex + windowStr.slice(0, exactMatch).split("\n").length - 1, - confidence, - strategy: "exact", - windowIndex, - }) - } - }) - - const combinedMatches = combineOverlappingMatches(matches) - return combinedMatches.length > 0 ? combinedMatches[0] : { index: -1, confidence: 0, strategy: "exact" } -} - -// String similarity strategy -export function findSimilarityMatch( - searchStr: string, - content: string[], - startIndex: number = 0, - confidenceThreshold: number = 0.97, -): SearchResult { - const searchLines = searchStr.split("\n") - let bestScore = 0 - let bestIndex = -1 - - for (let i = startIndex; i < content.length - searchLines.length + 1; i++) { - const windowStr = content.slice(i, i + searchLines.length).join("\n") - const score = compareTwoStrings(searchStr, windowStr) - if (score > bestScore && score >= confidenceThreshold) { - const similarity = getDMPSimilarity(searchStr, windowStr) - const contextSimilarity = validateContextLines(searchStr, windowStr, confidenceThreshold) - const adjustedScore = Math.min(similarity, contextSimilarity) * score - - if (adjustedScore > bestScore) { - bestScore = adjustedScore - bestIndex = i - } - } - } - - return { - index: bestIndex, - confidence: bestIndex !== -1 ? bestScore : 0, - strategy: "similarity", - } -} - -// Levenshtein strategy -export function findLevenshteinMatch( - searchStr: string, - content: string[], - startIndex: number = 0, - confidenceThreshold: number = 0.97, -): SearchResult { - const searchLines = searchStr.split("\n") - const candidates = [] - - for (let i = startIndex; i < content.length - searchLines.length + 1; i++) { - candidates.push(content.slice(i, i + searchLines.length).join("\n")) - } - - if (candidates.length > 0) { - const closestMatch = closest(searchStr, candidates) - const index = startIndex + candidates.indexOf(closestMatch) - const similarity = getDMPSimilarity(searchStr, closestMatch) - const contextSimilarity = validateContextLines(searchStr, closestMatch, confidenceThreshold) - const confidence = Math.min(similarity, contextSimilarity) - return { - index: confidence === 0 ? -1 : index, - confidence: index !== -1 ? confidence : 0, - strategy: "levenshtein", - } - } - - return { index: -1, confidence: 0, strategy: "levenshtein" } -} - -// Helper function to identify anchor lines -function identifyAnchors(searchStr: string): { first: string | null; last: string | null } { - const searchLines = searchStr.split("\n") - let first: string | null = null - let last: string | null = null - - // Find the first non-empty line - for (const line of searchLines) { - if (line.trim()) { - first = line - break - } - } - - // Find the last non-empty line - for (let i = searchLines.length - 1; i >= 0; i--) { - if (searchLines[i].trim()) { - last = searchLines[i] - break - } - } - - return { first, last } -} - -// Anchor-based search strategy -export function findAnchorMatch( - searchStr: string, - content: string[], - startIndex: number = 0, - confidenceThreshold: number = 0.97, -): SearchResult { - const searchLines = searchStr.split("\n") - const { first, last } = identifyAnchors(searchStr) - - if (!first || !last) { - return { index: -1, confidence: 0, strategy: "anchor" } - } - - let firstIndex = -1 - let lastIndex = -1 - - // Check if the first anchor is unique - let firstOccurrences = 0 - for (const contentLine of content) { - if (contentLine === first) { - firstOccurrences++ - } - } - - if (firstOccurrences !== 1) { - return { index: -1, confidence: 0, strategy: "anchor" } - } - - // Find the first anchor - for (let i = startIndex; i < content.length; i++) { - if (content[i] === first) { - firstIndex = i - break - } - } - - // Find the last anchor - for (let i = content.length - 1; i >= startIndex; i--) { - if (content[i] === last) { - lastIndex = i - break - } - } - - if (firstIndex === -1 || lastIndex === -1 || lastIndex <= firstIndex) { - return { index: -1, confidence: 0, strategy: "anchor" } - } - - // Validate the context - const expectedContext = searchLines.slice(searchLines.indexOf(first) + 1, searchLines.indexOf(last)).join("\n") - const actualContext = content.slice(firstIndex + 1, lastIndex).join("\n") - const contextSimilarity = evaluateSimilarity(expectedContext, actualContext) - - if (contextSimilarity < getAdaptiveThreshold(content.length, confidenceThreshold)) { - return { index: -1, confidence: 0, strategy: "anchor" } - } - - const confidence = 1 - - return { - index: firstIndex, - confidence: confidence, - strategy: "anchor", - } -} - -// Main search function that tries all strategies -export function findBestMatch( - searchStr: string, - content: string[], - startIndex: number = 0, - confidenceThreshold: number = 0.97, -): SearchResult { - const strategies = [findExactMatch, findAnchorMatch, findSimilarityMatch, findLevenshteinMatch] - - let bestResult: SearchResult = { index: -1, confidence: 0, strategy: "none" } - - for (const strategy of strategies) { - const result = strategy(searchStr, content, startIndex, confidenceThreshold) - if (result.confidence > bestResult.confidence) { - bestResult = result - } - } - - return bestResult -} diff --git a/src/core/diff/strategies/new-unified/types.ts b/src/core/diff/strategies/new-unified/types.ts deleted file mode 100644 index 0e243d35cc2..00000000000 --- a/src/core/diff/strategies/new-unified/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type Change = { - type: "context" | "add" | "remove" - content: string - indent: string - originalLine?: string -} - -export type Hunk = { - changes: Change[] -} - -export type Diff = { - hunks: Hunk[] -} - -export type EditResult = { - confidence: number - result: string[] - strategy: string -} diff --git a/src/core/diff/strategies/unified.ts b/src/core/diff/strategies/unified.ts deleted file mode 100644 index f4d6ead6aab..00000000000 --- a/src/core/diff/strategies/unified.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { applyPatch } from "diff" -import { DiffStrategy, DiffResult } from "../types" - -export class UnifiedDiffStrategy implements DiffStrategy { - getName(): string { - return "Unified" - } - 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 ${args.cwd}) -- diff: (required) The diff content in unified format to apply to the file. - -Format Requirements: - -1. Header (REQUIRED): - \`\`\` - --- path/to/original/file - +++ path/to/modified/file - \`\`\` - - Must include both lines exactly as shown - - Use actual file paths - - NO timestamps after paths - -2. Hunks: - \`\`\` - @@ -lineStart,lineCount +lineStart,lineCount @@ - -removed line - +added line - \`\`\` - - Each hunk starts with @@ showing line numbers for changes - - Format: @@ -originalStart,originalCount +newStart,newCount @@ - - Use - for removed/changed lines - - Use + for new/modified lines - - Indentation must match exactly - -Complete Example: - -Original file (with line numbers): -\`\`\` -1 | import { Logger } from '../logger'; -2 | -3 | function calculateTotal(items: number[]): number { -4 | return items.reduce((sum, item) => { -5 | return sum + item; -6 | }, 0); -7 | } -8 | -9 | export { calculateTotal }; -\`\`\` - -After applying the diff, the file would look like: -\`\`\` -1 | import { Logger } from '../logger'; -2 | -3 | function calculateTotal(items: number[]): number { -4 | const total = items.reduce((sum, item) => { -5 | return sum + item * 1.1; // Add 10% markup -6 | }, 0); -7 | return Math.round(total * 100) / 100; // Round to 2 decimal places -8 | } -9 | -10 | export { calculateTotal }; -\`\`\` - -Diff to modify the file: -\`\`\` ---- src/utils/helper.ts -+++ src/utils/helper.ts -@@ -1,9 +1,10 @@ - import { Logger } from '../logger'; - - function calculateTotal(items: number[]): number { -- return items.reduce((sum, item) => { -- return sum + item; -+ const total = items.reduce((sum, item) => { -+ return sum + item * 1.1; // Add 10% markup - }, 0); -+ return Math.round(total * 100) / 100; // Round to 2 decimal places - } - - export { calculateTotal }; -\`\`\` - -Common Pitfalls: -1. Missing or incorrect header lines -2. Incorrect line numbers in @@ lines -3. Wrong indentation in changed lines -4. Incomplete context (missing lines that need changing) -5. Not marking all modified lines with - and + - -Best Practices: -1. Replace entire code blocks: - - Remove complete old version with - lines - - Add complete new version with + lines - - Include correct line numbers -2. Moving code requires two hunks: - - First hunk: Remove from old location - - Second hunk: Add to new location -3. One hunk per logical change -4. Verify line numbers match the line numbers you have in the file - -Usage: - -File path here - -Your diff here - -` - } - - async applyDiff(originalContent: string, diffContent: string): Promise { - 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, - }, - } - } - } -}