mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 22:26:49 -06:00
refactor(engine): move the tests module of cmd/find.rs to cmd/find/tests.rs
This commit is contained in:
@@ -441,763 +441,7 @@ fn find_next(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn find_and_change_text_and_grep() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "Call 555-1234 or 555-9876.\nColour and colour."}))
|
||||
.unwrap();
|
||||
let m = s.execute("find.find", &json!({"find": "colour"})).unwrap();
|
||||
assert_eq!(m.as_array().unwrap().len(), 2);
|
||||
let m = s.execute("find.find", &json!({"find": "colour", "caseSensitive": true})).unwrap();
|
||||
assert_eq!(m.as_array().unwrap().len(), 1);
|
||||
let r = s.execute("find.change", &json!({"find": r"(\d{3})-(\d{4})", "change": "($1) $2", "grep": true})).unwrap();
|
||||
assert_eq!(r["count"], 2);
|
||||
let r = s.execute("find.change", &json!({"find": "colour", "change": "color", "attrs": {"fontStyle": "Italic"}})).unwrap();
|
||||
assert_eq!(r["count"], 2);
|
||||
let d = s.execute("document.inspect", &json!({})).unwrap();
|
||||
let sid = d["stories"][0]["id"].as_u64().unwrap();
|
||||
let text = s.execute("story.get", &json!({"story": sid})).unwrap()["text"].as_str().unwrap().to_string();
|
||||
assert_eq!(text, "Call (555) 1234 or (555) 9876.\ncolor and color.");
|
||||
// Paragraph metacharacter in text mode.
|
||||
assert_eq!(s.execute("find.find", &json!({"find": ".^pc"})).unwrap().as_array().unwrap().len(), 1);
|
||||
s.execute("find.next", &json!({"find": "or"})).unwrap();
|
||||
assert!(s.doc().unwrap().selection.text.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_first_changes_one_match_in_the_whole_document() {
|
||||
for grep in [false, true] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
for y in [36, 336] {
|
||||
s.execute("frame.create", &json!({"rect": [36, y, 300, y + 200], "content": "text", "text": "red and red"})).unwrap();
|
||||
}
|
||||
let r = s.execute("find.change", &json!({"find": "red", "change": "blue", "first": true, "grep": grep})).unwrap();
|
||||
assert_eq!(r["count"], 1);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "blue"})).unwrap().as_array().unwrap().len(), 1);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "red"})).unwrap().as_array().unwrap().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_session(text: &str) -> (Session, designcraft_doc::StoryId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let frame = s.execute("frame.create", &json!({"rect": [36, 36, 100, 100], "content": "text", "text": text})).unwrap();
|
||||
(s, designcraft_doc::StoryId(frame["story"].as_u64().unwrap()))
|
||||
}
|
||||
|
||||
fn scope_select(s: &mut Session, sid: designcraft_doc::StoryId, a: usize, b: usize) {
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": a, "focus": b})).unwrap();
|
||||
}
|
||||
|
||||
fn scope_hit(s: &mut Session, p: &serde_json::Value, sid: designcraft_doc::StoryId, a: usize, b: usize) {
|
||||
assert_eq!(s.execute("find.next", p).unwrap(), json!({"story": sid.0, "start": a, "end": b}));
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((selected.story, selected.range(), selected.cell), (sid, a..b, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_first_change_stops_after_first_matching_story_and_repairs_selection() {
|
||||
for (texts, first, explicit, grep, expected, count) in [
|
||||
(vec!["red red", "red"], Some(true), false, false, vec!["blue red", "red"], 1),
|
||||
(vec!["keep", "red red", "red"], Some(true), false, false, vec!["keep", "blue red", "red"], 1),
|
||||
(vec!["keep", "other"], Some(true), false, false, vec!["keep", "other"], 0),
|
||||
(vec!["red red", "red"], Some(false), false, false, vec!["blue blue", "blue"], 3),
|
||||
(vec!["red red", "red"], None, false, false, vec!["blue blue", "blue"], 3),
|
||||
(vec!["red red", "red"], Some(true), true, false, vec!["blue red", "red"], 1),
|
||||
(vec!["red red", "red"], Some(false), true, false, vec!["blue blue", "red"], 2),
|
||||
(vec!["", "é🙂"], Some(true), false, true, vec!["X", "é🙂"], 1),
|
||||
(vec!["é", "🙂"], Some(false), false, true, vec!["XéX", "X🙂X"], 4),
|
||||
] {
|
||||
let (mut s, sid) = scope_session(texts[0]);
|
||||
let mut sids = vec![sid];
|
||||
for text in texts.iter().skip(1) {
|
||||
let frame = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": text})).unwrap();
|
||||
sids.push(designcraft_doc::StoryId(frame["story"].as_u64().unwrap()));
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc.stories.keys().copied().collect::<Vec<_>>(), sids);
|
||||
// Document Change keeps canonical target order even with a later story current.
|
||||
scope_select(&mut s, *sids.last().unwrap(), 0, 0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let mut p = json!({
|
||||
"find": if grep { "(?:)" } else { "red" },
|
||||
"change": if grep { "X" } else { "blue" },
|
||||
"scope": "document", "grep": grep
|
||||
});
|
||||
if let Some(first) = first {
|
||||
p["first"] = json!(first);
|
||||
}
|
||||
if explicit {
|
||||
p["story"] = json!(sid.0);
|
||||
p["scope"] = json!("selection");
|
||||
}
|
||||
if first == Some(true) && !explicit && !grep && texts.last() == Some(&"red") {
|
||||
let last = *sids.last().unwrap();
|
||||
scope_hit(&mut s, &p, last, 0, 3);
|
||||
let first_hit = sids[if texts.len() == 2 { 0 } else { 1 }];
|
||||
assert_eq!(
|
||||
s.execute("find.find", &p).unwrap(),
|
||||
json!([
|
||||
{"story": first_hit.0, "start": 0, "end": 3, "text": "red"},
|
||||
{"story": first_hit.0, "start": 4, "end": 7, "text": "red"},
|
||||
{"story": last.0, "start": 0, "end": 3, "text": "red"}
|
||||
]),
|
||||
"Count remains canonical after Next paints the later story"
|
||||
);
|
||||
}
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": count}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.selection, selection);
|
||||
for (i, sid) in sids.iter().enumerate() {
|
||||
assert_eq!(st.doc.story(*sid).unwrap().text, expected[i]);
|
||||
if texts[i] == expected[i] {
|
||||
assert_eq!(st.doc.story(*sid), before.story(*sid), "untouched story {i} stays entirely unchanged");
|
||||
}
|
||||
}
|
||||
st.doc.check().unwrap();
|
||||
}
|
||||
|
||||
// Identical replacement is still one counted match, not a reason to visit another story.
|
||||
let (mut s, sid) = scope_session("red red");
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "red"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, other_sid, 0, 3);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert_eq!(
|
||||
s.execute("find.change", &json!({"find": "red", "change": "red", "scope": "document", "first": true})).unwrap(),
|
||||
json!({"count": 1})
|
||||
);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid), before.story(other_sid));
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
|
||||
for reverse in [false, true] {
|
||||
let (mut s, sid) = scope_session("éX X");
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "X"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, sid, if reverse { 3 } else { 2 }, if reverse { 2 } else { 3 });
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let mut selection = s.doc().unwrap().selection.clone();
|
||||
let t = selection.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = if reverse { (6, 2) } else { (2, 6) };
|
||||
let p = json!({"find": "X", "change": "🙂", "scope": "document", "first": true});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 3);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, "é🙂 X");
|
||||
assert_eq!(st.doc.story(other_sid), before.story(other_sid));
|
||||
assert_eq!(st.selection, selection, "first-match exit must follow final UTF-8 repair");
|
||||
st.doc.check().unwrap();
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "🙂", "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid.0, "start": 2, "end": 6, "text": "🙂"}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grep_in_the_documents_dialect() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "Москва and Paris, 1\u{2013}2 days.\nok"})).unwrap();
|
||||
// `\u` uppercase, `\l` lowercase letter.
|
||||
let m = s.execute("find.find", &json!({"find": r"\u\l+", "grep": true, "caseSensitive": true})).unwrap();
|
||||
let words: Vec<&str> = m.as_array().unwrap().iter().map(|x| x["text"].as_str().unwrap()).collect();
|
||||
assert_eq!(words, ["Москва", "Paris"]);
|
||||
// `~=` en dash, `\h` space, `\r` paragraph end; the change uses groups, `~S` and `\r`.
|
||||
let r = s.execute("find.change", &json!({"find": r"(\d)~=(\d)\h(\l+)\.\r", "change": r"$1~=$2~S$3!\r", "grep": true})).unwrap();
|
||||
assert_eq!(r["count"], 1);
|
||||
let sid = s.execute("document.inspect", &json!({})).unwrap()["stories"][0]["id"].as_u64().unwrap();
|
||||
let text = s.execute("story.get", &json!({"story": sid})).unwrap()["text"].as_str().unwrap().to_string();
|
||||
assert!(text.starts_with("Москва and Paris, 1\u{2013}2\u{A0}days!\nok"), "{text:?}");
|
||||
// A pattern that doesn't compile is an error, not a silent no-op.
|
||||
assert!(s.execute("find.find", &json!({"find": "[[:nope:]]", "grep": true})).is_err());
|
||||
assert!(s.execute("find.change", &json!({"find": "(", "change": "", "grep": true})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_exact_baseline_count_next_change() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let frame = s
|
||||
.execute(
|
||||
"frame.create",
|
||||
&json!({
|
||||
"rect": [36, 36, 300, 200], "content": "text", "text": "red red"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let sid = frame["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 3})).unwrap();
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "red", "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid, "start": 0, "end": 3, "text": "red"}])
|
||||
);
|
||||
assert_eq!(s.execute("find.next", &json!({"find": "red", "scope": "selection"})).unwrap(), json!({"story": sid, "start": 0, "end": 3}));
|
||||
assert_eq!(s.execute("find.change", &json!({"find": "red", "change": "blue", "scope": "selection"})).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.execute("story.get", &json!({"story": sid})).unwrap()["text"], "blue red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_growth_preserves_direction_and_full_scalar_hits() {
|
||||
for (text, find, change, changed, start, old_end, new_end) in [("a", "a", "é", "é", 0, 1, 2), ("éX", "X", "🙂", "é🙂", 2, 3, 6)] {
|
||||
for reverse in [false, true] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
// Real caret movement keeps the frame identity, unlike text.select.
|
||||
s.execute("text.move", &json!({"dir": "left", "extend": reverse})).unwrap();
|
||||
if !reverse {
|
||||
s.execute("text.move", &json!({"dir": "right", "extend": true})).unwrap();
|
||||
}
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((selected.anchor, selected.focus), if reverse { (old_end, start) } else { (start, old_end) });
|
||||
assert!(selected.frame.is_some());
|
||||
let mut expected = s.doc().unwrap().selection.clone();
|
||||
let t = expected.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = if reverse { (new_end, start) } else { (start, new_end) };
|
||||
let p = json!({"find": find, "change": change, "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": start, "end": old_end, "text": find}]));
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, changed);
|
||||
assert_eq!(st.selection, expected);
|
||||
let t = st.selection.text.unwrap();
|
||||
assert!(changed.is_char_boundary(t.anchor) && changed.is_char_boundary(t.focus));
|
||||
assert!(s.find_context.is_none());
|
||||
let p = json!({"find": change, "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": start, "end": new_end, "text": change}]));
|
||||
scope_hit(&mut s, &p, sid, start, new_end);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_repairs_final_boundaries_without_remapping_valid_offsets() {
|
||||
for (text, (a, b), find, change, changed, (after_a, after_b), count) in [
|
||||
("aX", (1, 1), "a", "é", "éX", (0, 0), 1), // A caret stays collapsed and floors.
|
||||
("aX", (2, 2), "a", "é", "éX", (2, 2), 1), // A valid caret stays at its byte offset.
|
||||
("abc", (1, 2), "abc", "🙂", "🙂", (0, 4), 1), // Both endpoints need repair.
|
||||
("abc", (2, 1), "abc", "🙂", "🙂", (4, 0), 1),
|
||||
("é🙂", (2, 6), "🙂", "X", "éX", (2, 3), 1), // Existing shortening behavior.
|
||||
("é🙂", (6, 2), "🙂", "X", "éX", (3, 2), 1),
|
||||
("red", (0, 3), "red", "blue", "blue", (0, 3), 1), // Do not extend a valid upper bound.
|
||||
("aX", (1, 2), "a", "zz", "zzX", (1, 2), 1), // Do not track the original X.
|
||||
("aaX", (1, 2), "a", "é", "ééX", (0, 2), 2), // Per-edit repair would wrongly yield 0..4.
|
||||
("aaX", (2, 3), "a", "é", "ééX", (2, 4), 2),
|
||||
("abc", (1, 3), "abc", "", "", (0, 0), 1),
|
||||
("abc", (2, 3), "abc", "é", "é", (2, 2), 1), // EOF clamp may collapse a nonempty range.
|
||||
] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
scope_select(&mut s, sid, a, b);
|
||||
let mut expected = s.doc().unwrap().selection.clone();
|
||||
let t = expected.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = (after_a, after_b);
|
||||
assert_eq!(s.execute("find.change", &json!({"find": find, "change": change, "scope": "document"})).unwrap(), json!({"count": count}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, changed, "{text} {a}->{b}");
|
||||
assert_eq!(st.selection, expected, "{text} {a}->{b}");
|
||||
assert!(changed.is_char_boundary(after_a) && changed.is_char_boundary(after_b));
|
||||
if after_a == after_b {
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "(?:)", "grep": true, "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid.0, "start": after_a, "end": after_a, "text": ""}])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_other_story_and_no_hits_preserve_selection() {
|
||||
for explicit in [false, true] {
|
||||
let (mut s, sid) = scope_session("éX");
|
||||
let frame = s.doc().unwrap().selection.text.unwrap().frame.unwrap();
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "a"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, sid, 3, 2);
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
let mut p = json!({"find": "a", "change": "🙂", "scope": "document"});
|
||||
if explicit {
|
||||
p["story"] = json!(other_sid.0);
|
||||
}
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "🙂");
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 0}));
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "🙂");
|
||||
|
||||
s.execute("selection.set", &json!({"ids": [frame.0]})).unwrap();
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
assert!(before.text.is_none());
|
||||
p["find"] = json!("🙂");
|
||||
p["change"] = json!("é");
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "é");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_mixed_grep_matches_count_next_and_change() {
|
||||
for (text, pattern, expected, changed) in [
|
||||
("a", "a*", vec![(0, 1)], "X"),
|
||||
("aa", "a|(?:)", vec![(0, 1), (1, 2)], "XX"),
|
||||
("ab", "a*", vec![(0, 1), (2, 2)], "XbX"),
|
||||
("éa", "é|(?:)", vec![(0, 2), (3, 3)], "XaX"),
|
||||
] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
scope_select(&mut s, sid, 0, text.len());
|
||||
let p = json!({"find": pattern, "grep": true, "scope": "selection", "change": "X"});
|
||||
let counted = s.execute("find.find", &p).unwrap();
|
||||
let ranges: Vec<_> =
|
||||
counted.as_array().unwrap().iter().map(|m| (m["start"].as_u64().unwrap() as usize, m["end"].as_u64().unwrap() as usize)).collect();
|
||||
assert_eq!(ranges, expected, "{pattern} on {text}");
|
||||
for (a, b) in &expected {
|
||||
scope_hit(&mut s, &p, sid, *a, *b);
|
||||
}
|
||||
for _ in 0..2 {
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null, "{pattern} on {text}");
|
||||
}
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), counted);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], expected.len());
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, changed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_capture_replacement_and_first_keep_original_domain() {
|
||||
let (mut s, sid) = scope_session("a1 a2 | a3");
|
||||
scope_select(&mut s, sid, 0, 5);
|
||||
let p = json!({"find": r"a(\d)", "grep": true, "scope": "selection", "change": "b$1", "first": true});
|
||||
scope_hit(&mut s, &p, sid, 0, 2);
|
||||
scope_hit(&mut s, &p, sid, 3, 5);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "b1 a2 | a3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_selection_retains_nonempty_hits_count_change_and_native_text() {
|
||||
for (a, b) in [(0, 7), (7, 0)] {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, a, b);
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection", "attrs": {"fontStyle": "Italic"}});
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let strong = std::sync::Arc::strong_count(&before);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
assert_eq!(s.doc().unwrap().revision, revision);
|
||||
assert_eq!(std::sync::Arc::strong_count(&before), strong, "search stores weak document identity");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
scope_hit(&mut s, &p, sid, 4, 7);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 2);
|
||||
let changed = s.doc().unwrap().doc.story(sid).unwrap().clone();
|
||||
assert_eq!(changed.text, "blue blue | red");
|
||||
assert_eq!(changed.char_format_at(0).over.font_style.as_deref(), Some("Italic"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history + 1);
|
||||
assert!(s.find_context.is_none());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
assert!(s.find_context.is_none());
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap(), &changed);
|
||||
let native = s.execute("file.serialize", &json!({})).unwrap();
|
||||
s.execute("file.openBytes", &json!({"name": "scope.designcraft", "base64": native["base64"]})).unwrap();
|
||||
assert_eq!(serde_json::to_value(s.doc().unwrap().doc.story(sid).unwrap()).unwrap(), serde_json::to_value(&changed).unwrap());
|
||||
assert!(s.find_context.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_matcher_restart_replacement_continuation_and_external_reselection() {
|
||||
let (mut s, sid) = scope_session("red blue red | blue red");
|
||||
scope_select(&mut s, sid, 0, 12);
|
||||
let mut p = json!({"find": "red", "scope": "selection", "change": "one"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["change"] = json!("two");
|
||||
p["attrs"] = json!({"fontStyle": "Italic"});
|
||||
scope_hit(&mut s, &p, sid, 9, 12);
|
||||
p["find"] = json!("blue");
|
||||
scope_hit(&mut s, &p, sid, 4, 8);
|
||||
p["find"] = json!("red");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["caseSensitive"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["grep"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3); // GREP alone changes matcher identity.
|
||||
p["find"] = json!("r.d");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["wholeWord"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let hit = s.doc().unwrap().selection.text.unwrap();
|
||||
scope_select(&mut s, sid, hit.anchor, hit.focus);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1, "same-range external reselection starts a new domain");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
scope_select(&mut s, sid, 20, 23);
|
||||
p["find"] = json!("red");
|
||||
scope_hit(&mut s, &p, sid, 20, 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_word_reselection_at_same_hit_recaptures_bounds() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let selected = s.doc().unwrap().selection.clone();
|
||||
let frame = selected.text.unwrap().frame.unwrap();
|
||||
let point = {
|
||||
let st = s.doc().unwrap();
|
||||
let cs = s.cache.get(&st.doc, sid, None);
|
||||
let (fi, x, baseline, ascent, _) = designcraft_compose::caret(&cs, 1).unwrap();
|
||||
let inner = designcraft_geom::Point::new(x, baseline - ascent * 0.5);
|
||||
assert_eq!(cs.frames[fi].frame, frame);
|
||||
assert_eq!(designcraft_compose::hit(&cs, fi, inner), Some(1), "real point lies inside the current hit");
|
||||
let loc = st.doc.find(frame).unwrap();
|
||||
st.doc.parent_xf(&loc) * st.doc.text_xf(st.doc.item(frame).unwrap()) * inner
|
||||
};
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
s.execute("text.selectWord", &json!({"frame": frame.0, "point": [point.x, point.y]})).unwrap();
|
||||
assert!(!s.text_drag, "explicit word reselection does not start a text drag");
|
||||
assert_eq!(s.doc().unwrap().selection, selected, "same full selection, including frame");
|
||||
assert!(s.doc().unwrap().revision > revision, "a new revision");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": 0, "end": 3, "text": "red"}]));
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "blue red | red");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "blue red | red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_story_explicit_story_document_order_and_scope_changes() {
|
||||
let (mut s, a) = scope_session("black red");
|
||||
let other = s.execute("frame.create", &json!({"rect": [150, 36, 100, 100], "content": "text", "text": "red"})).unwrap();
|
||||
let b = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
let story = json!({"find": "red", "scope": "story"});
|
||||
assert_eq!(s.execute("find.next", &story).unwrap(), serde_json::Value::Null, "Story does not enter B or wrap to its prefix");
|
||||
scope_select(&mut s, b, 0, 0);
|
||||
let explicit = json!({"find": "black", "story": a.0, "scope": "selection"});
|
||||
scope_hit(&mut s, &explicit, a, 0, 5);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert!(s.execute("find.next", &json!({"find": "red", "story": u64::MAX})).is_err());
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
scope_hit(&mut s, &json!({"find": "red"}), b, 0, 3);
|
||||
let counted = s.execute("find.find", &json!({"find": "red"})).unwrap();
|
||||
assert_eq!(counted[0]["story"], a.0, "Count retains canonical story order");
|
||||
assert_eq!(counted[1]["story"], b.0);
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
scope_hit(&mut s, &json!({"find": "red", "scope": "selection"}), a, 6, 9);
|
||||
assert_eq!(s.execute("find.next", &story).unwrap(), serde_json::Value::Null, "scope change uses the current caret");
|
||||
s.execute("selection.set", &json!({"ids": []})).unwrap();
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
assert!(s.execute(command, &json!({"find": "red", "change": "blue", "scope": "selection"})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_substring_context_utf8_zero_width_and_invalid_ranges() {
|
||||
let (mut s, sid) = scope_session("scat é🙂 red");
|
||||
scope_select(&mut s, sid, 1, 4);
|
||||
let p = json!({"find": "cat", "wholeWord": true, "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 1, 4);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
scope_select(&mut s, sid, 1, 4);
|
||||
let p = json!({"find": "^c|^a", "grep": true, "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 1, 2);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null, "cursor does not redefine ^");
|
||||
scope_select(&mut s, sid, 5, 11);
|
||||
let empty = json!({"find": "(?:)", "grep": true, "scope": "selection"});
|
||||
for pos in [5, 7, 11] {
|
||||
scope_hit(&mut s, &empty, sid, pos, pos);
|
||||
}
|
||||
for _ in 0..2 {
|
||||
assert_eq!(s.execute("find.next", &empty).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
assert_eq!(s.execute("find.find", &empty).unwrap().as_array().unwrap().len(), 3);
|
||||
scope_select(&mut s, sid, 7, 7);
|
||||
scope_hit(&mut s, &empty, sid, 7, 7);
|
||||
assert_eq!(s.execute("find.next", &empty).unwrap(), serde_json::Value::Null);
|
||||
scope_select(&mut s, sid, 12, 15);
|
||||
let nonempty = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
scope_hit(&mut s, &nonempty, sid, 12, 15);
|
||||
assert_eq!(s.execute("find.change", &nonempty).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "scat é🙂 blue");
|
||||
for range in [6..7, 0..usize::MAX] {
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.selection = designcraft_doc::Selection::text(designcraft_doc::TextSel {
|
||||
story: sid,
|
||||
anchor: range.start,
|
||||
focus: range.end,
|
||||
frame: None,
|
||||
cell: None,
|
||||
});
|
||||
st.bump_revision();
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let err = s.execute(command, &json!({"find": "x", "change": "y", "scope": "selection"})).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_invalid_query_keeps_continuation_and_cross_boundary_nonmatches() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let selected = s.doc().unwrap().selection.clone();
|
||||
assert!(s.execute("find.next", &json!({"find": "[", "grep": true, "scope": "selection"})).is_err());
|
||||
assert_eq!(s.doc().unwrap().selection, selected);
|
||||
scope_hit(&mut s, &p, sid, 4, 7);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "red |", "scope": "selection"})).unwrap(), json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_document_switch_edit_and_reset() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.set_active(0);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("story.setText", &json!({"story": sid.0, "text": "red"})).unwrap();
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
s.reset_find_scope();
|
||||
assert!(s.find_context.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_real_preview_restores_before_resolving_bounds() {
|
||||
let (mut s, sid) = scope_session("");
|
||||
s.execute(
|
||||
"data.source.select",
|
||||
&json!({
|
||||
"bytes": crate::cmd::base64_encode(b"x\nred red | red\n"), "name": "scope.csv"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("data.placeholder.add", &json!({"field": "x", "story": sid.0, "at": 0})).unwrap();
|
||||
let template = s.doc().unwrap().doc.clone();
|
||||
assert_eq!(template.story(sid).unwrap().text, "<<x>>");
|
||||
s.execute("data.preview", &json!({"record": 1})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
// Install a viewport selection in the real preview. text.select is journaled
|
||||
// and would itself restore the template before selecting.
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.selection = designcraft_doc::Selection::text(designcraft_doc::TextSel { story: sid, anchor: 0, focus: 7, frame: None, cell: None });
|
||||
st.bump_revision();
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let err = s.execute("find.change", &p).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(std::sync::Arc::ptr_eq(&template, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "<<x>>");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert!(s.doc().unwrap().preview_stash.is_none());
|
||||
assert!(s.find_context.is_none());
|
||||
// Valid fresh bounds are allowed after restoration; no cached preview range
|
||||
// survives. A document query can see the preview, but Next sees the template.
|
||||
s.execute("data.preview", &json!({"record": 1})).unwrap();
|
||||
let document = json!({"find": "red"});
|
||||
assert_eq!(s.execute("find.find", &document).unwrap().as_array().unwrap().len(), 3);
|
||||
assert_eq!(s.execute("find.next", &document).unwrap(), serde_json::Value::Null);
|
||||
assert!(std::sync::Arc::ptr_eq(&template, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.execute("find.find", &document).unwrap(), json!([]));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_scope_searches_and_changes_table_cells() {
|
||||
let (mut s, sid) = scope_session("Ore 12.15 here");
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
let table = s.execute("table.insert", &json!({"rows": 1, "cols": 2})).unwrap()["table"].as_u64().unwrap();
|
||||
s.execute("table.setCell", &json!({"row": 0, "col": 1, "text": "Ore 12.15"})).unwrap();
|
||||
// The caret sits in the first cell, so Find Next goes to the second cell, then the story.
|
||||
let p = json!({"find": "Ore 12.15", "scope": "document"});
|
||||
let cell = designcraft_doc::CellAddr { table, row: 0, col: 1 };
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), json!({"story": sid.0, "start": 0, "end": 9, "cell": {"table": table, "row": 0, "col": 1}}));
|
||||
assert_eq!(s.doc().unwrap().selection.text.map(|t| (t.story, t.range(), t.cell)), Some((sid, 0..9, Some(cell))));
|
||||
let next = s.execute("find.next", &p).unwrap();
|
||||
assert!(next.get("start").is_some() && next.get("cell").is_none(), "{next}");
|
||||
let hits = s.execute("find.find", &p).unwrap();
|
||||
let hits = hits.as_array().unwrap();
|
||||
assert_eq!(hits.len(), 2);
|
||||
let in_cell: Vec<_> = hits.iter().filter(|h| h.get("cell").is_some()).collect();
|
||||
assert_eq!(in_cell.len(), 1);
|
||||
assert_eq!((in_cell[0]["cell"]["row"].as_u64(), in_cell[0]["cell"]["col"].as_u64()), (Some(0), Some(1)));
|
||||
let n = s.execute("find.change", &json!({"find": "Ore 12.15", "change": "Ore 13.00", "scope": "document"})).unwrap();
|
||||
assert_eq!(n["count"], 2);
|
||||
assert!(s.execute("find.find", &json!({"find": "12.15", "scope": "document"})).unwrap().as_array().unwrap().is_empty());
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "13.00", "scope": "document"})).unwrap().as_array().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
fn child_scope_session(note: bool, child: &str) -> (Session, designcraft_doc::StoryId, designcraft_doc::TextSel) {
|
||||
let (mut s, sid) = scope_session("red");
|
||||
scope_select(&mut s, sid, 3, 3);
|
||||
if note {
|
||||
s.execute("footnote.insert", &json!({"text": child})).unwrap();
|
||||
} else {
|
||||
s.execute("table.insert", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": child, "raw": true})).unwrap();
|
||||
}
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
(s, sid, selected)
|
||||
}
|
||||
|
||||
/// Child-story scopes are rejected by every find command, leaving the document untouched.
|
||||
fn assert_child_scope_requests_rejected(s: &mut Session) {
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let journal = s.journal.clone();
|
||||
for scope in ["story", "selection"] {
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
let err = s.execute(command, &json!({"find": "red", "change": "blue", "scope": scope})).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(err.to_string().contains("not supported"));
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert_eq!(s.doc().unwrap().revision, revision);
|
||||
assert_eq!(s.journal, journal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_children_error_atomically_and_parent_requests_stay_top_level() {
|
||||
for note in [false, true] {
|
||||
for child in ["red", "red x", "é red red red red red"] {
|
||||
let (mut s, sid, mut selected) = child_scope_session(note, child);
|
||||
selected.anchor = 0;
|
||||
selected.focus = child.len();
|
||||
s.doc_mut().unwrap().selection = designcraft_doc::Selection::text(selected);
|
||||
assert_child_scope_requests_rejected(&mut s);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
for explicit in [false, true] {
|
||||
let mut p = json!({"find": "red", "change": "blue"});
|
||||
if explicit {
|
||||
p["story"] = json!(sid.0);
|
||||
p["scope"] = json!("selection");
|
||||
}
|
||||
// Document scope also covers table cells (#1624); an explicit story and footnotes stay top-level.
|
||||
let in_cell = if explicit || note { 0 } else { child.matches("red").count() };
|
||||
let changed = if in_cell > 0 { child.replace("red", "blue") } else { child.to_string() };
|
||||
s.doc_mut().unwrap().selection = selection.clone();
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1 + in_cell);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.doc_mut().unwrap().selection = selection.clone();
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 1 + in_cell);
|
||||
assert_eq!(s.doc().unwrap().doc.text_story(sid, selected.cell).unwrap().text, changed);
|
||||
assert_eq!(s.doc().unwrap().selection, selection, "parent replacement does not clamp child offsets");
|
||||
p["find"] = json!("blue");
|
||||
p["change"] = json!("red");
|
||||
s.execute("find.change", &p).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut s, _, _) = child_scope_session(false, "red");
|
||||
s.execute("table.selectTable", &json!({})).unwrap();
|
||||
for scope in ["story", "selection"] {
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
assert!(
|
||||
s.execute(command, &json!({"find": "red", "change": "blue", "scope": scope})).unwrap_err().to_string().contains("not supported")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_threaded_overset_story_uses_logical_offsets() {
|
||||
let text = format!("{}red", "word ".repeat(100));
|
||||
let (mut s, sid) = scope_session(&text);
|
||||
let a = s.doc().unwrap().doc.story(sid).unwrap().frames[0];
|
||||
let next = s.execute("frame.create", &json!({"rect": [150, 36, 20, 1], "content": "text"})).unwrap();
|
||||
let b = designcraft_doc::ItemId(next["id"].as_u64().unwrap());
|
||||
s.edit(|d, _| Ok(d.thread(a, b)?)).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().frames.len(), 2);
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
scope_hit(&mut s, &json!({"find": "red", "scope": "story"}), sid, 500, 503);
|
||||
assert_eq!(s.execute("find.next", &json!({"find": "red", "scope": "story"})).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_zero_width_story_explicit_and_document_endpoints_terminate() {
|
||||
for scope in ["story", "explicit", "document"] {
|
||||
let (mut s, sid) = scope_session("é🙂");
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
let mut p = json!({"find": "(?:)", "grep": true, "scope": scope});
|
||||
if scope == "explicit" {
|
||||
p["story"] = json!(sid.0);
|
||||
}
|
||||
for pos in [0, 2, 6] {
|
||||
scope_hit(&mut s, &p, sid, pos, pos);
|
||||
}
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn find_scope_revert_releases_context_with_same_document_uid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("scope.designcraft");
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
s.execute("file.save", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let uid = s.doc().unwrap().uid;
|
||||
s.execute("story.setText", &json!({"story": sid.0, "text": "red red"})).unwrap();
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().uid, uid);
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
assert!(s.execute("find.find", &p).is_err(), "revert's empty selection cannot inherit a previous range");
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
/// Objects matching Find/Change › Object criteria, in document order (nested ones included).
|
||||
fn find_objects(d: &designcraft_doc::Document, p: &Value) -> Vec<designcraft_doc::ItemId> {
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn find_and_change_text_and_grep() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "Call 555-1234 or 555-9876.\nColour and colour."}))
|
||||
.unwrap();
|
||||
let m = s.execute("find.find", &json!({"find": "colour"})).unwrap();
|
||||
assert_eq!(m.as_array().unwrap().len(), 2);
|
||||
let m = s.execute("find.find", &json!({"find": "colour", "caseSensitive": true})).unwrap();
|
||||
assert_eq!(m.as_array().unwrap().len(), 1);
|
||||
let r = s.execute("find.change", &json!({"find": r"(\d{3})-(\d{4})", "change": "($1) $2", "grep": true})).unwrap();
|
||||
assert_eq!(r["count"], 2);
|
||||
let r = s.execute("find.change", &json!({"find": "colour", "change": "color", "attrs": {"fontStyle": "Italic"}})).unwrap();
|
||||
assert_eq!(r["count"], 2);
|
||||
let d = s.execute("document.inspect", &json!({})).unwrap();
|
||||
let sid = d["stories"][0]["id"].as_u64().unwrap();
|
||||
let text = s.execute("story.get", &json!({"story": sid})).unwrap()["text"].as_str().unwrap().to_string();
|
||||
assert_eq!(text, "Call (555) 1234 or (555) 9876.\ncolor and color.");
|
||||
// Paragraph metacharacter in text mode.
|
||||
assert_eq!(s.execute("find.find", &json!({"find": ".^pc"})).unwrap().as_array().unwrap().len(), 1);
|
||||
s.execute("find.next", &json!({"find": "or"})).unwrap();
|
||||
assert!(s.doc().unwrap().selection.text.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_first_changes_one_match_in_the_whole_document() {
|
||||
for grep in [false, true] {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
for y in [36, 336] {
|
||||
s.execute("frame.create", &json!({"rect": [36, y, 300, y + 200], "content": "text", "text": "red and red"})).unwrap();
|
||||
}
|
||||
let r = s.execute("find.change", &json!({"find": "red", "change": "blue", "first": true, "grep": grep})).unwrap();
|
||||
assert_eq!(r["count"], 1);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "blue"})).unwrap().as_array().unwrap().len(), 1);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "red"})).unwrap().as_array().unwrap().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_session(text: &str) -> (Session, designcraft_doc::StoryId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let frame = s.execute("frame.create", &json!({"rect": [36, 36, 100, 100], "content": "text", "text": text})).unwrap();
|
||||
(s, designcraft_doc::StoryId(frame["story"].as_u64().unwrap()))
|
||||
}
|
||||
|
||||
fn scope_select(s: &mut Session, sid: designcraft_doc::StoryId, a: usize, b: usize) {
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": a, "focus": b})).unwrap();
|
||||
}
|
||||
|
||||
fn scope_hit(s: &mut Session, p: &serde_json::Value, sid: designcraft_doc::StoryId, a: usize, b: usize) {
|
||||
assert_eq!(s.execute("find.next", p).unwrap(), json!({"story": sid.0, "start": a, "end": b}));
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((selected.story, selected.range(), selected.cell), (sid, a..b, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_first_change_stops_after_first_matching_story_and_repairs_selection() {
|
||||
for (texts, first, explicit, grep, expected, count) in [
|
||||
(vec!["red red", "red"], Some(true), false, false, vec!["blue red", "red"], 1),
|
||||
(vec!["keep", "red red", "red"], Some(true), false, false, vec!["keep", "blue red", "red"], 1),
|
||||
(vec!["keep", "other"], Some(true), false, false, vec!["keep", "other"], 0),
|
||||
(vec!["red red", "red"], Some(false), false, false, vec!["blue blue", "blue"], 3),
|
||||
(vec!["red red", "red"], None, false, false, vec!["blue blue", "blue"], 3),
|
||||
(vec!["red red", "red"], Some(true), true, false, vec!["blue red", "red"], 1),
|
||||
(vec!["red red", "red"], Some(false), true, false, vec!["blue blue", "red"], 2),
|
||||
(vec!["", "é🙂"], Some(true), false, true, vec!["X", "é🙂"], 1),
|
||||
(vec!["é", "🙂"], Some(false), false, true, vec!["XéX", "X🙂X"], 4),
|
||||
] {
|
||||
let (mut s, sid) = scope_session(texts[0]);
|
||||
let mut sids = vec![sid];
|
||||
for text in texts.iter().skip(1) {
|
||||
let frame = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": text})).unwrap();
|
||||
sids.push(designcraft_doc::StoryId(frame["story"].as_u64().unwrap()));
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc.stories.keys().copied().collect::<Vec<_>>(), sids);
|
||||
// Document Change keeps canonical target order even with a later story current.
|
||||
scope_select(&mut s, *sids.last().unwrap(), 0, 0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let mut p = json!({
|
||||
"find": if grep { "(?:)" } else { "red" },
|
||||
"change": if grep { "X" } else { "blue" },
|
||||
"scope": "document", "grep": grep
|
||||
});
|
||||
if let Some(first) = first {
|
||||
p["first"] = json!(first);
|
||||
}
|
||||
if explicit {
|
||||
p["story"] = json!(sid.0);
|
||||
p["scope"] = json!("selection");
|
||||
}
|
||||
if first == Some(true) && !explicit && !grep && texts.last() == Some(&"red") {
|
||||
let last = *sids.last().unwrap();
|
||||
scope_hit(&mut s, &p, last, 0, 3);
|
||||
let first_hit = sids[if texts.len() == 2 { 0 } else { 1 }];
|
||||
assert_eq!(
|
||||
s.execute("find.find", &p).unwrap(),
|
||||
json!([
|
||||
{"story": first_hit.0, "start": 0, "end": 3, "text": "red"},
|
||||
{"story": first_hit.0, "start": 4, "end": 7, "text": "red"},
|
||||
{"story": last.0, "start": 0, "end": 3, "text": "red"}
|
||||
]),
|
||||
"Count remains canonical after Next paints the later story"
|
||||
);
|
||||
}
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": count}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.selection, selection);
|
||||
for (i, sid) in sids.iter().enumerate() {
|
||||
assert_eq!(st.doc.story(*sid).unwrap().text, expected[i]);
|
||||
if texts[i] == expected[i] {
|
||||
assert_eq!(st.doc.story(*sid), before.story(*sid), "untouched story {i} stays entirely unchanged");
|
||||
}
|
||||
}
|
||||
st.doc.check().unwrap();
|
||||
}
|
||||
|
||||
// Identical replacement is still one counted match, not a reason to visit another story.
|
||||
let (mut s, sid) = scope_session("red red");
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "red"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, other_sid, 0, 3);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert_eq!(s.execute("find.change", &json!({"find": "red", "change": "red", "scope": "document", "first": true})).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid), before.story(other_sid));
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
|
||||
for reverse in [false, true] {
|
||||
let (mut s, sid) = scope_session("éX X");
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "X"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, sid, if reverse { 3 } else { 2 }, if reverse { 2 } else { 3 });
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let mut selection = s.doc().unwrap().selection.clone();
|
||||
let t = selection.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = if reverse { (6, 2) } else { (2, 6) };
|
||||
let p = json!({"find": "X", "change": "🙂", "scope": "document", "first": true});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 3);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, "é🙂 X");
|
||||
assert_eq!(st.doc.story(other_sid), before.story(other_sid));
|
||||
assert_eq!(st.selection, selection, "first-match exit must follow final UTF-8 repair");
|
||||
st.doc.check().unwrap();
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "🙂", "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid.0, "start": 2, "end": 6, "text": "🙂"}])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grep_in_the_documents_dialect() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "Москва and Paris, 1\u{2013}2 days.\nok"})).unwrap();
|
||||
// `\u` uppercase, `\l` lowercase letter.
|
||||
let m = s.execute("find.find", &json!({"find": r"\u\l+", "grep": true, "caseSensitive": true})).unwrap();
|
||||
let words: Vec<&str> = m.as_array().unwrap().iter().map(|x| x["text"].as_str().unwrap()).collect();
|
||||
assert_eq!(words, ["Москва", "Paris"]);
|
||||
// `~=` en dash, `\h` space, `\r` paragraph end; the change uses groups, `~S` and `\r`.
|
||||
let r = s.execute("find.change", &json!({"find": r"(\d)~=(\d)\h(\l+)\.\r", "change": r"$1~=$2~S$3!\r", "grep": true})).unwrap();
|
||||
assert_eq!(r["count"], 1);
|
||||
let sid = s.execute("document.inspect", &json!({})).unwrap()["stories"][0]["id"].as_u64().unwrap();
|
||||
let text = s.execute("story.get", &json!({"story": sid})).unwrap()["text"].as_str().unwrap().to_string();
|
||||
assert!(text.starts_with("Москва and Paris, 1\u{2013}2\u{A0}days!\nok"), "{text:?}");
|
||||
// A pattern that doesn't compile is an error, not a silent no-op.
|
||||
assert!(s.execute("find.find", &json!({"find": "[[:nope:]]", "grep": true})).is_err());
|
||||
assert!(s.execute("find.change", &json!({"find": "(", "change": "", "grep": true})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_exact_baseline_count_next_change() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let frame = s
|
||||
.execute(
|
||||
"frame.create",
|
||||
&json!({
|
||||
"rect": [36, 36, 300, 200], "content": "text", "text": "red red"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let sid = frame["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 3})).unwrap();
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "red", "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid, "start": 0, "end": 3, "text": "red"}])
|
||||
);
|
||||
assert_eq!(s.execute("find.next", &json!({"find": "red", "scope": "selection"})).unwrap(), json!({"story": sid, "start": 0, "end": 3}));
|
||||
assert_eq!(s.execute("find.change", &json!({"find": "red", "change": "blue", "scope": "selection"})).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.execute("story.get", &json!({"story": sid})).unwrap()["text"], "blue red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_growth_preserves_direction_and_full_scalar_hits() {
|
||||
for (text, find, change, changed, start, old_end, new_end) in [("a", "a", "é", "é", 0, 1, 2), ("éX", "X", "🙂", "é🙂", 2, 3, 6)] {
|
||||
for reverse in [false, true] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
// Real caret movement keeps the frame identity, unlike text.select.
|
||||
s.execute("text.move", &json!({"dir": "left", "extend": reverse})).unwrap();
|
||||
if !reverse {
|
||||
s.execute("text.move", &json!({"dir": "right", "extend": true})).unwrap();
|
||||
}
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
assert_eq!((selected.anchor, selected.focus), if reverse { (old_end, start) } else { (start, old_end) });
|
||||
assert!(selected.frame.is_some());
|
||||
let mut expected = s.doc().unwrap().selection.clone();
|
||||
let t = expected.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = if reverse { (new_end, start) } else { (start, new_end) };
|
||||
let p = json!({"find": find, "change": change, "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": start, "end": old_end, "text": find}]));
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, changed);
|
||||
assert_eq!(st.selection, expected);
|
||||
let t = st.selection.text.unwrap();
|
||||
assert!(changed.is_char_boundary(t.anchor) && changed.is_char_boundary(t.focus));
|
||||
assert!(s.find_context.is_none());
|
||||
let p = json!({"find": change, "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": start, "end": new_end, "text": change}]));
|
||||
scope_hit(&mut s, &p, sid, start, new_end);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_repairs_final_boundaries_without_remapping_valid_offsets() {
|
||||
for (text, (a, b), find, change, changed, (after_a, after_b), count) in [
|
||||
("aX", (1, 1), "a", "é", "éX", (0, 0), 1), // A caret stays collapsed and floors.
|
||||
("aX", (2, 2), "a", "é", "éX", (2, 2), 1), // A valid caret stays at its byte offset.
|
||||
("abc", (1, 2), "abc", "🙂", "🙂", (0, 4), 1), // Both endpoints need repair.
|
||||
("abc", (2, 1), "abc", "🙂", "🙂", (4, 0), 1),
|
||||
("é🙂", (2, 6), "🙂", "X", "éX", (2, 3), 1), // Existing shortening behavior.
|
||||
("é🙂", (6, 2), "🙂", "X", "éX", (3, 2), 1),
|
||||
("red", (0, 3), "red", "blue", "blue", (0, 3), 1), // Do not extend a valid upper bound.
|
||||
("aX", (1, 2), "a", "zz", "zzX", (1, 2), 1), // Do not track the original X.
|
||||
("aaX", (1, 2), "a", "é", "ééX", (0, 2), 2), // Per-edit repair would wrongly yield 0..4.
|
||||
("aaX", (2, 3), "a", "é", "ééX", (2, 4), 2),
|
||||
("abc", (1, 3), "abc", "", "", (0, 0), 1),
|
||||
("abc", (2, 3), "abc", "é", "é", (2, 2), 1), // EOF clamp may collapse a nonempty range.
|
||||
] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
scope_select(&mut s, sid, a, b);
|
||||
let mut expected = s.doc().unwrap().selection.clone();
|
||||
let t = expected.text.as_mut().unwrap();
|
||||
(t.anchor, t.focus) = (after_a, after_b);
|
||||
assert_eq!(s.execute("find.change", &json!({"find": find, "change": change, "scope": "document"})).unwrap(), json!({"count": count}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.story(sid).unwrap().text, changed, "{text} {a}->{b}");
|
||||
assert_eq!(st.selection, expected, "{text} {a}->{b}");
|
||||
assert!(changed.is_char_boundary(after_a) && changed.is_char_boundary(after_b));
|
||||
if after_a == after_b {
|
||||
assert_eq!(
|
||||
s.execute("find.find", &json!({"find": "(?:)", "grep": true, "scope": "selection"})).unwrap(),
|
||||
json!([{"story": sid.0, "start": after_a, "end": after_a, "text": ""}])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_change_other_story_and_no_hits_preserve_selection() {
|
||||
for explicit in [false, true] {
|
||||
let (mut s, sid) = scope_session("éX");
|
||||
let frame = s.doc().unwrap().selection.text.unwrap().frame.unwrap();
|
||||
let other = s.execute("frame.create", &json!({"rect": [120, 36, 200, 100], "content": "text", "text": "a"})).unwrap();
|
||||
let other_sid = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, sid, 3, 2);
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
let mut p = json!({"find": "a", "change": "🙂", "scope": "document"});
|
||||
if explicit {
|
||||
p["story"] = json!(other_sid.0);
|
||||
}
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "🙂");
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 0}));
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "🙂");
|
||||
|
||||
s.execute("selection.set", &json!({"ids": [frame.0]})).unwrap();
|
||||
let before = s.doc().unwrap().selection.clone();
|
||||
assert!(before.text.is_none());
|
||||
p["find"] = json!("🙂");
|
||||
p["change"] = json!("é");
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().selection, before);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "éX");
|
||||
assert_eq!(s.doc().unwrap().doc.story(other_sid).unwrap().text, "é");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_mixed_grep_matches_count_next_and_change() {
|
||||
for (text, pattern, expected, changed) in [
|
||||
("a", "a*", vec![(0, 1)], "X"),
|
||||
("aa", "a|(?:)", vec![(0, 1), (1, 2)], "XX"),
|
||||
("ab", "a*", vec![(0, 1), (2, 2)], "XbX"),
|
||||
("éa", "é|(?:)", vec![(0, 2), (3, 3)], "XaX"),
|
||||
] {
|
||||
let (mut s, sid) = scope_session(text);
|
||||
scope_select(&mut s, sid, 0, text.len());
|
||||
let p = json!({"find": pattern, "grep": true, "scope": "selection", "change": "X"});
|
||||
let counted = s.execute("find.find", &p).unwrap();
|
||||
let ranges: Vec<_> =
|
||||
counted.as_array().unwrap().iter().map(|m| (m["start"].as_u64().unwrap() as usize, m["end"].as_u64().unwrap() as usize)).collect();
|
||||
assert_eq!(ranges, expected, "{pattern} on {text}");
|
||||
for (a, b) in &expected {
|
||||
scope_hit(&mut s, &p, sid, *a, *b);
|
||||
}
|
||||
for _ in 0..2 {
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null, "{pattern} on {text}");
|
||||
}
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), counted);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], expected.len());
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, changed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_capture_replacement_and_first_keep_original_domain() {
|
||||
let (mut s, sid) = scope_session("a1 a2 | a3");
|
||||
scope_select(&mut s, sid, 0, 5);
|
||||
let p = json!({"find": r"a(\d)", "grep": true, "scope": "selection", "change": "b$1", "first": true});
|
||||
scope_hit(&mut s, &p, sid, 0, 2);
|
||||
scope_hit(&mut s, &p, sid, 3, 5);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "b1 a2 | a3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_selection_retains_nonempty_hits_count_change_and_native_text() {
|
||||
for (a, b) in [(0, 7), (7, 0)] {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, a, b);
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection", "attrs": {"fontStyle": "Italic"}});
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let strong = std::sync::Arc::strong_count(&before);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
assert_eq!(s.doc().unwrap().revision, revision);
|
||||
assert_eq!(std::sync::Arc::strong_count(&before), strong, "search stores weak document identity");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
scope_hit(&mut s, &p, sid, 4, 7);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 2);
|
||||
let changed = s.doc().unwrap().doc.story(sid).unwrap().clone();
|
||||
assert_eq!(changed.text, "blue blue | red");
|
||||
assert_eq!(changed.char_format_at(0).over.font_style.as_deref(), Some("Italic"));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history + 1);
|
||||
assert!(s.find_context.is_none());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
assert!(s.find_context.is_none());
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap(), &changed);
|
||||
let native = s.execute("file.serialize", &json!({})).unwrap();
|
||||
s.execute("file.openBytes", &json!({"name": "scope.designcraft", "base64": native["base64"]})).unwrap();
|
||||
assert_eq!(serde_json::to_value(s.doc().unwrap().doc.story(sid).unwrap()).unwrap(), serde_json::to_value(&changed).unwrap());
|
||||
assert!(s.find_context.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_matcher_restart_replacement_continuation_and_external_reselection() {
|
||||
let (mut s, sid) = scope_session("red blue red | blue red");
|
||||
scope_select(&mut s, sid, 0, 12);
|
||||
let mut p = json!({"find": "red", "scope": "selection", "change": "one"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["change"] = json!("two");
|
||||
p["attrs"] = json!({"fontStyle": "Italic"});
|
||||
scope_hit(&mut s, &p, sid, 9, 12);
|
||||
p["find"] = json!("blue");
|
||||
scope_hit(&mut s, &p, sid, 4, 8);
|
||||
p["find"] = json!("red");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["caseSensitive"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["grep"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3); // GREP alone changes matcher identity.
|
||||
p["find"] = json!("r.d");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
p["wholeWord"] = json!(true);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let hit = s.doc().unwrap().selection.text.unwrap();
|
||||
scope_select(&mut s, sid, hit.anchor, hit.focus);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1, "same-range external reselection starts a new domain");
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
scope_select(&mut s, sid, 20, 23);
|
||||
p["find"] = json!("red");
|
||||
scope_hit(&mut s, &p, sid, 20, 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_word_reselection_at_same_hit_recaptures_bounds() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let selected = s.doc().unwrap().selection.clone();
|
||||
let frame = selected.text.unwrap().frame.unwrap();
|
||||
let point = {
|
||||
let st = s.doc().unwrap();
|
||||
let cs = s.cache.get(&st.doc, sid, None);
|
||||
let (fi, x, baseline, ascent, _) = designcraft_compose::caret(&cs, 1).unwrap();
|
||||
let inner = designcraft_geom::Point::new(x, baseline - ascent * 0.5);
|
||||
assert_eq!(cs.frames[fi].frame, frame);
|
||||
assert_eq!(designcraft_compose::hit(&cs, fi, inner), Some(1), "real point lies inside the current hit");
|
||||
let loc = st.doc.find(frame).unwrap();
|
||||
st.doc.parent_xf(&loc) * st.doc.text_xf(st.doc.item(frame).unwrap()) * inner
|
||||
};
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
s.execute("text.selectWord", &json!({"frame": frame.0, "point": [point.x, point.y]})).unwrap();
|
||||
assert!(!s.text_drag, "explicit word reselection does not start a text drag");
|
||||
assert_eq!(s.doc().unwrap().selection, selected, "same full selection, including frame");
|
||||
assert!(s.doc().unwrap().revision > revision, "a new revision");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.execute("find.find", &p).unwrap(), json!([{"story": sid.0, "start": 0, "end": 3, "text": "red"}]));
|
||||
assert_eq!(s.execute("find.change", &p).unwrap(), json!({"count": 1}));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "blue red | red");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "blue red | red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_story_explicit_story_document_order_and_scope_changes() {
|
||||
let (mut s, a) = scope_session("black red");
|
||||
let other = s.execute("frame.create", &json!({"rect": [150, 36, 100, 100], "content": "text", "text": "red"})).unwrap();
|
||||
let b = designcraft_doc::StoryId(other["story"].as_u64().unwrap());
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
let story = json!({"find": "red", "scope": "story"});
|
||||
assert_eq!(s.execute("find.next", &story).unwrap(), serde_json::Value::Null, "Story does not enter B or wrap to its prefix");
|
||||
scope_select(&mut s, b, 0, 0);
|
||||
let explicit = json!({"find": "black", "story": a.0, "scope": "selection"});
|
||||
scope_hit(&mut s, &explicit, a, 0, 5);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
assert!(s.execute("find.next", &json!({"find": "red", "story": u64::MAX})).is_err());
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
scope_hit(&mut s, &json!({"find": "red"}), b, 0, 3);
|
||||
let counted = s.execute("find.find", &json!({"find": "red"})).unwrap();
|
||||
assert_eq!(counted[0]["story"], a.0, "Count retains canonical story order");
|
||||
assert_eq!(counted[1]["story"], b.0);
|
||||
scope_select(&mut s, a, 0, 9);
|
||||
scope_hit(&mut s, &json!({"find": "red", "scope": "selection"}), a, 6, 9);
|
||||
assert_eq!(s.execute("find.next", &story).unwrap(), serde_json::Value::Null, "scope change uses the current caret");
|
||||
s.execute("selection.set", &json!({"ids": []})).unwrap();
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
assert!(s.execute(command, &json!({"find": "red", "change": "blue", "scope": "selection"})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_substring_context_utf8_zero_width_and_invalid_ranges() {
|
||||
let (mut s, sid) = scope_session("scat é🙂 red");
|
||||
scope_select(&mut s, sid, 1, 4);
|
||||
let p = json!({"find": "cat", "wholeWord": true, "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 1, 4);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
scope_select(&mut s, sid, 1, 4);
|
||||
let p = json!({"find": "^c|^a", "grep": true, "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 1, 2);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null, "cursor does not redefine ^");
|
||||
scope_select(&mut s, sid, 5, 11);
|
||||
let empty = json!({"find": "(?:)", "grep": true, "scope": "selection"});
|
||||
for pos in [5, 7, 11] {
|
||||
scope_hit(&mut s, &empty, sid, pos, pos);
|
||||
}
|
||||
for _ in 0..2 {
|
||||
assert_eq!(s.execute("find.next", &empty).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
assert_eq!(s.execute("find.find", &empty).unwrap().as_array().unwrap().len(), 3);
|
||||
scope_select(&mut s, sid, 7, 7);
|
||||
scope_hit(&mut s, &empty, sid, 7, 7);
|
||||
assert_eq!(s.execute("find.next", &empty).unwrap(), serde_json::Value::Null);
|
||||
scope_select(&mut s, sid, 12, 15);
|
||||
let nonempty = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
scope_hit(&mut s, &nonempty, sid, 12, 15);
|
||||
assert_eq!(s.execute("find.change", &nonempty).unwrap()["count"], 1);
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "scat é🙂 blue");
|
||||
for range in [6..7, 0..usize::MAX] {
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.selection =
|
||||
designcraft_doc::Selection::text(designcraft_doc::TextSel { story: sid, anchor: range.start, focus: range.end, frame: None, cell: None });
|
||||
st.bump_revision();
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let err = s.execute(command, &json!({"find": "x", "change": "y", "scope": "selection"})).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_invalid_query_keeps_continuation_and_cross_boundary_nonmatches() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let selected = s.doc().unwrap().selection.clone();
|
||||
assert!(s.execute("find.next", &json!({"find": "[", "grep": true, "scope": "selection"})).is_err());
|
||||
assert_eq!(s.doc().unwrap().selection, selected);
|
||||
scope_hit(&mut s, &p, sid, 4, 7);
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "red |", "scope": "selection"})).unwrap(), json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_document_switch_edit_and_reset() {
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.set_active(0);
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("story.setText", &json!({"story": sid.0, "text": "red"})).unwrap();
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1);
|
||||
s.reset_find_scope();
|
||||
assert!(s.find_context.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_real_preview_restores_before_resolving_bounds() {
|
||||
let (mut s, sid) = scope_session("");
|
||||
s.execute(
|
||||
"data.source.select",
|
||||
&json!({
|
||||
"bytes": crate::cmd::base64_encode(b"x\nred red | red\n"), "name": "scope.csv"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
s.execute("data.placeholder.add", &json!({"field": "x", "story": sid.0, "at": 0})).unwrap();
|
||||
let template = s.doc().unwrap().doc.clone();
|
||||
assert_eq!(template.story(sid).unwrap().text, "<<x>>");
|
||||
s.execute("data.preview", &json!({"record": 1})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
// Install a viewport selection in the real preview. text.select is journaled
|
||||
// and would itself restore the template before selecting.
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.selection = designcraft_doc::Selection::text(designcraft_doc::TextSel { story: sid, anchor: 0, focus: 7, frame: None, cell: None });
|
||||
st.bump_revision();
|
||||
let p = json!({"find": "red", "change": "blue", "scope": "selection"});
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 2);
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let err = s.execute("find.change", &p).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(std::sync::Arc::ptr_eq(&template, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "<<x>>");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert!(s.doc().unwrap().preview_stash.is_none());
|
||||
assert!(s.find_context.is_none());
|
||||
// Valid fresh bounds are allowed after restoration; no cached preview range
|
||||
// survives. A document query can see the preview, but Next sees the template.
|
||||
s.execute("data.preview", &json!({"record": 1})).unwrap();
|
||||
let document = json!({"find": "red"});
|
||||
assert_eq!(s.execute("find.find", &document).unwrap().as_array().unwrap().len(), 3);
|
||||
assert_eq!(s.execute("find.next", &document).unwrap(), serde_json::Value::Null);
|
||||
assert!(std::sync::Arc::ptr_eq(&template, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.execute("find.find", &document).unwrap(), json!([]));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_scope_searches_and_changes_table_cells() {
|
||||
let (mut s, sid) = scope_session("Ore 12.15 here");
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
let table = s.execute("table.insert", &json!({"rows": 1, "cols": 2})).unwrap()["table"].as_u64().unwrap();
|
||||
s.execute("table.setCell", &json!({"row": 0, "col": 1, "text": "Ore 12.15"})).unwrap();
|
||||
// The caret sits in the first cell, so Find Next goes to the second cell, then the story.
|
||||
let p = json!({"find": "Ore 12.15", "scope": "document"});
|
||||
let cell = designcraft_doc::CellAddr { table, row: 0, col: 1 };
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), json!({"story": sid.0, "start": 0, "end": 9, "cell": {"table": table, "row": 0, "col": 1}}));
|
||||
assert_eq!(s.doc().unwrap().selection.text.map(|t| (t.story, t.range(), t.cell)), Some((sid, 0..9, Some(cell))));
|
||||
let next = s.execute("find.next", &p).unwrap();
|
||||
assert!(next.get("start").is_some() && next.get("cell").is_none(), "{next}");
|
||||
let hits = s.execute("find.find", &p).unwrap();
|
||||
let hits = hits.as_array().unwrap();
|
||||
assert_eq!(hits.len(), 2);
|
||||
let in_cell: Vec<_> = hits.iter().filter(|h| h.get("cell").is_some()).collect();
|
||||
assert_eq!(in_cell.len(), 1);
|
||||
assert_eq!((in_cell[0]["cell"]["row"].as_u64(), in_cell[0]["cell"]["col"].as_u64()), (Some(0), Some(1)));
|
||||
let n = s.execute("find.change", &json!({"find": "Ore 12.15", "change": "Ore 13.00", "scope": "document"})).unwrap();
|
||||
assert_eq!(n["count"], 2);
|
||||
assert!(s.execute("find.find", &json!({"find": "12.15", "scope": "document"})).unwrap().as_array().unwrap().is_empty());
|
||||
assert_eq!(s.execute("find.find", &json!({"find": "13.00", "scope": "document"})).unwrap().as_array().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
fn child_scope_session(note: bool, child: &str) -> (Session, designcraft_doc::StoryId, designcraft_doc::TextSel) {
|
||||
let (mut s, sid) = scope_session("red");
|
||||
scope_select(&mut s, sid, 3, 3);
|
||||
if note {
|
||||
s.execute("footnote.insert", &json!({"text": child})).unwrap();
|
||||
} else {
|
||||
s.execute("table.insert", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": child, "raw": true})).unwrap();
|
||||
}
|
||||
let selected = s.doc().unwrap().selection.text.unwrap();
|
||||
(s, sid, selected)
|
||||
}
|
||||
|
||||
/// Child-story scopes are rejected by every find command, leaving the document untouched.
|
||||
fn assert_child_scope_requests_rejected(s: &mut Session) {
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let history = s.doc().unwrap().history.undo.len();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
let journal = s.journal.clone();
|
||||
for scope in ["story", "selection"] {
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
let err = s.execute(command, &json!({"find": "red", "change": "blue", "scope": scope})).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::BadParams { .. }), "{err}");
|
||||
assert!(err.to_string().contains("not supported"));
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), history);
|
||||
assert_eq!(s.doc().unwrap().revision, revision);
|
||||
assert_eq!(s.journal, journal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_children_error_atomically_and_parent_requests_stay_top_level() {
|
||||
for note in [false, true] {
|
||||
for child in ["red", "red x", "é red red red red red"] {
|
||||
let (mut s, sid, mut selected) = child_scope_session(note, child);
|
||||
selected.anchor = 0;
|
||||
selected.focus = child.len();
|
||||
s.doc_mut().unwrap().selection = designcraft_doc::Selection::text(selected);
|
||||
assert_child_scope_requests_rejected(&mut s);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
for explicit in [false, true] {
|
||||
let mut p = json!({"find": "red", "change": "blue"});
|
||||
if explicit {
|
||||
p["story"] = json!(sid.0);
|
||||
p["scope"] = json!("selection");
|
||||
}
|
||||
// Document scope also covers table cells (#1624); an explicit story and footnotes stay top-level.
|
||||
let in_cell = if explicit || note { 0 } else { child.matches("red").count() };
|
||||
let changed = if in_cell > 0 { child.replace("red", "blue") } else { child.to_string() };
|
||||
s.doc_mut().unwrap().selection = selection.clone();
|
||||
assert_eq!(s.execute("find.find", &p).unwrap().as_array().unwrap().len(), 1 + in_cell);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.doc_mut().unwrap().selection = selection.clone();
|
||||
assert_eq!(s.execute("find.change", &p).unwrap()["count"], 1 + in_cell);
|
||||
assert_eq!(s.doc().unwrap().doc.text_story(sid, selected.cell).unwrap().text, changed);
|
||||
assert_eq!(s.doc().unwrap().selection, selection, "parent replacement does not clamp child offsets");
|
||||
p["find"] = json!("blue");
|
||||
p["change"] = json!("red");
|
||||
s.execute("find.change", &p).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut s, _, _) = child_scope_session(false, "red");
|
||||
s.execute("table.selectTable", &json!({})).unwrap();
|
||||
for scope in ["story", "selection"] {
|
||||
for command in ["find.find", "find.next", "find.change"] {
|
||||
assert!(s.execute(command, &json!({"find": "red", "change": "blue", "scope": scope})).unwrap_err().to_string().contains("not supported"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_threaded_overset_story_uses_logical_offsets() {
|
||||
let text = format!("{}red", "word ".repeat(100));
|
||||
let (mut s, sid) = scope_session(&text);
|
||||
let a = s.doc().unwrap().doc.story(sid).unwrap().frames[0];
|
||||
let next = s.execute("frame.create", &json!({"rect": [150, 36, 20, 1], "content": "text"})).unwrap();
|
||||
let b = designcraft_doc::ItemId(next["id"].as_u64().unwrap());
|
||||
s.edit(|d, _| Ok(d.thread(a, b)?)).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().frames.len(), 2);
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
scope_hit(&mut s, &json!({"find": "red", "scope": "story"}), sid, 500, 503);
|
||||
assert_eq!(s.execute("find.next", &json!({"find": "red", "scope": "story"})).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_scope_zero_width_story_explicit_and_document_endpoints_terminate() {
|
||||
for scope in ["story", "explicit", "document"] {
|
||||
let (mut s, sid) = scope_session("é🙂");
|
||||
scope_select(&mut s, sid, 0, 0);
|
||||
let mut p = json!({"find": "(?:)", "grep": true, "scope": scope});
|
||||
if scope == "explicit" {
|
||||
p["story"] = json!(sid.0);
|
||||
}
|
||||
for pos in [0, 2, 6] {
|
||||
scope_hit(&mut s, &p, sid, pos, pos);
|
||||
}
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
assert_eq!(s.execute("find.next", &p).unwrap(), serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn find_scope_revert_releases_context_with_same_document_uid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("scope.designcraft");
|
||||
let (mut s, sid) = scope_session("red red | red");
|
||||
s.execute("file.save", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
let p = json!({"find": "red", "scope": "selection"});
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
let uid = s.doc().unwrap().uid;
|
||||
s.execute("story.setText", &json!({"story": sid.0, "text": "red red"})).unwrap();
|
||||
scope_select(&mut s, sid, 0, 7);
|
||||
scope_hit(&mut s, &p, sid, 0, 3);
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().uid, uid);
|
||||
assert!(s.find_context.is_none());
|
||||
assert_eq!(s.doc().unwrap().doc.story(sid).unwrap().text, "red red | red");
|
||||
assert!(s.execute("find.find", &p).is_err(), "revert's empty selection cannot inherit a previous range");
|
||||
}
|
||||
Reference in New Issue
Block a user