refactor(engine): split find change and used fonts (Sonar S3776)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Dylan M. Taylor
2026-10-24 08:50:41 +00:00
co-authored by Claude Sonnet 5.5
parent 8299d26400
commit 88dd71cc51
2 changed files with 58 additions and 43 deletions
+27 -17
View File
@@ -323,18 +323,38 @@ fn replacement(p: &Value) -> String {
if bool_or(p, "grep", false) { translate_replacement(c) } else { metachars(c) }
}
/// Repair byte validity of a text selection once against the final story, without remapping valid offsets.
fn repair_selection(t: &mut designcraft_doc::TextSel, st: &designcraft_doc::Story) {
let range = t.range();
let start = floor_char_boundary(&st.text, range.start);
let mut end = range.end.min(st.len());
if t.is_caret() {
end = start;
} else if !st.text.is_char_boundary(end) {
let floor = floor_char_boundary(&st.text, end);
end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8());
}
(t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) };
}
/// The character overrides a Change applies to every replacement, from its `attrs` object.
fn char_overrides(attrs: Option<&Value>) -> Result<designcraft_doc::CharAttrs> {
let mut over = designcraft_doc::CharAttrs::default();
if let Some(o) = attrs.and_then(Value::as_object) {
for (k, v) in o {
over.set_json(k, v).map_err(|e| bad("find.change", e))?;
}
}
Ok(over)
}
fn change(s: &mut Session, p: &Value) -> Result<Value> {
let re = build(p)?;
let grep = bool_or(p, "grep", false);
let rep = replacement(p);
let first_only = bool_or(p, "first", false);
let attrs = p.get("attrs").cloned();
let mut over = designcraft_doc::CharAttrs::default();
if let Some(o) = attrs.as_ref().and_then(Value::as_object) {
for (k, v) in o {
over.set_json(k, v).map_err(|e| bad("find.change", e))?;
}
}
let over = char_overrides(attrs.as_ref())?;
super::text::validate_char_fill(&s.doc()?.doc, &over, "find.change")?;
let targets = scope(s, p)?.targets;
s.edit(|d, sel| {
@@ -361,17 +381,7 @@ fn change(s: &mut Session, p: &Value) -> Result<Value> {
count += 1;
}
if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell == cell) {
// Repair byte validity once against the final story, without remapping valid offsets.
let range = t.range();
let start = floor_char_boundary(&st.text, range.start);
let mut end = range.end.min(st.len());
if t.is_caret() {
end = start;
} else if !st.text.is_char_boundary(end) {
let floor = floor_char_boundary(&st.text, end);
end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8());
}
(t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) };
repair_selection(t, st);
}
// `first` is the first match of the whole search, not of each story.
if first_only && count > 0 {
+31 -26
View File
@@ -81,36 +81,41 @@ fn list(d: &Document) -> Vec<Value> {
used_fonts(d).into_iter().map(|font| json!(font)).collect()
}
pub(super) fn used_fonts(d: &Document) -> Vec<UsedFont> {
let db = designcraft_fonts::FontDb::global().scoped(d.font_scope);
let mut used: BTreeMap<(String, String), (usize, std::collections::BTreeSet<char>)> = BTreeMap::new();
for_each_story(d, &mut |st| {
let ranges = st.para_ranges();
for (pi, r) in ranges.iter().enumerate() {
let (_, base) = d.styles.resolve_para(&st.paras[pi]);
for (rr, fmt) in st.runs() {
let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count();
if n == 0 && !(r.is_empty() && rr.contains(&r.start)) {
continue;
}
let p = d.styles.resolve_char(&base, fmt);
if let Some(f) = d.styles.composite_font(&p.font_family) {
let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("");
for c in text.chars() {
if let Some(e) = f.entry(c) {
let entry = used.entry((e.family.clone(), e.style.clone())).or_default();
entry.0 += 1;
entry.1.insert(c);
}
type FontTally = BTreeMap<(String, String), (usize, std::collections::BTreeSet<char>)>;
/// Count the characters of every run of one story against the font each resolves to.
fn tally_story(d: &Document, st: &Story, used: &mut FontTally) {
let ranges = st.para_ranges();
for (pi, r) in ranges.iter().enumerate() {
let (_, base) = d.styles.resolve_para(&st.paras[pi]);
for (rr, fmt) in st.runs() {
let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count();
if n == 0 && !(r.is_empty() && rr.contains(&r.start)) {
continue;
}
let p = d.styles.resolve_char(&base, fmt);
if let Some(f) = d.styles.composite_font(&p.font_family) {
let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("");
for c in text.chars() {
if let Some(e) = f.entry(c) {
let entry = used.entry((e.family.clone(), e.style.clone())).or_default();
entry.0 += 1;
entry.1.insert(c);
}
} else {
let entry = used.entry((p.font_family, p.font_style)).or_default();
entry.0 += n;
entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars());
}
} else {
let entry = used.entry((p.font_family, p.font_style)).or_default();
entry.0 += n;
entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars());
}
}
});
}
}
pub(super) fn used_fonts(d: &Document) -> Vec<UsedFont> {
let db = designcraft_fonts::FontDb::global().scoped(d.font_scope);
let mut used = FontTally::new();
for_each_story(d, &mut |st| tally_story(d, st, &mut used));
let mut out: Vec<UsedFont> = used
.into_iter()
.map(|((family, style), (n, characters))| {