( filePath: string, edits: FileEdit[], dryRun: boolean = false )
| 192 | } |
| 193 | |
| 194 | export async function applyFileEdits( |
| 195 | filePath: string, |
| 196 | edits: FileEdit[], |
| 197 | dryRun: boolean = false |
| 198 | ): Promise<string> { |
| 199 | // Read file content and normalize line endings |
| 200 | const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); |
| 201 | |
| 202 | // Apply edits sequentially |
| 203 | let modifiedContent = content; |
| 204 | for (const edit of edits) { |
| 205 | const normalizedOld = normalizeLineEndings(edit.oldText); |
| 206 | const normalizedNew = normalizeLineEndings(edit.newText); |
| 207 | |
| 208 | // If exact match exists, use it |
| 209 | if (modifiedContent.includes(normalizedOld)) { |
| 210 | modifiedContent = modifiedContent.replace(normalizedOld, () => normalizedNew); |
| 211 | continue; |
| 212 | } |
| 213 | |
| 214 | // Otherwise, try line-by-line matching with flexibility for whitespace |
| 215 | const oldLines = normalizedOld.split('\n'); |
| 216 | const contentLines = modifiedContent.split('\n'); |
| 217 | let matchFound = false; |
| 218 | |
| 219 | for (let i = 0; i <= contentLines.length - oldLines.length; i++) { |
| 220 | const potentialMatch = contentLines.slice(i, i + oldLines.length); |
| 221 | |
| 222 | // Compare lines with normalized whitespace |
| 223 | const isMatch = oldLines.every((oldLine, j) => { |
| 224 | const contentLine = potentialMatch[j]; |
| 225 | return oldLine.trim() === contentLine.trim(); |
| 226 | }); |
| 227 | |
| 228 | if (isMatch) { |
| 229 | // Preserve original indentation of first line |
| 230 | const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; |
| 231 | const newLines = normalizedNew.split('\n').map((line, j) => { |
| 232 | if (j === 0) return originalIndent + line.trimStart(); |
| 233 | // For subsequent lines, try to preserve relative indentation |
| 234 | const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; |
| 235 | const newIndent = line.match(/^\s*/)?.[0] || ''; |
| 236 | if (oldIndent && newIndent) { |
| 237 | const relativeIndent = newIndent.length - oldIndent.length; |
| 238 | return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); |
| 239 | } |
| 240 | return line; |
| 241 | }); |
| 242 | |
| 243 | contentLines.splice(i, oldLines.length, ...newLines); |
| 244 | modifiedContent = contentLines.join('\n'); |
| 245 | matchFound = true; |
| 246 | break; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | if (!matchFound) { |
| 251 | throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); |
no test coverage detected