mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-30 05:37:00 -06:00
Bound the all-level walk to the sheet sparse cell store instead of iterating every position of each precedent area: formulas late in a wide area are no longer skipped, blank positions are never selected, and only formula cells extend the frontier. Adds a regression test (=SUM(A1:Z1) with a formula at Z1) and silences two clippy lints in the same function. Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
4.7 KiB
Rust
118 lines
4.7 KiB
Rust
//! 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");
|
|
}
|