From b190e675b9446d8f0090ae928eff485411522233 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Wed, 11 Nov 2026 14:34:56 -0500 Subject: [PATCH] M4: a list item made by Enter has no start of its own, so a split list restarts at 1, as measured Enter copied the item's start (a typed "5. " gives 4) onto the new item, so once a middle item was unlisted the next one restarted at its copied start. Every paragraph an insertion creates now has start_offset 0; the first item keeps its start. Measured in Figma: unlisting "b" in 5. 6. 7. leaves "c" at 1., and the same for a list started with 1. Co-Authored-By: Claude Sonnet 5.5 Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium --- crates/engine/src/canvas/textedit.rs | 8 +++++ crates/engine/src/text_tail_tests.rs | 50 ++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/canvas/textedit.rs b/crates/engine/src/canvas/textedit.rs index 0f82330af..f726cc7ab 100644 --- a/crates/engine/src/canvas/textedit.rs +++ b/crates/engine/src/canvas/textedit.rs @@ -319,6 +319,14 @@ fn insert_model(s: &mut Session, text: &str, ime_commit: bool) -> Result NodeId { + let t = text(s, "x"); + s.execute("node.set", &json!({"id": t, "props": {"characters": ""}})).unwrap(); + begin(s, t, Some(0), false).unwrap(); + for c in first.chars() { + insert(s, &c.to_string()).unwrap(); + } + for step in ["Enter", "b", "Enter", "c"] { + if step == "Enter" { + key(s, step, false, false, false).unwrap(); + } else { + insert(s, step).unwrap(); + } + } + t +} + +#[test] +fn a_list_split_by_unlisting_a_middle_item_restarts_at_one() { + // As measured in Figma (2026-11-11): typed "5. a", Enter, "b", Enter, "c" reads 5. 6. 7.; + // Backspace at the start of "b" takes its marker and "c" then reads 1., for "1. " too. + // `list_markers` counts on after a numbered item and restarts a numbered item that follows a + // plain paragraph at its own start, so the starts and the neighbours give the markers. + for (first, start) in [("5. a", 4), ("1. a", 0)] { + let mut s = Session::new(); + let t = three_typed_items(&mut s, first); + assert_eq!( + data(&s, t).characters, + "a +b +c" + ); + // The guard: unsplit, "b" and "c" go on counting (5. 6. 7.). + let d = data(&s, t); + assert_eq!(lists(&s, t), [(N, 0); 3], "{first}: no split"); + assert_eq!((d.para_list(0).start_offset, d.para_list(1).start_offset, d.para_list(2).start_offset), (start, 0, 0), "{first}"); + set_caret(&mut s, 2, false).unwrap(); + key(&mut s, "Backspace", false, false, false).unwrap(); + let d = data(&s, t); + assert_eq!(lists(&s, t), [(N, 0), (P, 0), (N, 0)], "{first}: split"); + assert_eq!(d.para_list(2).start_offset, 0, "{first}: c restarts at 1 (the item made by Enter has no start)"); + } +} + #[test] fn list_keys_at_list_items() { // (chars, lists before, caret, key, shift, chars after, lists after, derivation)