e497aef110
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
207 lines
6.9 KiB
Rust
207 lines
6.9 KiB
Rust
pub mod cell;
|
|
pub mod commit;
|
|
pub mod core;
|
|
pub mod effect_cmds;
|
|
pub mod fit;
|
|
pub mod grid;
|
|
pub mod mode;
|
|
pub mod navigation;
|
|
pub mod panel;
|
|
pub mod registry;
|
|
pub mod search;
|
|
pub mod text_buffer;
|
|
pub mod tile;
|
|
pub mod wizard;
|
|
|
|
#[cfg(test)]
|
|
mod determinism;
|
|
|
|
// Re-export items used by external code
|
|
pub use self::core::{Cmd, CmdContext, CmdRegistry};
|
|
pub use registry::default_registry;
|
|
|
|
#[cfg(test)]
|
|
pub(super) mod test_helpers {
|
|
use std::collections::HashMap;
|
|
|
|
use crate::command::key::KeyCode;
|
|
|
|
use crate::ui::app::AppMode;
|
|
use crate::ui::effect::Effect;
|
|
use crate::view::GridLayout;
|
|
use crate::workbook::Workbook;
|
|
|
|
use super::core::CmdContext;
|
|
use super::registry::default_registry;
|
|
|
|
pub type CmdRegistry = super::core::CmdRegistry;
|
|
|
|
pub static EMPTY_BUFFERS: std::sync::LazyLock<HashMap<String, String>> =
|
|
std::sync::LazyLock::new(HashMap::new);
|
|
pub static EMPTY_EXPANDED: std::sync::LazyLock<std::collections::HashSet<String>> =
|
|
std::sync::LazyLock::new(std::collections::HashSet::new);
|
|
|
|
pub fn make_layout(workbook: &Workbook) -> GridLayout {
|
|
GridLayout::new(&workbook.model, workbook.active_view())
|
|
}
|
|
|
|
pub fn make_ctx<'a>(
|
|
workbook: &'a Workbook,
|
|
layout: &'a GridLayout,
|
|
registry: &'a CmdRegistry,
|
|
) -> CmdContext<'a> {
|
|
let view = workbook.active_view();
|
|
let (sr, sc) = (0, 0);
|
|
CmdContext {
|
|
model: &workbook.model,
|
|
workbook,
|
|
view,
|
|
layout,
|
|
registry,
|
|
mode: &AppMode::Normal,
|
|
selected: (0, 0),
|
|
row_offset: 0,
|
|
col_offset: 0,
|
|
search_query: "",
|
|
yanked: &None,
|
|
dirty: false,
|
|
search_mode: false,
|
|
formula_panel_open: false,
|
|
category_panel_open: false,
|
|
view_panel_open: false,
|
|
formula_cursor: 0,
|
|
cat_panel_cursor: 0,
|
|
view_panel_cursor: 0,
|
|
tile_cat_idx: 0,
|
|
buffers: &EMPTY_BUFFERS,
|
|
opened_editor: None,
|
|
view_back_stack: &[],
|
|
view_forward_stack: &[],
|
|
has_drill_state: false,
|
|
display_value: {
|
|
let key = layout.cell_key(sr, sc);
|
|
key.as_ref()
|
|
.and_then(|k| workbook.model.get_cell(k))
|
|
.map(crate::model::cell::CellValue::edit_text)
|
|
.unwrap_or_default()
|
|
},
|
|
visible_rows: 20,
|
|
visible_cols: 8,
|
|
expanded_cats: &EMPTY_EXPANDED,
|
|
wizard: None,
|
|
key_code: KeyCode::Null,
|
|
}
|
|
}
|
|
|
|
pub fn two_cat_model() -> Workbook {
|
|
let mut wb = Workbook::new("Test");
|
|
wb.add_category("Type").unwrap();
|
|
wb.add_category("Month").unwrap();
|
|
wb.add_item("Type", "Food").unwrap();
|
|
wb.add_item("Type", "Clothing").unwrap();
|
|
wb.add_item("Month", "Jan").unwrap();
|
|
wb.add_item("Month", "Feb").unwrap();
|
|
wb
|
|
}
|
|
|
|
pub fn three_cat_model_with_page() -> Workbook {
|
|
let mut wb = Workbook::new("Test");
|
|
wb.add_category("Type").unwrap();
|
|
wb.add_category("Month").unwrap();
|
|
wb.add_category("Region").unwrap();
|
|
wb.add_item("Type", "Food").unwrap();
|
|
wb.add_item("Type", "Clothing").unwrap();
|
|
wb.add_item("Month", "Jan").unwrap();
|
|
wb.add_item("Month", "Feb").unwrap();
|
|
wb.add_item("Region", "North").unwrap();
|
|
wb.add_item("Region", "South").unwrap();
|
|
wb.add_item("Region", "East").unwrap();
|
|
wb.active_view_mut()
|
|
.set_axis("Region", crate::view::Axis::Page);
|
|
wb
|
|
}
|
|
|
|
/// `two_cat_model` with one `_Measure` formula per target, in order.
|
|
pub fn with_formulas(targets: &[&str]) -> Workbook {
|
|
let mut wb = two_cat_model();
|
|
for target in targets {
|
|
wb.model.add_formula(crate::formula::ast::Formula {
|
|
raw: format!("{target} = 0"),
|
|
target: target.to_string(),
|
|
target_category: "_Measure".to_string(),
|
|
expr: crate::formula::ast::Expr::Number(0.0),
|
|
filter: None,
|
|
format: None,
|
|
});
|
|
}
|
|
wb
|
|
}
|
|
|
|
pub fn effects_debug(effects: &[Box<dyn Effect>]) -> String {
|
|
format!("{:?}", effects)
|
|
}
|
|
|
|
pub fn make_registry() -> CmdRegistry {
|
|
default_registry()
|
|
}
|
|
|
|
/// Two quarters of months on Row, two measures on Column, every cell funded,
|
|
/// Q2 collapsed: rows `[Jan, Feb, Q2]`, and the cursor on the fold at `(2, 1)`.
|
|
pub fn app_on_a_fold() -> crate::ui::app::App {
|
|
use crate::model::cell::{CellKey, CellValue};
|
|
use crate::view::Axis;
|
|
let mut wb = Workbook::new("T");
|
|
wb.add_category("Month").unwrap();
|
|
for (month, quarter) in [("Jan", "Q1"), ("Feb", "Q1"), ("Apr", "Q2"), ("May", "Q2")] {
|
|
wb.add_item_in_group("Month", month, quarter).unwrap();
|
|
}
|
|
wb.active_view_mut().set_expand_depth("Month", 1);
|
|
wb.add_item("_Measure", "Revenue").unwrap();
|
|
wb.add_item("_Measure", "Cost").unwrap();
|
|
for (i, month) in ["Jan", "Feb", "Apr", "May"].into_iter().enumerate() {
|
|
for measure in ["Revenue", "Cost"] {
|
|
let key = CellKey::new(vec![
|
|
("Month".into(), month.into()),
|
|
("_Measure".into(), measure.into()),
|
|
]);
|
|
wb.model.set_cell(key, CellValue::Number(10.0 + i as f64));
|
|
}
|
|
}
|
|
wb.active_view_mut().set_axis("Month", Axis::Row);
|
|
wb.active_view_mut().set_axis("_Measure", Axis::Column);
|
|
wb.active_view_mut().toggle_group_collapse("Month", &["Q2"]);
|
|
let mut app = crate::ui::app::App::new(wb, None);
|
|
app.rebuild_layout();
|
|
app.cursor_mut().selected = (2, 1);
|
|
assert!(
|
|
app.layout.folds_a_group(2, 1),
|
|
"setup: the cursor is on the fold"
|
|
);
|
|
assert_eq!(app.layout.row_label(2), "Q2", "setup: the fold is Q2's row");
|
|
app
|
|
}
|
|
|
|
/// Every stored coordinate naming no item of its category — what a write to a
|
|
/// fold leaves behind (core.md §14).
|
|
pub fn non_item_coordinates(wb: &Workbook) -> Vec<(String, String)> {
|
|
let model = &wb.model;
|
|
model
|
|
.data
|
|
.iter_cells()
|
|
.flat_map(|(key, _)| key.0.clone())
|
|
.filter(|(cat, item)| {
|
|
!model
|
|
.category(cat)
|
|
.is_some_and(|c| c.items.contains_key(item.as_str()))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The document saved and read back: what reaches the file, not the store.
|
|
pub fn saved_and_reloaded(wb: &Workbook) -> (String, Workbook) {
|
|
let text = crate::persistence::format_md(wb);
|
|
let back = crate::persistence::parse_md(&text).expect("the saved text reloads");
|
|
(text, back)
|
|
}
|
|
}
|