mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-28 20:56:54 -06:00
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Formulas tab: AutoSum, Insert Function, names, auditing, calculation.
|
||||
|
||||
use gridcraft_core::{CellRef, RangeRef};
|
||||
use gridcraft_core::{CellRef, RangeRef, Value};
|
||||
use gridcraft_model::{CalcMode, DefinedName, Formula};
|
||||
use serde_json::{Value as Json, json};
|
||||
|
||||
@@ -49,6 +49,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
cmd!(query "formulas.tracePrecedents", "Trace Precedents", ["Formulas", "Formula Auditing"], None, "{cell?} → ranges", has_doc, trace_precedents),
|
||||
cmd!(query "formulas.traceDependents", "Trace Dependents", ["Formulas", "Formula Auditing"], None, "{cell?} → cells", has_doc, trace_dependents),
|
||||
cmd!(noundo "formulas.selectPrecedents", "Select Precedents", ["Formulas", "Formula Auditing"], Some("Ctrl+["), "{cell?, all?: true = every level (Ctrl+Shift+[)} → selects the precedent cells", has_doc, select_precedents),
|
||||
cmd!(noundo "formulas.selectDependents", "Select Dependents", ["Formulas", "Formula Auditing"], Some("Ctrl+]"), "{cell?, all?: true = every level (Ctrl+Shift+])}", has_doc, select_dependents),
|
||||
cmd!(noundo "formulas.removeArrows", "Remove Arrows", ["Formulas", "Formula Auditing"], None, "{}", has_doc, |_, _| ok()),
|
||||
cmd!("formulas.showFormulas", "Show Formulas", ["Formulas", "Formula Auditing"], Some("Ctrl+`"), "{on?}", has_doc, show_formulas),
|
||||
cmd!(query "formulas.errorChecking", "Error Checking", ["Formulas", "Formula Auditing"], None, "{} → cells with errors", has_doc, error_checking),
|
||||
@@ -479,6 +481,82 @@ fn trace_dependents(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
Ok(Json::Array(v))
|
||||
}
|
||||
|
||||
/// Excel's Ctrl+[ / Ctrl+]: select the precedent (or dependent) cells. When they all live on
|
||||
/// another sheet, jump there. `all` walks every level (Ctrl+Shift+[ / Ctrl+Shift+]).
|
||||
fn select_trace_cells(s: &mut Session, p: &Json, precedents: bool) -> Result<Json> {
|
||||
use std::collections::BTreeSet;
|
||||
let d = s.doc()?;
|
||||
let at = cell_param(s, p, "cell")?.unwrap_or(d.selection.active);
|
||||
let sheet = d.wb.active_sheet;
|
||||
let all = bool_param(p, "all").unwrap_or(false);
|
||||
// (sheet, a1) pairs to select; direct level keeps whole precedent ranges, transitive cells.
|
||||
let mut found: BTreeSet<(usize, String)> = BTreeSet::new();
|
||||
let mut seen: BTreeSet<(usize, CellRef)> = BTreeSet::new();
|
||||
let mut frontier = vec![(sheet, at)];
|
||||
seen.insert((sheet, at));
|
||||
while let Some((ks, kc)) = frontier.pop() {
|
||||
if precedents {
|
||||
for a in d.calc.graph.precedents_of((ks, kc)) {
|
||||
if all {
|
||||
// Walk only the stored cells inside the precedent area (sparse
|
||||
// range iterator, not the whole sheet): blanks are never selected
|
||||
// and only formula cells extend the frontier, so a formula late in
|
||||
// a large range is not skipped.
|
||||
let Some(sh) = d.wb.sheet(a.sheet) else { continue };
|
||||
for (c, cell) in sh.cells.iter_range(a.range) {
|
||||
if matches!(cell.value, Value::Empty) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert((a.sheet, c)) {
|
||||
found.insert((a.sheet, c.a1()));
|
||||
if cell.formula.is_some() {
|
||||
frontier.push((a.sheet, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
found.insert((a.sheet, a.range.a1()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut deps = Vec::new();
|
||||
d.calc.graph.dependents(ks, kc, &mut deps);
|
||||
for k2 in deps {
|
||||
if all {
|
||||
if seen.insert(k2) {
|
||||
found.insert((k2.0, k2.1.a1()));
|
||||
frontier.push(k2);
|
||||
}
|
||||
} else {
|
||||
found.insert((k2.0, k2.1.a1()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
found.remove(&(sheet, at.a1()));
|
||||
let name = if precedents { "precedent" } else { "dependent" };
|
||||
let Some(si) = found.iter().find(|(si, _)| *si == sheet).or_else(|| found.first()).map(|(si, _)| *si) else {
|
||||
return Err(bad("cell", format!("No {name} cells.")));
|
||||
};
|
||||
let list: Vec<String> = found.iter().filter(|(s, _)| *s == si).map(|(_, a)| a.clone()).collect();
|
||||
let mut params = json!({ "range": list.join(",") });
|
||||
if si != sheet {
|
||||
params["sheet"] = json!(si);
|
||||
}
|
||||
s.execute("selection.set", params)
|
||||
}
|
||||
|
||||
fn select_precedents(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
select_trace_cells(s, p, true)
|
||||
}
|
||||
|
||||
fn select_dependents(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
select_trace_cells(s, p, false)
|
||||
}
|
||||
|
||||
fn show_formulas(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let sheet = s.doc()?.wb.active_sheet;
|
||||
let cur = s.doc()?.wb.sheet(sheet).is_some_and(|sh| sh.show_formulas);
|
||||
|
||||
@@ -84,6 +84,11 @@ pub fn lookup(key: Key, physical: Option<Key>, m: Modifiers, mac: bool) -> Optio
|
||||
Semicolon if ctrl && s => Shortcut::Time,
|
||||
Quote if ctrl && s => Shortcut::ValueAbove,
|
||||
Quote if ctrl => Shortcut::FormulaAbove,
|
||||
// Ctrl+[ / Ctrl+] (Excel): select precedent / dependent cells; Shift walks every level.
|
||||
OpenBracket if ctrl && !s => run("formulas.selectPrecedents"),
|
||||
OpenBracket if ctrl => Shortcut::Run("formulas.selectPrecedents", json!({"all": true})),
|
||||
CloseBracket if ctrl && !s => run("formulas.selectDependents"),
|
||||
CloseBracket if ctrl => Shortcut::Run("formulas.selectDependents", json!({"all": true})),
|
||||
// Windows Ctrl+; is in `ribbon::shortcut`; Excel for Mac uses Control+; and Command+;.
|
||||
Semicolon if mac && ctrl => Shortcut::Date,
|
||||
Semicolon if mac && cmd && !s => Shortcut::Time,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Ctrl+[ / Ctrl+] select precedent and dependent cells (#1865).
|
||||
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::CellRef;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness(session: Session) -> Harness {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn active(h: &Harness) -> String {
|
||||
let d = h.state().session.active().unwrap();
|
||||
d.selection.active.a1()
|
||||
}
|
||||
|
||||
fn ranges(h: &Harness) -> Vec<String> {
|
||||
let d = h.state().session.active().unwrap();
|
||||
d.selection.ranges.iter().map(|r| r.a1()).collect()
|
||||
}
|
||||
|
||||
fn key(h: &mut Harness, k: Key, m: Modifiers) {
|
||||
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: true, repeat: false, modifiers: m });
|
||||
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: false, repeat: false, modifiers: m });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// A1 and B1 feed C1; C1 feeds D1.
|
||||
fn chain() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2, 3]]})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": "=A1+B1"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "D1", "input": "=C1*2"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_bracket_selects_precedents_and_dependents() {
|
||||
let mut h = harness(chain());
|
||||
h.state_mut().session.execute("selection.set", json!({"range": "C1"})).unwrap();
|
||||
key(&mut h, Key::OpenBracket, Modifiers::CTRL);
|
||||
let mut r = ranges(&h);
|
||||
r.sort();
|
||||
assert_eq!(r, vec!["A1".to_string(), "B1".to_string()], "Ctrl+[ selects the precedent cells");
|
||||
h.state_mut().session.execute("selection.set", json!({"range": "C1"})).unwrap();
|
||||
key(&mut h, Key::CloseBracket, Modifiers::CTRL);
|
||||
assert_eq!(active(&h), "D1", "Ctrl+] lands on the dependent cell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_shift_bracket_walks_every_level() {
|
||||
let mut h = harness(chain());
|
||||
h.state_mut().session.execute("selection.set", json!({"range": "D1"})).unwrap();
|
||||
key(&mut h, Key::OpenBracket, Modifiers::CTRL | Modifiers::SHIFT);
|
||||
let r = ranges(&h);
|
||||
assert!(
|
||||
r.contains(&"A1".to_string()) && r.contains(&"B1".to_string()) && r.contains(&"C1".to_string()),
|
||||
"Ctrl+Shift+[ walks back through C1 to A1:B1, got {r:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transitive_walk_reaches_formulas_late_in_a_wide_area() {
|
||||
// =SUM(A1:Z1) has a wide precedent area; a formula stored late in it must still be
|
||||
// walked, and blank positions must not be selected (#1865 review).
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("cell.set", json!({"cell": "Z1", "input": "5"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "AB1", "input": "=SUM(A1:Z1)"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "AD1", "input": "=AB1*2"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
h.state_mut().session.execute("selection.set", json!({"range": "AD1"})).unwrap();
|
||||
key(&mut h, Key::OpenBracket, Modifiers::CTRL | Modifiers::SHIFT);
|
||||
let r = ranges(&h);
|
||||
assert!(r.contains(&"Z1".to_string()), "the formula late in the area is reached, got {r:?}");
|
||||
assert_eq!(r.len(), 2, "only the stored cells (AB1, Z1) are selected, no blanks, got {r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_precedents_leaves_selection_untouched() {
|
||||
let mut h = harness(chain());
|
||||
h.state_mut().session.execute("selection.set", json!({"range": "A1"})).unwrap();
|
||||
key(&mut h, Key::OpenBracket, Modifiers::CTRL);
|
||||
assert_eq!(active(&h), "A1", "a plain entry cell has no precedents to select");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_sheet_precedents_jump_to_the_other_sheet() {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("home.insertSheet", json!({"name": "Src"})).unwrap();
|
||||
s.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "Src!B2", "input": "42"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "=Src!B2*2"})).unwrap();
|
||||
// Back on the first sheet, Ctrl+[ from A1 jumps to Src!B2.
|
||||
let d = s.active().unwrap();
|
||||
let first = d.wb.active_sheet;
|
||||
s.execute("selection.set", json!({"range": "A1"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
key(&mut h, Key::OpenBracket, Modifiers::CTRL);
|
||||
let d = h.state().session.active().unwrap();
|
||||
assert_ne!(d.wb.active_sheet, first, "selection jumped to the precedent's sheet");
|
||||
assert_eq!(d.selection.active, CellRef { row: 1, col: 1 }, "B2 on the Src sheet");
|
||||
}
|
||||
Reference in New Issue
Block a user