Skip to main content

ravenrs_sheet/engine/cmd/edit/
go_to.rs

1use super::selection::{select_region, selection_set};
2use super::{CellRef, EngineError, Json, RangeRef, Result, Session, bad, bool_param, coalesce, json, split_sheet, str_param};
3
4/// How many references the Go To dialog keeps for going back to.
5pub(super) const GO_TO_RECENT: usize = 10;
6
7/// The most areas one Go To selects.
8pub(super) const GO_TO_MAX_AREAS: usize = 2000;
9
10/// How deep a name that refers to other names is followed.
11const 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        // One selection holds the areas of one sheet.
28        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
52/// The parts of a Go To reference, split at the commas outside quoted sheet names and
53/// parentheses: `Price, Type` or `B14:C22,F19:G30`. The parts are read one at a time, so a caller
54/// that stops at an invalid part or at `GO_TO_MAX_AREAS` does not split the rest of the text.
55pub(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
80/// Where one part of a Go To reference points, seen from sheet `sheet`: the sheet, its areas,
81/// and whether a defined name or a table gave them. `Sheet2!Name` is the name scoped to Sheet2.
82/// A name counts when what it refers to is a reference; a constant or a formula is not one.
83fn 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        // As in Excel, a table's name stands for its data, without the header and totals rows.
106        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
112/// The sheet and areas defined name `n` refers to, seen from sheet `sheet`, when that is a
113/// reference.
114fn 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
133/// The names the Go To dialog lists, seen from sheet `sheet`: the named ranges of the workbook and
134/// of every sheet, in alphabetical order. A name scoped to another sheet is written `Sheet2!Name`,
135/// and the active sheet's own name hides the workbook name it shares. Hidden names and names that
136/// do not refer to a range are left out.
137pub 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}