Spaces:
Configuration error
Configuration error
| import fs from "node:fs/promises"; | |
| type UpdateFileChunk = { | |
| changeContext?: string; | |
| oldLines: string[]; | |
| newLines: string[]; | |
| isEndOfFile: boolean; | |
| }; | |
| export async function applyUpdateHunk( | |
| filePath: string, | |
| chunks: UpdateFileChunk[], | |
| ): Promise<string> { | |
| const originalContents = await fs.readFile(filePath, "utf8").catch((err) => { | |
| throw new Error(`Failed to read file to update ${filePath}: ${err}`); | |
| }); | |
| const originalLines = originalContents.split("\n"); | |
| if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { | |
| originalLines.pop(); | |
| } | |
| const replacements = computeReplacements(originalLines, filePath, chunks); | |
| let newLines = applyReplacements(originalLines, replacements); | |
| if (newLines.length === 0 || newLines[newLines.length - 1] !== "") { | |
| newLines = [...newLines, ""]; | |
| } | |
| return newLines.join("\n"); | |
| } | |
| function computeReplacements( | |
| originalLines: string[], | |
| filePath: string, | |
| chunks: UpdateFileChunk[], | |
| ): Array<[number, number, string[]]> { | |
| const replacements: Array<[number, number, string[]]> = []; | |
| let lineIndex = 0; | |
| for (const chunk of chunks) { | |
| if (chunk.changeContext) { | |
| const ctxIndex = seekSequence(originalLines, [chunk.changeContext], lineIndex, false); | |
| if (ctxIndex === null) { | |
| throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}`); | |
| } | |
| lineIndex = ctxIndex + 1; | |
| } | |
| if (chunk.oldLines.length === 0) { | |
| const insertionIndex = | |
| originalLines.length > 0 && originalLines[originalLines.length - 1] === "" | |
| ? originalLines.length - 1 | |
| : originalLines.length; | |
| replacements.push([insertionIndex, 0, chunk.newLines]); | |
| continue; | |
| } | |
| let pattern = chunk.oldLines; | |
| let newSlice = chunk.newLines; | |
| let found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile); | |
| if (found === null && pattern[pattern.length - 1] === "") { | |
| pattern = pattern.slice(0, -1); | |
| if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { | |
| newSlice = newSlice.slice(0, -1); | |
| } | |
| found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile); | |
| } | |
| if (found === null) { | |
| throw new Error( | |
| `Failed to find expected lines in ${filePath}:\n${chunk.oldLines.join("\n")}`, | |
| ); | |
| } | |
| replacements.push([found, pattern.length, newSlice]); | |
| lineIndex = found + pattern.length; | |
| } | |
| replacements.sort((a, b) => a[0] - b[0]); | |
| return replacements; | |
| } | |
| function applyReplacements( | |
| lines: string[], | |
| replacements: Array<[number, number, string[]]>, | |
| ): string[] { | |
| const result = [...lines]; | |
| for (const [startIndex, oldLen, newLines] of [...replacements].reverse()) { | |
| for (let i = 0; i < oldLen; i += 1) { | |
| if (startIndex < result.length) { | |
| result.splice(startIndex, 1); | |
| } | |
| } | |
| for (let i = 0; i < newLines.length; i += 1) { | |
| result.splice(startIndex + i, 0, newLines[i]); | |
| } | |
| } | |
| return result; | |
| } | |
| function seekSequence( | |
| lines: string[], | |
| pattern: string[], | |
| start: number, | |
| eof: boolean, | |
| ): number | null { | |
| if (pattern.length === 0) return start; | |
| if (pattern.length > lines.length) return null; | |
| const maxStart = lines.length - pattern.length; | |
| const searchStart = eof && lines.length >= pattern.length ? maxStart : start; | |
| if (searchStart > maxStart) return null; | |
| for (let i = searchStart; i <= maxStart; i += 1) { | |
| if (linesMatch(lines, pattern, i, (value) => value)) return i; | |
| } | |
| for (let i = searchStart; i <= maxStart; i += 1) { | |
| if (linesMatch(lines, pattern, i, (value) => value.trimEnd())) return i; | |
| } | |
| for (let i = searchStart; i <= maxStart; i += 1) { | |
| if (linesMatch(lines, pattern, i, (value) => value.trim())) return i; | |
| } | |
| for (let i = searchStart; i <= maxStart; i += 1) { | |
| if (linesMatch(lines, pattern, i, (value) => normalizePunctuation(value.trim()))) { | |
| return i; | |
| } | |
| } | |
| return null; | |
| } | |
| function linesMatch( | |
| lines: string[], | |
| pattern: string[], | |
| start: number, | |
| normalize: (value: string) => string, | |
| ): boolean { | |
| for (let idx = 0; idx < pattern.length; idx += 1) { | |
| if (normalize(lines[start + idx]) !== normalize(pattern[idx])) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function normalizePunctuation(value: string): string { | |
| return Array.from(value) | |
| .map((char) => { | |
| switch (char) { | |
| case "\u2010": | |
| case "\u2011": | |
| case "\u2012": | |
| case "\u2013": | |
| case "\u2014": | |
| case "\u2015": | |
| case "\u2212": | |
| return "-"; | |
| case "\u2018": | |
| case "\u2019": | |
| case "\u201A": | |
| case "\u201B": | |
| return "'"; | |
| case "\u201C": | |
| case "\u201D": | |
| case "\u201E": | |
| case "\u201F": | |
| return '"'; | |
| case "\u00A0": | |
| case "\u2002": | |
| case "\u2003": | |
| case "\u2004": | |
| case "\u2005": | |
| case "\u2006": | |
| case "\u2007": | |
| case "\u2008": | |
| case "\u2009": | |
| case "\u200A": | |
| case "\u202F": | |
| case "\u205F": | |
| case "\u3000": | |
| return " "; | |
| default: | |
| return char; | |
| } | |
| }) | |
| .join(""); | |
| } | |