fix(core): keep trailing blank lines a patch does not touch

derive() split the file into lines, dropped the terminating empty element, and
then restored a final newline only when the last line was not already empty.
A file ending in a blank line therefore lost that blank line on every update,
and a hunk adding an empty line at the end of a file had it silently dropped.
Always restoring the terminator preserves the file's own trailing newlines
while still adding one to a file that lacked it.
This commit is contained in:
Kit Langton
2026-11-06 17:18:07 -08:00
Unverified
parent b2b5da600e
commit e1d210e20c
2 changed files with 13 additions and 1 deletions
+9
View File
@@ -248,6 +248,15 @@ describe("Patch", () => {
).toBe("line 1\nline 2\nadded 1\nadded 2\n")
})
test("keeps trailing blank lines the patch did not touch", () => {
expect(
Patch.derive("update.txt", [{ oldLines: ["line 2"], newLines: ["LINE 2"] }], "line 1\nline 2\n\n").content,
).toBe("line 1\nLINE 2\n\n")
expect(
Patch.derive("update.txt", [{ oldLines: ["line 1"], newLines: ["line 1", ""] }], "line 1\n").content,
).toBe("line 1\n\n")
})
test.each(["", "original\n"])("preserves equal-offset insertion order and frozen chunks for %j", (original) => {
const chunks = Object.freeze([
Object.freeze({ oldLines: Object.freeze([]), newLines: Object.freeze(["first"]) }),
+4 -1
View File
@@ -131,7 +131,10 @@ export function derive(path: string, chunks: ReadonlyArray<UpdateFileChunk>, ori
if (lines.at(-1) === "") lines.pop()
const replacements = computeReplacements(lines, path, chunks)
for (const [start, remove, insert] of replacements.reverse()) lines.splice(start, remove, ...insert)
if (lines.at(-1) !== "") lines.push("")
// The split above lost the final newline as an empty element, and the file must end with one. Always
// restoring it keeps any other trailing blank lines the patch did not touch, which is why this is not
// conditional on the array's last line.
lines.push("")
const next = Bom.split(lines.join("\n"))
return { content: next.text, bom: source.bom || next.bom }
}