Merge pull request #1002 from Silvertree2010/fix/h2821-find-replace-ignores-line-breaks

This commit is contained in:
Brandon Thomas
2026-10-23 17:22:13 -04:00
2 changed files with 62 additions and 3 deletions
+31 -3
View File
@@ -105,7 +105,7 @@ impl Opts {
}
fn para_chars(p: &Paragraph) -> Vec<char> {
p.runs.iter().flat_map(|r| r.text.chars()).collect()
p.runs.iter().flat_map(|r| r.content().chars()).collect()
}
/// Matches that lie wholly in plain text runs (never inside a field, equation or break).
@@ -113,7 +113,7 @@ fn para_matches(p: &Paragraph, o: &Opts) -> Vec<(usize, usize)> {
let mut spans = vec![];
let mut off = 0;
for r in &p.runs {
let n = r.text.chars().count();
let n = r.char_len();
spans.push((off, off + n, matches!(r.kind, RunKind::Text)));
off += n;
}
@@ -125,7 +125,7 @@ fn splice(p: &mut Paragraph, a: usize, b: usize, with: &str) {
let mut off = 0;
let mut placed = false;
for r in &mut p.runs {
let chars: Vec<char> = r.text.chars().collect();
let chars: Vec<char> = r.content().chars().collect();
let (s, e) = (off, off + chars.len());
off = e;
if e <= a || s >= b {
@@ -222,6 +222,34 @@ mod tests {
assert_eq!(opts("cat", false, true).matches(&t), vec![(0, 3), (4, 7)]);
}
fn text_run(t: &str) -> Run {
Run { text: t.into(), props: RunProps::default(), kind: RunKind::Text }
}
fn br() -> Run {
Run { text: String::new(), props: RunProps::default(), kind: RunKind::Break }
}
#[test]
fn a_match_never_spans_a_line_break() {
let p = Paragraph { runs: vec![text_run("a"), br(), text_run("b")], ..Default::default() };
assert!(para_matches(&p, &opts("ab", true, false)).is_empty());
}
#[test]
fn offsets_count_a_line_break_as_one_char() {
let p = Paragraph { runs: vec![text_run("ab"), br(), text_run("cd")], ..Default::default() };
assert_eq!(para_matches(&p, &opts("cd", true, false)), vec![(3, 5)]);
}
#[test]
fn splice_after_a_line_break_keeps_the_break() {
let mut p = Paragraph { runs: vec![text_run("ab"), br(), text_run("cd")], ..Default::default() };
splice(&mut p, 3, 5, "X");
assert_eq!(p.text(), "ab\u{b}X");
assert_eq!(p.runs[1].kind, RunKind::Break);
}
#[test]
fn splice_across_runs_keeps_the_first_runs_format() {
let bold = RunProps { bold: Some(true), ..Default::default() };
+31
View File
@@ -3685,6 +3685,37 @@ fn find_and_replace_across_shapes_and_notes() {
assert_eq!(s.execute("text.find", &q).unwrap().as_array().unwrap().len(), n - 1);
}
/// Find and Replace count a line break as one character, so offsets agree with the editor and no
/// match spans the break.
#[test]
fn find_and_replace_after_a_line_break() {
let mut s = session();
let id = s.execute("insert.textBox", &json!({"rect": [10, 10, 300, 80], "text": "ab"})).unwrap()["id"].as_u64().unwrap();
s.execute("text.edit", &json!({"id": id, "end": true})).unwrap();
s.execute("text.insert", &json!({"text": "\u{b}cd"})).unwrap();
s.execute("text.exit", &json!({})).unwrap();
let text = |s: &Session| {
s.doc()
.unwrap()
.current_slide()
.unwrap()
.shapes
.iter()
.find(|x| u64::from(x.id.0) == id)
.and_then(|x| x.text.as_ref())
.map(|t| t.text())
.unwrap()
};
assert_eq!(text(&s), "ab\u{b}cd");
assert!(s.execute("text.find", &json!({"text": "bc"})).unwrap().as_array().unwrap().is_empty(), "no match spans the break");
let found = s.execute("text.find", &json!({"text": "cd"})).unwrap();
assert_eq!((found[0]["start"].as_u64(), found[0]["end"].as_u64()), (Some(3), Some(5)), "{found}");
s.execute("text.replace", &json!({"text": "cd", "with": "X"})).unwrap();
assert_eq!(text(&s), "ab\u{b}X");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(text(&s), "ab\u{b}cd");
}
#[test]
fn pen_pressure_is_kept_and_a_mouse_draws_at_full_width() {
let mut s = session();