mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 12:46:53 -06:00
Notes: deleting a note or converting it to text keeps the caret on the same text
note.delete removes the note's anchor (3 bytes) from the story, and note.convertToText replaces it with the note's text. Both left a caret or selection in the story text at its old byte offsets. With "copy" selected after a note in "Draft copy.", Delete Note left the selection at 9..13 in an 11-byte story, and Convert Note to Text with the note's text "ab" left "opy." selected. Both commands now move a caret or selection in the story's own text by the change in length when it comes after the anchor. A caret before the anchor stays where it is, and a caret in a table cell or in another story is unchanged.
This commit is contained in:
@@ -65,9 +65,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}),
|
||||
cmd!("note.delete", "Delete Note", ["Type", "Notes"], None, "{story, id}", 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("note.delete", "no such story"))?;
|
||||
let at = anchor(st, id).ok_or_else(|| bad("note.delete", format!("no note {id}")))?;
|
||||
follow_replace(sel, sid, at..at + NOTE_MARK.len_utf8(), 0);
|
||||
st.delete(at..at + NOTE_MARK.len_utf8());
|
||||
ok()
|
||||
})
|
||||
@@ -81,10 +82,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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("note.convertToText", "no such story"))?;
|
||||
let at = anchor(st, id).ok_or_else(|| bad("note.convertToText", format!("no note {id}")))?;
|
||||
let text = st.editorial.iter().find(|n| n.id == id).map(|n| n.text.clone()).unwrap_or_default();
|
||||
follow_replace(sel, sid, at..at + NOTE_MARK.len_utf8(), text.len());
|
||||
st.replace(at..at + NOTE_MARK.len_utf8(), &text);
|
||||
ok()
|
||||
})
|
||||
@@ -184,6 +186,16 @@ fn anchor(st: &designcraft_doc::Story, id: u64) -> Option<usize> {
|
||||
st.text.match_indices(NOTE_MARK).nth(k).map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// Keeps a caret or selection in story `sid`'s own text (not a cell) on the same characters when `range` is replaced
|
||||
/// by `len` bytes: an end after the range moves by the change in length, and an end inside it moves to its start.
|
||||
fn follow_replace(sel: &mut designcraft_doc::Selection, sid: StoryId, range: std::ops::Range<usize>, len: usize) {
|
||||
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell.is_none()) {
|
||||
let follow = |i: usize| if i >= range.end { i - range.len() + len } else { i.min(range.start) };
|
||||
t.anchor = follow(t.anchor);
|
||||
t.focus = follow(t.focus);
|
||||
}
|
||||
}
|
||||
|
||||
/// The anchor offset and id of the note before or after the selection `t` in its story. Going
|
||||
/// back skips the note whose anchor the caret directly follows, so repeated steps reach every note.
|
||||
fn neighbor(st: &designcraft_doc::Story, t: &designcraft_doc::TextSel, forward: bool) -> Option<(usize, u64)> {
|
||||
@@ -377,4 +389,32 @@ mod tests {
|
||||
assert!(s.doc().unwrap().selection.text.is_some_and(|t| t.cell.is_some() && !t.is_caret()));
|
||||
assert!(disabled(s.execute("note.convertFromText", &json!({}))), "notes go in story text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_or_converting_a_note_keeps_the_caret_on_its_character() {
|
||||
let m = designcraft_doc::NOTE_MARK.len_utf8();
|
||||
// (anchor, focus) in "Draft{note} copy." before the command, then (anchor, focus) and the selected text after
|
||||
// Delete Note and after Convert Note to Text with the note's text "ab".
|
||||
let cases = [
|
||||
((6 + m, 10 + m), ((6, 10), "copy"), ((8, 12), "copy")),
|
||||
((0, 10 + m), ((0, 10), "Draft copy"), ((0, 12), "Draftab copy")),
|
||||
((2, 4), ((2, 4), "af"), ((2, 4), "af")),
|
||||
((5 + m, 5 + m), ((5, 5), ""), ((7, 7), "")),
|
||||
];
|
||||
for (before, deleted, converted) in cases {
|
||||
for (cmd, (after, selected)) in [("note.delete", deleted), ("note.convertToText", converted)] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Draft copy."})).unwrap();
|
||||
let sid = designcraft_doc::StoryId(r["story"].as_u64().unwrap());
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": 5, "focus": 5})).unwrap();
|
||||
let n = s.execute("note.new", &json!({"text": "ab"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": before.0, "focus": before.1})).unwrap();
|
||||
s.execute(cmd, &json!({"story": n["story"], "id": n["id"]})).unwrap();
|
||||
let t = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((t.anchor, t.focus), after, "{cmd} with {before:?} selected");
|
||||
assert_eq!(&s.doc().unwrap().doc.stories[&sid].text[t.anchor..t.focus], selected, "{cmd} with {before:?} selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user