ravenrs_sheet/engine/cmd/edit/
go_to.rs1use super::selection::{select_region, selection_set};
2use super::{CellRef, EngineError, Json, RangeRef, Result, Session, bad, bool_param, coalesce, json, split_sheet, str_param};
3
4pub(super) const GO_TO_RECENT: usize = 10;
6
7pub(super) const GO_TO_MAX_AREAS: usize = 2000;
9
10const GO_TO_NAME_DEPTH: usize = 8;
12
13pub(in super::super) fn go_to(s: &mut Session, p: &Json) -> Result<Json> {
14 if bool_param(p, "list") == Some(true) {
15 let d = s.doc()?;
16 return Ok(json!({"names": go_to_names(&d.wb, d.wb.active_sheet()), "recent": d.go_to_recent}));
17 }
18 let Some(reference) = str_param(p, "reference").or_else(|| str_param(p, "range")) else { return Err(bad("edit.goTo", "missing `reference`")) };
19 let d = s.doc()?;
20 let mut target: Option<usize> = None;
21 let mut areas: Vec<RangeRef> = Vec::new();
22 let mut named = false;
23 for part in reference_parts(reference) {
24 let Some((sheet, found, by_name)) = go_to_target(&d.wb, d.wb.active_sheet(), part, 0) else {
25 return Err(EngineError::Other("Reference isn't valid.".into()));
26 };
27 if target.is_some_and(|t| t != sheet) {
29 return Err(EngineError::Other("Reference isn't valid.".into()));
30 }
31 target = Some(sheet);
32 named |= by_name;
33 areas.extend(found);
34 if areas.len() > GO_TO_MAX_AREAS {
35 return Err(EngineError::Other(format!("Go To selects at most {GO_TO_MAX_AREAS} areas.")));
36 }
37 }
38 let Some(sheet) = target else { return Err(EngineError::Other("Reference isn't valid.".into())) };
39 let range = areas.iter().map(RangeRef::a1).collect::<Vec<_>>().join(",");
40 let out = selection_set(s, &json!({"sheet": sheet, "range": range}))?;
41 if bool_param(p, "record") == Some(true) && !named {
42 let d = s.doc_mut()?;
43 let prefix = crate::base::quote_sheet(&d.wb.sheet(sheet).map(|sh| sh.name.clone()).unwrap_or_default());
44 let entry = areas.iter().map(|r| format!("{prefix}!{}", r.a1())).collect::<Vec<_>>().join(",");
45 d.go_to_recent.retain(|r| !r.eq_ignore_ascii_case(&entry));
46 d.go_to_recent.insert(0, entry);
47 d.go_to_recent.truncate(GO_TO_RECENT);
48 }
49 Ok(out)
50}
51
52pub(super) fn reference_parts(text: &str) -> impl Iterator<Item = &str> {
56 let (mut depth, mut quoted, mut start, mut done) = (0usize, false, 0, false);
57 let mut chars = text.char_indices();
58 std::iter::from_fn(move || {
59 if done {
60 return None;
61 }
62 for (i, c) in chars.by_ref() {
63 match c {
64 '\'' => quoted = !quoted,
65 '(' if !quoted => depth += 1,
66 ')' if !quoted => depth = depth.saturating_sub(1),
67 ',' if !quoted && depth == 0 => {
68 let part = text.get(start..i).unwrap_or("").trim();
69 start = i + 1;
70 return Some(part);
71 }
72 _ => {}
73 }
74 }
75 done = true;
76 Some(text.get(start..).unwrap_or("").trim())
77 })
78}
79
80fn go_to_target(wb: &crate::model::Workbook, sheet: usize, part: &str, depth: usize) -> Option<(usize, Vec<RangeRef>, bool)> {
84 if part.is_empty() || depth > GO_TO_NAME_DEPTH {
85 return None;
86 }
87 let (prefix, body) = split_sheet(part);
88 let on = match &prefix {
89 Some(n) => wb.sheet_index(n)?,
90 None => sheet,
91 };
92 if let Some(r) = RangeRef::parse(&body.replace('$', "")) {
93 return Some((on, vec![r], false));
94 }
95 let name = match prefix {
96 Some(_) => wb.names.iter().find(|n| n.scope == Some(on) && crate::formula::same_name(&n.name, body)),
97 None => wb.name(body, sheet),
98 };
99 if let Some(n) = name {
100 return name_areas(wb, n, sheet, depth).map(|(t, areas)| (t, areas, true));
101 }
102 if prefix.is_none()
103 && let Some((si, ti)) = wb.table(body)
104 {
105 let t = wb.sheet(si)?.tables.get(ti)?;
107 return Some((si, vec![t.data_range().unwrap_or(t.range)], true));
108 }
109 None
110}
111
112fn name_areas(wb: &crate::model::Workbook, n: &crate::model::DefinedName, sheet: usize, depth: usize) -> Option<(usize, Vec<RangeRef>)> {
115 let from = n.scope.unwrap_or(sheet);
116 let formula = n.formula.trim().trim_start_matches('=');
117 let mut target: Option<usize> = None;
118 let mut areas = Vec::new();
119 for p in reference_parts(formula) {
120 let (s, found, _) = go_to_target(wb, from, p, depth + 1)?;
121 if target.is_some_and(|t| t != s) {
122 return None;
123 }
124 target = Some(s);
125 areas.extend(found);
126 if areas.len() > GO_TO_MAX_AREAS {
127 return None;
128 }
129 }
130 target.map(|t| (t, areas))
131}
132
133pub fn go_to_names(wb: &crate::model::Workbook, sheet: usize) -> Vec<String> {
138 let own: std::collections::HashSet<String> =
139 wb.names.iter().filter(|n| n.scope == Some(sheet)).map(|n| crate::formula::fold_name(&n.name)).collect();
140 let mut out: Vec<String> = wb
141 .names
142 .iter()
143 .filter(|n| !n.hidden && name_areas(wb, n, sheet, 0).is_some())
144 .filter_map(|n| match n.scope {
145 None if own.contains(&crate::formula::fold_name(&n.name)) => None,
146 None => Some(n.name.clone()),
147 Some(s) if s == sheet => Some(n.name.clone()),
148 Some(s) => Some(format!("{}!{}", crate::base::quote_sheet(&wb.sheet(s)?.name), n.name)),
149 })
150 .collect();
151 out.sort_by_cached_key(|n| crate::formula::fold_name(n));
152 out.dedup_by(|a, b| crate::formula::same_name(a, b));
153 out
154}
155
156pub(super) fn go_to_special(s: &mut Session, p: &Json) -> Result<Json> {
157 let kind = str_param(p, "kind").unwrap_or("blanks");
158 let d = s.doc()?;
159 let Some(sheet) = d.wb.active() else { return Err(EngineError::NoDocument) };
160 let scope = if d.selection.is_single_cell() { sheet.used_range().unwrap_or(RangeRef::cell(d.selection.active)) } else { d.selection.bounds() };
161 if kind == "lastCell" {
162 let c = sheet.used_range().map(|r| r.end).unwrap_or_default();
163 return selection_set(s, &json!({"cell": c.a1()}));
164 }
165 if kind == "currentRegion" {
166 return select_region(s, &json!({}));
167 }
168 let mut hits: Vec<CellRef> = Vec::new();
169 if scope.count() > 4_000_000 {
170 return Err(bad("edit.goToSpecial", "range too large"));
171 }
172 for c in scope.iter() {
173 let cell = sheet.cell(c);
174 let v = sheet.value(c);
175 let ok = match kind {
176 "blanks" => v.is_empty(),
177 "constants" => cell.is_some_and(|x| x.formula.is_none() && x.py.is_none() && !x.value.is_empty()),
178 "formulas" => cell.is_some_and(|x| x.formula.is_some() || x.py.is_some()),
179 "comments" | "notes" => sheet.comments.contains_key(&c),
180 "errors" => v.is_error(),
181 "numbers" => v.is_number(),
182 "text" => v.is_text(),
183 "visible" => !sheet.is_row_hidden(c.row) && !sheet.is_col_hidden(c.col),
184 "conditionalFormats" => sheet.cond_formats.iter().any(|cf| cf.ranges.iter().any(|r| r.contains(c))),
185 "dataValidation" => sheet.validations.iter().any(|dv| dv.ranges.iter().any(|r| r.contains(c))),
186 _ => return Err(bad("edit.goToSpecial", format!("unknown kind `{kind}`"))),
187 };
188 if ok {
189 hits.push(c);
190 }
191 }
192 if hits.is_empty() {
193 return Err(EngineError::Other("No cells were found.".into()));
194 }
195 let ranges = coalesce(&hits);
196 let d = s.doc_mut()?;
197 d.selection = crate::engine::Selection { active: hits[0], anchor: hits[0], ranges };
198 Ok(json!({"selection": d.selection.a1(), "count": hits.len()}))
199}