diff --git a/crates/model/src/edit.rs b/crates/model/src/edit.rs index 33c2a8bf..d635c052 100644 --- a/crates/model/src/edit.rs +++ b/crates/model/src/edit.rs @@ -418,6 +418,8 @@ fn pos_of_char(body: &TextBody, mut n: usize) -> Pos { /// keeps its run and paragraph formatting and new text takes the formatting where it lands. Text /// that shares nothing with the old text replaces it as plain text. pub fn replace_text(body: &mut TextBody, text: &str) { + // Normalise line endings first so the common prefix/suffix cannot split a `\r\n` pair. + let text = &text.replace("\r\n", "\n").replace('\r', "\n"); let old: Vec = body.text().chars().collect(); let new: Vec = text.chars().collect(); if old == new { @@ -723,6 +725,22 @@ mod tests { b } + #[test] + fn replace_text_crlf_matches_from_text() { + // The kept suffix must not start inside a `\r\n` pair. + let mut b = TextBody::from_text("x\ny"); + replace_text(&mut b, "z\r\ny"); + assert_eq!(b.text(), "z\ny"); + // CRLF in the kept prefix, and the same text with CRLF changes nothing. + let before = notes(); + let mut b = before.clone(); + replace_text(&mut b, "Overview\r\nFirst point\r\nSecond point"); + assert_eq!(b, before); + replace_text(&mut b, "Overview\r\nFirst point\r\nSecond\rpoint"); + assert_eq!(b.text(), "Overview\nFirst point\nSecond\npoint"); + assert_eq!(b.paragraphs[0].runs[0].props.bold, Some(true)); + } + #[test] fn replace_text_keeps_formatting() { let before = notes();