From 56fcfb31538bb6dab51cffb3fb1d80694fa8016b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 06:59:15 -0700 Subject: [PATCH] Footnotes: moving the caret into or out of a footnote drops formatting not yet typed Go to Footnote Reference, Insert Footnote and Delete Footnote (when the caret is in the deleted footnote) moved the caret without clearing the pending typing format. Formatting chosen at the old insertion point and not yet typed then came back when the caret returned there, for example in a new footnote that takes a deleted footnote's id. The three commands now clear it, as text.select and text.move do. --- crates/engine/src/cmd/notes.rs | 50 ++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/notes.rs b/crates/engine/src/cmd/notes.rs index 9318458de..ba4c455e1 100644 --- a/crates/engine/src/cmd/notes.rs +++ b/crates/engine/src/cmd/notes.rs @@ -59,7 +59,7 @@ fn in_note(s: &Session) -> std::result::Result<(), String> { fn insert(s: &mut Session, p: &Value) -> Result { let text = super::text_param(p, "text"); - s.edit(|d, sel| { + let r = s.edit(|d, sel| { let t = sel.text.ok_or_else(|| bad("footnote.insert", "no insertion point"))?; let para = ParaFormat { style: d.footnote_options.para_style.clone(), ..Default::default() }; let st = d.story_mut(t.story).ok_or(designcraft_doc::DocError::NoStory(t.story))?; @@ -69,7 +69,9 @@ fn insert(s: &mut Session, p: &Value) -> Result { let id = st.insert_note(r.start, &text, para).ok_or_else(|| bad("footnote.insert", "the story's footnotes don't match their references"))?; sel.text = Some(TextSel { story: t.story, anchor: text.len(), focus: text.len(), frame: t.frame, cell: Some(CellAddr::footnote(id)) }); Ok(json!({"story": t.story.0, "id": id})) - }) + })?; + s.typing_format = None; + Ok(r) } /// Overlay `p` onto `base` (objects merge key by key). @@ -143,15 +145,20 @@ fn set_text(s: &mut Session, p: &Value) -> Result { fn delete(s: &mut Session, p: &Value) -> Result { let sid = story_id(p, "footnote.delete")?; let id = note_id(p, "footnote.delete")?; - s.edit(|d, sel| { + let moved = s.edit(|d, sel| { let st = d.story_mut(sid).ok_or(designcraft_doc::DocError::NoStory(sid))?; let at = st.note_anchor(id).ok_or_else(|| bad("footnote.delete", format!("no footnote {id}")))?; st.delete(at..at + designcraft_doc::FOOTNOTE_REF.len_utf8()); - if sel.text.is_some_and(|t| t.story == sid && t.cell == Some(CellAddr::footnote(id))) { + let moved = sel.text.is_some_and(|t| t.story == sid && t.cell == Some(CellAddr::footnote(id))); + if moved { sel.text = Some(TextSel::caret(sid, at)); } - Ok(Value::Null) - }) + Ok(moved) + })?; + if moved { + s.typing_format = None; + } + Ok(Value::Null) } fn go_to_reference(s: &mut Session, _: &Value) -> Result { @@ -162,6 +169,7 @@ fn go_to_reference(s: &mut Session, _: &Value) -> Result { let pos = at + designcraft_doc::FOOTNOTE_REF.len_utf8(); st.selection.text = Some(TextSel { anchor: pos, focus: pos, cell: None, ..t }); st.bump_revision(); + s.typing_format = None; Ok(json!({"story": t.story.0, "pos": pos})) } @@ -213,6 +221,36 @@ mod tests { assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().notes.len(), 1); } + #[test] + fn footnote_caret_moves_drop_the_pending_typing_format() { + let (mut s, sid) = session_with_text(); + let pending = |s: &mut Session| { + s.execute("type.char", &json!({"attrs": {"size": 30}})).unwrap(); + assert!(s.typing_format.is_some()); + }; + // Formatting chosen in footnote text, then away to the reference and back to the same caret (a new + // footnote takes the deleted one's id): typing there is in the footnote text's own format. + s.execute("text.select", &json!({"story": sid.0, "anchor": 16, "focus": 16})).unwrap(); + let id = s.execute("footnote.insert", &json!({"text": "A source."})).unwrap()["id"].as_u64().unwrap(); + pending(&mut s); + s.execute("footnote.goToReference", &json!({})).unwrap(); + s.execute("footnote.delete", &json!({"story": sid.0, "id": id})).unwrap(); + let again = s.execute("footnote.insert", &json!({"text": "A source."})).unwrap()["id"].as_u64().unwrap(); + assert_eq!(again, id); + s.execute("text.insert", &json!({"text": "x"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().notes[0].text.char_format_at(10).over.size, None); + // Each command that moves the caret drops it. + pending(&mut s); + s.execute("footnote.goToReference", &json!({})).unwrap(); + assert!(s.typing_format.is_none(), "footnote.goToReference"); + pending(&mut s); + let id = s.execute("footnote.insert", &json!({"text": "y"})).unwrap()["id"].as_u64().unwrap(); + assert!(s.typing_format.is_none(), "footnote.insert"); + pending(&mut s); + s.execute("footnote.delete", &json!({"story": sid.0, "id": id})).unwrap(); + assert!(s.typing_format.is_none(), "footnote.delete"); + } + #[test] fn not_in_cells() { let (mut s, sid) = session_with_text();