diff --git a/crates/maestro-tui/src/structured_pane.rs b/crates/maestro-tui/src/structured_pane.rs index 26b7b28..ed14064 100644 --- a/crates/maestro-tui/src/structured_pane.rs +++ b/crates/maestro-tui/src/structured_pane.rs @@ -678,10 +678,11 @@ fn render_diff_ops(ops: &[DiffOp<'_>]) -> Vec { for op in run.iter().take(keep_head) { push_diff_op(&mut rendered, *op); } - rendered.push(format!( - "… {} unchanged lines …", - run.len() - keep_head - keep_tail - )); + let collapsed = &run[keep_head..run.len() - keep_tail]; + rendered.push(format!("… {} unchanged lines …", collapsed.len())); + if collapsed.iter().any(|op| !op.line.terminated) { + rendered.push("\\ No newline at end of file".into()); + } for op in run.iter().skip(run.len() - keep_tail) { push_diff_op(&mut rendered, *op); } @@ -1055,6 +1056,35 @@ mod tests { " line11", " line12", "… 7 unchanged lines …", + "\\ No newline at end of file", + ] + ); + } + + #[test] + fn structured_diff_preserves_eof_marker_through_trailing_collapse() { + let trailing = (0..10).map(|index| format!("t{index}")).collect::>(); + let old = ["OLD".into()] + .into_iter() + .chain(trailing.iter().cloned()) + .collect::>() + .join("\n"); + let new = ["NEW".into()] + .into_iter() + .chain(trailing) + .collect::>() + .join("\n"); + + assert_eq!( + render_diff(Some(&old), &new, None), + [ + "- OLD", + "+ NEW", + " t0", + " t1", + " t2", + "… 7 unchanged lines …", + "\\ No newline at end of file", ] ); } diff --git a/web/src/structuredDiff.test.ts b/web/src/structuredDiff.test.ts index 70c2ced..837f671 100644 --- a/web/src/structuredDiff.test.ts +++ b/web/src/structuredDiff.test.ts @@ -114,6 +114,22 @@ describe("computeDiffLines", () => { expect(lines.filter((l) => l.kind === "hunk")).toHaveLength(2); }); + it("preserves the EOF marker when a long unterminated trailing run collapses", () => { + const trailing = Array.from({ length: 10 }, (_, i) => `t${i}`); + const oldText = ["OLD", ...trailing].join("\n"); + const newText = ["NEW", ...trailing].join("\n"); + + expect(texts(computeDiffLines(oldText, newText))).toEqual([ + ["del", "OLD"], + ["add", "NEW"], + ["ctx", "t0"], + ["ctx", "t1"], + ["ctx", "t2"], + ["hunk", "⋯ 7 unchanged lines ⋯"], + ["eof", "\\ No newline at end of file"], + ]); + }); + it("produces one hunk for the long middle run, none for the short edges", () => { // Edge runs (2 lines) fit within their one-sided 3-line context budget; // only the interior run (10 lines, budgeted 3+3=6) needs to collapse. diff --git a/web/src/structuredDiff.ts b/web/src/structuredDiff.ts index c7eb8b0..5654114 100644 --- a/web/src/structuredDiff.ts +++ b/web/src/structuredDiff.ts @@ -130,10 +130,14 @@ function hunk(ops: Op[]): DiffLine[] { run.forEach(push); } else { if (keepHead) run.slice(0, keepHead).forEach(push); + const collapsed = run.slice(keepHead, run.length - keepTail); out.push({ kind: "hunk", - text: `⋯ ${run.length - keepHead - keepTail} unchanged lines ⋯`, + text: `⋯ ${collapsed.length} unchanged lines ⋯`, }); + if (collapsed.some((op) => !op.line.terminated)) { + out.push({ kind: "eof", text: "\\ No newline at end of file" }); + } if (keepTail) run.slice(run.length - keepTail).forEach(push); } i = j;