Create a structure for different diff strategies, starting with unified (#55)

This commit is contained in:
Matt Rubens
2024-12-09 11:04:40 -05:00
committed by GitHub
parent 39b51fa78f
commit da31a23499
13 changed files with 422 additions and 129 deletions

View File

@@ -0,0 +1,214 @@
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', () => {
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 = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(expected)
})
it('should successfully apply a diff adding a new method', () => {
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 = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(expected)
})
it('should successfully apply a diff modifying imports', () => {
const originalContent = `import { useState } from 'react';
import { Button } from './components';
function App() {
const [count, setCount] = useState(0);
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
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 <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
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 <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const result = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(expected)
})
it('should successfully apply a diff with multiple hunks', () => {
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 = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(expected)
})
it('should handle empty original content', () => {
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 = strategy.applyDiff(originalContent, diffContent)
expect(result).toBe(expected)
})
})
})

View File

@@ -0,0 +1,114 @@
import { applyPatch } from "diff"
import { DiffStrategy } from "../types"
export class UnifiedDiffStrategy implements DiffStrategy {
getToolDescription(cwd: 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})
- 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:
<apply_diff>
<path>File path here</path>
<diff>
Your diff here
</diff>
</apply_diff>`
}
applyDiff(originalContent: string, diffContent: string): string | false {
return applyPatch(originalContent, diffContent) as string | false
}
}