Skip to content

Commit 205bb54

Browse files
committed
Fix indentation issue
1 parent adf747d commit 205bb54

File tree

2 files changed

+241
-3
lines changed

2 files changed

+241
-3
lines changed

src/core/diff/strategies/__tests__/search-replace.test.ts

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,221 @@ Invalid diff format`
268268
const result = strategy.applyDiff(originalContent, diffContent)
269269
expect(result).toBe(" modified\n still indented\n end\n")
270270
})
271+
272+
it('should handle complex refactoring with multiple functions', () => {
273+
const originalContent = `export async function extractTextFromFile(filePath: string): Promise<string> {
274+
try {
275+
await fs.access(filePath)
276+
} catch (error) {
277+
throw new Error(\`File not found: \${filePath}\`)
278+
}
279+
const fileExtension = path.extname(filePath).toLowerCase()
280+
switch (fileExtension) {
281+
case ".pdf":
282+
return extractTextFromPDF(filePath)
283+
case ".docx":
284+
return extractTextFromDOCX(filePath)
285+
case ".ipynb":
286+
return extractTextFromIPYNB(filePath)
287+
default:
288+
const isBinary = await isBinaryFile(filePath).catch(() => false)
289+
if (!isBinary) {
290+
return addLineNumbers(await fs.readFile(filePath, "utf8"))
291+
} else {
292+
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
293+
}
294+
}
295+
}
296+
297+
export function addLineNumbers(content: string): string {
298+
const lines = content.split('\\n')
299+
const maxLineNumberWidth = String(lines.length).length
300+
return lines
301+
.map((line, index) => {
302+
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
303+
return \`\${lineNumber} | \${line}\`
304+
}).join('\\n')
305+
}`
306+
307+
const diffContent = `test.ts
308+
<<<<<<< SEARCH
309+
export async function extractTextFromFile(filePath: string): Promise<string> {
310+
try {
311+
await fs.access(filePath)
312+
} catch (error) {
313+
throw new Error(\`File not found: \${filePath}\`)
314+
}
315+
const fileExtension = path.extname(filePath).toLowerCase()
316+
switch (fileExtension) {
317+
case ".pdf":
318+
return extractTextFromPDF(filePath)
319+
case ".docx":
320+
return extractTextFromDOCX(filePath)
321+
case ".ipynb":
322+
return extractTextFromIPYNB(filePath)
323+
default:
324+
const isBinary = await isBinaryFile(filePath).catch(() => false)
325+
if (!isBinary) {
326+
return addLineNumbers(await fs.readFile(filePath, "utf8"))
327+
} else {
328+
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
329+
}
330+
}
331+
}
332+
333+
export function addLineNumbers(content: string): string {
334+
const lines = content.split('\\n')
335+
const maxLineNumberWidth = String(lines.length).length
336+
return lines
337+
.map((line, index) => {
338+
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
339+
return \`\${lineNumber} | \${line}\`
340+
}).join('\\n')
341+
}
342+
=======
343+
function extractLineRange(content: string, startLine?: number, endLine?: number): string {
344+
const lines = content.split('\\n')
345+
const start = startLine ? Math.max(1, startLine) : 1
346+
const end = endLine ? Math.min(lines.length, endLine) : lines.length
347+
348+
if (start > end || start > lines.length) {
349+
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
350+
}
351+
352+
return lines.slice(start - 1, end).join('\\n')
353+
}
354+
355+
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
356+
try {
357+
await fs.access(filePath)
358+
} catch (error) {
359+
throw new Error(\`File not found: \${filePath}\`)
360+
}
361+
const fileExtension = path.extname(filePath).toLowerCase()
362+
let content: string
363+
364+
switch (fileExtension) {
365+
case ".pdf": {
366+
const dataBuffer = await fs.readFile(filePath)
367+
const data = await pdf(dataBuffer)
368+
content = extractLineRange(data.text, startLine, endLine)
369+
break
370+
}
371+
case ".docx": {
372+
const result = await mammoth.extractRawText({ path: filePath })
373+
content = extractLineRange(result.value, startLine, endLine)
374+
break
375+
}
376+
case ".ipynb": {
377+
const data = await fs.readFile(filePath, "utf8")
378+
const notebook = JSON.parse(data)
379+
let extractedText = ""
380+
381+
for (const cell of notebook.cells) {
382+
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
383+
extractedText += cell.source.join("\\n") + "\\n"
384+
}
385+
}
386+
content = extractLineRange(extractedText, startLine, endLine)
387+
break
388+
}
389+
default: {
390+
const isBinary = await isBinaryFile(filePath).catch(() => false)
391+
if (!isBinary) {
392+
const fileContent = await fs.readFile(filePath, "utf8")
393+
content = extractLineRange(fileContent, startLine, endLine)
394+
} else {
395+
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
396+
}
397+
}
398+
}
399+
400+
return addLineNumbers(content, startLine)
401+
}
402+
403+
export function addLineNumbers(content: string, startLine: number = 1): string {
404+
const lines = content.split('\\n')
405+
const maxLineNumberWidth = String(startLine + lines.length - 1).length
406+
return lines
407+
.map((line, index) => {
408+
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
409+
return \`\${lineNumber} | \${line}\`
410+
}).join('\\n')
411+
}
412+
>>>>>>> REPLACE`
413+
414+
const result = strategy.applyDiff(originalContent, diffContent)
415+
const expected = `function extractLineRange(content: string, startLine?: number, endLine?: number): string {
416+
const lines = content.split('\\n')
417+
const start = startLine ? Math.max(1, startLine) : 1
418+
const end = endLine ? Math.min(lines.length, endLine) : lines.length
419+
420+
if (start > end || start > lines.length) {
421+
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
422+
}
423+
424+
return lines.slice(start - 1, end).join('\\n')
425+
}
426+
427+
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
428+
try {
429+
await fs.access(filePath)
430+
} catch (error) {
431+
throw new Error(\`File not found: \${filePath}\`)
432+
}
433+
const fileExtension = path.extname(filePath).toLowerCase()
434+
let content: string
435+
436+
switch (fileExtension) {
437+
case ".pdf": {
438+
const dataBuffer = await fs.readFile(filePath)
439+
const data = await pdf(dataBuffer)
440+
content = extractLineRange(data.text, startLine, endLine)
441+
break
442+
}
443+
case ".docx": {
444+
const result = await mammoth.extractRawText({ path: filePath })
445+
content = extractLineRange(result.value, startLine, endLine)
446+
break
447+
}
448+
case ".ipynb": {
449+
const data = await fs.readFile(filePath, "utf8")
450+
const notebook = JSON.parse(data)
451+
let extractedText = ""
452+
453+
for (const cell of notebook.cells) {
454+
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
455+
extractedText += cell.source.join("\\n") + "\\n"
456+
}
457+
}
458+
content = extractLineRange(extractedText, startLine, endLine)
459+
break
460+
}
461+
default: {
462+
const isBinary = await isBinaryFile(filePath).catch(() => false)
463+
if (!isBinary) {
464+
const fileContent = await fs.readFile(filePath, "utf8")
465+
content = extractLineRange(fileContent, startLine, endLine)
466+
} else {
467+
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
468+
}
469+
}
470+
}
471+
472+
return addLineNumbers(content, startLine)
473+
}
474+
475+
export function addLineNumbers(content: string, startLine: number = 1): string {
476+
const lines = content.split('\\n')
477+
const maxLineNumberWidth = String(startLine + lines.length - 1).length
478+
return lines
479+
.map((line, index) => {
480+
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
481+
return \`\${lineNumber} | \${line}\`
482+
}).join('\\n')
483+
}`
484+
expect(result).toBe(expected)
485+
})
271486
})
272487

