Text: story.replaceRange keeps the caret on a character boundary

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-05 17:32:57 -04:00
1 parent e47f65a595
commit 02f187cc5d
1 file changed
+23 -2
+23 -2
View File
@@ -84,8 +84,8 @@ pub fn specs() -> Vec<CommandSpec> {
st.replace(a..b, &text);
let len = st.len();
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid) {
t.anchor = t.anchor.min(len);
t.focus = t.focus.min(len);
t.anchor = floor_char_boundary(&st.text, t.anchor);
t.focus = floor_char_boundary(&st.text, t.focus);
}
Ok(json!({"length": len}))
})
@@ -973,6 +973,27 @@ mod open_type_tests {
assert_eq!(s.doc().unwrap().selection.text.unwrap().focus, 3);
}
#[test]
fn replacing_a_range_stays_on_utf8_boundaries() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("frame.create", &json!({"rect": [0, 0, 200, 100], "content": "text", "text": "é漢🙂x"})).unwrap();
let sid = r["story"].clone();
// Offsets inside 漢 (2..5) and 🙂 (5..9) snap back to the character starts: 漢 is replaced.
s.execute("story.replaceRange", &json!({"story": sid, "start": 3, "end": 6, "text": "字"})).unwrap();
let story = designcraft_doc::StoryId(sid.as_u64().unwrap());
assert_eq!(s.doc().unwrap().doc.story(story).unwrap().text, "é字🙂x");
// A caret before 🙂 (byte 5) would land inside it once é (2 bytes) is gone.
s.execute("text.select", &json!({"story": sid, "anchor": 5})).unwrap();
s.execute("story.replaceRange", &json!({"story": sid, "start": 0, "end": 2, "text": ""})).unwrap();
let st = s.doc().unwrap();
let t = st.selection.text.unwrap();
let text = &st.doc.story(t.story).unwrap().text;
assert_eq!(text, "字🙂x");
assert!(text.is_char_boundary(t.anchor) && text.is_char_boundary(t.focus), "caret {t:?} in {text:?}");
s.execute("text.move", &json!({"dir": "right"})).unwrap();
}
#[test]
fn open_type_toggles_reach_the_text() {
let mut s = Session::new();