mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 12:46:53 -06:00
Footnotes: deleting a footnote or endnote keeps the caret on the same text
footnote.delete and endnote.delete removed the reference character from the story but left a caret or selection in the story text at its old byte offsets. Everything after the reference moved back by the reference's length, so the caret ended up three bytes further along the text. A caret or selection after the reference now moves back with the text, and one before it stays where it is. footnote.setText left a caret in the replaced footnote at its old offset, which could fall past the end of the new text or inside a character. Backspace there then failed with an internal error. The caret now goes to the end of the new text.
This commit is contained in:
@@ -31,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}),
|
||||
cmd!("endnote.delete", "Delete Endnote", [], None, "{story, id} — removes the reference and its text", has_doc, |s, p| {
|
||||
let (sid, id) = target(p)?;
|
||||
s.edit(|d, _| {
|
||||
s.edit(|d, sel| {
|
||||
let st = d.story_mut(sid).ok_or_else(|| bad("endnote.delete", "no such story"))?;
|
||||
let k = st.endnotes.iter().position(|n| n.id == id).ok_or_else(|| bad("endnote.delete", format!("no endnote {id}")))?;
|
||||
let at = st
|
||||
@@ -40,6 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.nth(k)
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| bad("endnote.delete", format!("endnote {id} has no reference in the text")))?;
|
||||
super::notes::follow_delete(sel, sid, at..at + designcraft_doc::ENDNOTE_REF.len_utf8());
|
||||
st.delete(at..at + designcraft_doc::ENDNOTE_REF.len_utf8());
|
||||
ok()
|
||||
})
|
||||
@@ -172,4 +173,21 @@ mod tests {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.stories[&esid].text, "Notes\ni\tEarlier.\nii\tEdited.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_an_endnote_keeps_the_caret_on_its_character() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 1})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Alpha beta."})).unwrap();
|
||||
let sid = designcraft_doc::StoryId(a["story"].as_u64().unwrap());
|
||||
s.execute("text.select", &json!({"story": a["story"], "anchor": 5, "focus": 5})).unwrap();
|
||||
let r = s.execute("endnote.insert", &json!({"text": "Source."})).unwrap();
|
||||
let n = designcraft_doc::ENDNOTE_REF.len_utf8();
|
||||
// "beta" is selected after the reference.
|
||||
s.execute("text.select", &json!({"story": a["story"], "anchor": 6 + n, "focus": 10 + n})).unwrap();
|
||||
s.execute("endnote.delete", &json!({"story": r["story"], "id": r["id"]})).unwrap();
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((t.anchor, t.focus), (6, 10));
|
||||
assert_eq!(&s.doc().unwrap().doc.stories[&sid].text[t.anchor..t.focus], "beta");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,16 @@ fn note_id(p: &Value, cmd: &str) -> Result<u64> {
|
||||
p.get("id").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing id"))
|
||||
}
|
||||
|
||||
/// Moves a caret or selection in story `sid`'s own text (not a cell or note) back by the length of `range` when it lies
|
||||
/// after `range.start`, or to `range.start` when it lies inside `range`.
|
||||
pub(super) fn follow_delete(sel: &mut designcraft_doc::Selection, sid: StoryId, range: std::ops::Range<usize>) {
|
||||
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell.is_none()) {
|
||||
let back = |i: usize| if i > range.start { i - (i - range.start).min(range.len()) } else { i };
|
||||
t.anchor = back(t.anchor);
|
||||
t.focus = back(t.focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let only = p.get("story").and_then(Value::as_u64).map(StoryId);
|
||||
@@ -131,11 +141,15 @@ fn set_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let sid = story_id(p, "footnote.setText")?;
|
||||
let id = note_id(p, "footnote.setText")?;
|
||||
let text = super::text_param(p, "text");
|
||||
s.edit(|d, _| {
|
||||
s.edit(|d, sel| {
|
||||
let st = d.story_mut(sid).ok_or(designcraft_doc::DocError::NoStory(sid))?;
|
||||
let n = st.note_mut(id).ok_or_else(|| bad("footnote.setText", format!("no footnote {id}")))?;
|
||||
let len = n.text.len();
|
||||
n.text.replace(0..len, &text);
|
||||
// A caret in the replaced text goes to its end.
|
||||
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell == Some(CellAddr::footnote(id))) {
|
||||
(t.anchor, t.focus) = (n.text.len(), n.text.len());
|
||||
}
|
||||
Ok(Value::Null)
|
||||
})
|
||||
}
|
||||
@@ -146,6 +160,7 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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}")))?;
|
||||
follow_delete(sel, sid, at..at + designcraft_doc::FOOTNOTE_REF.len_utf8());
|
||||
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))) {
|
||||
sel.text = Some(TextSel::caret(sid, at));
|
||||
@@ -232,4 +247,35 @@ mod tests {
|
||||
let cs = s.cache.get(&d, sid, None);
|
||||
assert_eq!(cs.frames[0].notes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_footnote_keeps_the_caret_on_its_character() {
|
||||
let n = designcraft_doc::FOOTNOTE_REF.len_utf8();
|
||||
// (anchor, focus) in the story text before the delete, then after it.
|
||||
for (before, after) in [((17 + n, 21 + n), (17, 21)), ((5, 21 + n), (5, 21)), ((5, 5), (5, 5)), ((16 + n, 16 + n), (16, 16))] {
|
||||
let (mut s, sid) = session_with_text();
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 16, "focus": 16})).unwrap();
|
||||
let id = s.execute("footnote.insert", &json!({"text": "x"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": before.0, "focus": before.1})).unwrap();
|
||||
s.execute("footnote.delete", &json!({"story": sid.0, "id": id})).unwrap();
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((t.anchor, t.focus), after, "selected {before:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_footnote_text_puts_its_caret_at_the_end() {
|
||||
let (mut s, sid) = session_with_text();
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 16, "focus": 16})).unwrap();
|
||||
let id = s.execute("footnote.insert", &json!({"text": "abcd"})).unwrap()["id"].as_u64().unwrap();
|
||||
// The caret between "c" and "d" is inside the second "é" of the new text.
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!(t.cell, Some(CellAddr::footnote(id)));
|
||||
s.doc_mut().unwrap().selection.text = Some(TextSel { anchor: 3, focus: 3, ..t });
|
||||
s.execute("footnote.setText", &json!({"story": sid.0, "id": id, "text": "éé"})).unwrap();
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((t.anchor, t.focus), (4, 4));
|
||||
s.execute("text.delete", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().notes[0].text.text, "é");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user