273488
describe('getToolDescription', () => {

src/core/diff/strategies/search-replace.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,32 @@ Your search/replace content here
133133
if (currentIndent.length === searchIndent.length) {
134134
return originalIndent + line.trim();
135135
} else {
136-
// Calculate additional indentation needed
137-
const additionalIndent = currentIndent.slice(searchIndent.length);
138-
return originalIndent + additionalIndent + line.trim();
136+
// Get the corresponding search line's indentation
137+
const searchLineIndex = Math.min(i, searchLines.length - 1);
138+
const searchLineIndent = searchIndents[searchLineIndex];
139+
140+
// Get the corresponding original line's indentation
141+
const originalLineIndex = Math.min(i, originalIndents.length - 1);
142+
const originalLineIndent = originalIndents[originalLineIndex];
143+
144+
// If this line has the same indentation as its corresponding search line,
145+
// use the original indentation
146+
if (currentIndent === searchLineIndent) {
147+
return originalLineIndent + line.trim();
148+
}
149+
150+
// Otherwise, preserve the original indentation structure
151+
const indentChar = originalLineIndent.charAt(0) || '\t';
152+
const indentLevel = Math.floor(originalLineIndent.length / indentChar.length);
153+
154+
// Calculate the relative indentation from the search line
155+
const searchLevel = Math.floor(searchLineIndent.length / indentChar.length);
156+
const currentLevel = Math.floor(currentIndent.length / indentChar.length);
157+
const relativeLevel = currentLevel - searchLevel;
158+
159+
// Apply the relative indentation to the original level
160+
const targetLevel = Math.max(0, indentLevel + relativeLevel);
161+
return indentChar.repeat(targetLevel) + line.trim();
139162
}
140163
});
141164

0 commit comments

Comments
 (0)