refactor: break Model↔View cycle, introduce Workbook wrapper
Model is now pure data (categories, cells, formulas, measure_agg) with no references to view/. The Workbook struct owns the Model together with views and the active view name, and is responsible for cross-slice operations (add/remove category → notify views, view management). - New: src/workbook.rs with Workbook wrapper and cross-slice helpers (add_category, add_label_category, remove_category, create_view, switch_view, delete_view, normalize_view_state). - Model: strip view state and view-touching methods. recompute_formulas remains on Model as a primitive; the view-derived none_cats list is gathered at each call site (App::rebuild_layout, persistence::load) so the view dependency is explicit, not hidden behind a wrapper. - View: add View::none_cats() helper. - CmdContext: add workbook and view fields so commands can reach both slices without threading Model + View through every call. - App: rename `model` field to `workbook`. - Persistence (save/load/format_md/parse_md/export_csv): take/return Workbook so the on-disk format carries model + views together. - Widgets (GridWidget, TileBar, CategoryContent, ViewContent): take explicit &Model + &View instead of routing through Model. Tests updated throughout to reflect the new shape. View-management tests that previously lived on Model continue to cover the same behaviour via a build_workbook() helper in model/types.rs. All 573 tests pass; clippy is clean. This is Phase A of improvise-36h. Phase B will mechanically extract crates/improvise-core/ containing model/, view/, format.rs, workbook.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+94
-98
@@ -12,13 +12,13 @@ use ratatui::style::Color;
|
||||
use crate::command::cmd::CmdContext;
|
||||
use crate::command::keymap::{Keymap, KeymapSet};
|
||||
use crate::import::wizard::ImportWizard;
|
||||
use crate::model::Model;
|
||||
use crate::model::cell::CellValue;
|
||||
use crate::persistence;
|
||||
use crate::ui::grid::{
|
||||
compute_col_widths, compute_row_header_width, compute_visible_cols, parse_number_format,
|
||||
};
|
||||
use crate::view::GridLayout;
|
||||
use crate::workbook::Workbook;
|
||||
|
||||
/// Drill-down state: frozen record snapshot + pending edits that have not
|
||||
/// yet been applied to the model.
|
||||
@@ -152,7 +152,7 @@ impl AppMode {
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub model: Model,
|
||||
pub workbook: Workbook,
|
||||
pub file_path: Option<PathBuf>,
|
||||
pub mode: AppMode,
|
||||
pub status_msg: String,
|
||||
@@ -199,22 +199,19 @@ pub struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(mut model: Model, file_path: Option<PathBuf>) -> Self {
|
||||
pub fn new(mut workbook: Workbook, file_path: Option<PathBuf>) -> Self {
|
||||
// Recompute formula cache before building the initial layout so
|
||||
// formula-derived values are available on the first frame.
|
||||
let none_cats: Vec<String> = model
|
||||
.active_view()
|
||||
.categories_on(crate::view::Axis::None)
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
model.recompute_formulas(&none_cats);
|
||||
// formula-derived values are available on the first frame. The
|
||||
// cache is keyed by the active view's None-axis categories, so
|
||||
// the caller must gather them explicitly.
|
||||
let none_cats = workbook.active_view().none_cats();
|
||||
workbook.model.recompute_formulas(&none_cats);
|
||||
let layout = {
|
||||
let view = model.active_view();
|
||||
GridLayout::with_frozen_records(&model, view, None)
|
||||
let view = workbook.active_view();
|
||||
GridLayout::with_frozen_records(&workbook.model, view, None)
|
||||
};
|
||||
Self {
|
||||
model,
|
||||
workbook,
|
||||
file_path,
|
||||
mode: AppMode::Normal,
|
||||
status_msg: String::new(),
|
||||
@@ -245,29 +242,24 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the grid layout from current model, view, and drill state.
|
||||
/// Note: `with_frozen_records` already handles pruning internally.
|
||||
/// Rebuild the grid layout from current workbook, active view, and drill
|
||||
/// state. Note: `with_frozen_records` already handles pruning internally.
|
||||
pub fn rebuild_layout(&mut self) {
|
||||
// Gather none_cats before mutable borrow for formula recomputation
|
||||
let none_cats: Vec<String> = self
|
||||
.model
|
||||
.active_view()
|
||||
.categories_on(crate::view::Axis::None)
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
self.model.recompute_formulas(&none_cats);
|
||||
let view = self.model.active_view();
|
||||
let none_cats = self.workbook.active_view().none_cats();
|
||||
self.workbook.model.recompute_formulas(&none_cats);
|
||||
let view = self.workbook.active_view();
|
||||
let frozen = self.drill_state.as_ref().map(|s| Rc::clone(&s.records));
|
||||
self.layout = GridLayout::with_frozen_records(&self.model, view, frozen);
|
||||
self.layout = GridLayout::with_frozen_records(&self.workbook.model, view, frozen);
|
||||
}
|
||||
|
||||
pub fn cmd_context(&self, key: KeyCode, _mods: KeyModifiers) -> CmdContext<'_> {
|
||||
let view = self.model.active_view();
|
||||
let view = self.workbook.active_view();
|
||||
let layout = &self.layout;
|
||||
let (sel_row, sel_col) = view.selected;
|
||||
CmdContext {
|
||||
model: &self.model,
|
||||
model: &self.workbook.model,
|
||||
workbook: &self.workbook,
|
||||
view,
|
||||
layout,
|
||||
registry: self.keymap_set.registry(),
|
||||
mode: &self.mode,
|
||||
@@ -298,7 +290,8 @@ impl App {
|
||||
.or_else(|| layout.resolve_display(k))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
self.model
|
||||
self.workbook
|
||||
.model
|
||||
.get_cell(k)
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default()
|
||||
@@ -310,7 +303,7 @@ impl App {
|
||||
visible_rows: (self.term_height as usize).saturating_sub(8),
|
||||
visible_cols: {
|
||||
let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format);
|
||||
let col_widths = compute_col_widths(&self.model, layout, fmt_comma, fmt_decimals);
|
||||
let col_widths = compute_col_widths(&self.workbook.model, layout, fmt_comma, fmt_decimals);
|
||||
let row_header_width = compute_row_header_width(layout);
|
||||
compute_visible_cols(
|
||||
&col_widths,
|
||||
@@ -335,7 +328,7 @@ impl App {
|
||||
/// Virtual categories (_Index, _Dim, _Measure) are always present and don't count.
|
||||
pub fn is_empty_model(&self) -> bool {
|
||||
use crate::model::category::CategoryKind;
|
||||
self.model.categories.values().all(|c| {
|
||||
self.workbook.model.categories.values().all(|c| {
|
||||
matches!(
|
||||
c.kind,
|
||||
CategoryKind::VirtualIndex
|
||||
@@ -379,7 +372,7 @@ impl App {
|
||||
&& let Some(path) = &self.file_path.clone()
|
||||
{
|
||||
let ap = persistence::autosave_path(path);
|
||||
let _ = persistence::save(&self.model, &ap);
|
||||
let _ = persistence::save(&self.workbook, &ap);
|
||||
self.last_autosave = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -422,18 +415,17 @@ impl App {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::Model;
|
||||
|
||||
fn two_col_model() -> App {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Row").unwrap(); // → Row axis
|
||||
m.add_category("Col").unwrap(); // → Column axis
|
||||
m.category_mut("Row").unwrap().add_item("A");
|
||||
m.category_mut("Row").unwrap().add_item("B");
|
||||
m.category_mut("Row").unwrap().add_item("C");
|
||||
m.category_mut("Col").unwrap().add_item("X");
|
||||
m.category_mut("Col").unwrap().add_item("Y");
|
||||
App::new(m, None)
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Row").unwrap(); // → Row axis
|
||||
wb.add_category("Col").unwrap(); // → Column axis
|
||||
wb.model.category_mut("Row").unwrap().add_item("A");
|
||||
wb.model.category_mut("Row").unwrap().add_item("B");
|
||||
wb.model.category_mut("Row").unwrap().add_item("C");
|
||||
wb.model.category_mut("Col").unwrap().add_item("X");
|
||||
wb.model.category_mut("Col").unwrap().add_item("Y");
|
||||
App::new(wb, None)
|
||||
}
|
||||
|
||||
fn run_cmd(app: &mut App, cmd: &dyn crate::command::cmd::Cmd) {
|
||||
@@ -445,7 +437,7 @@ mod tests {
|
||||
|
||||
fn enter_advance_cmd(app: &App) -> crate::command::cmd::navigation::EnterAdvance {
|
||||
use crate::command::cmd::navigation::CursorState;
|
||||
let view = app.model.active_view();
|
||||
let view = app.workbook.active_view();
|
||||
let cursor = CursorState {
|
||||
row: view.selected.0,
|
||||
col: view.selected.1,
|
||||
@@ -462,29 +454,29 @@ mod tests {
|
||||
#[test]
|
||||
fn enter_advance_moves_down_within_column() {
|
||||
let mut app = two_col_model();
|
||||
app.model.active_view_mut().selected = (0, 0);
|
||||
app.workbook.active_view_mut().selected = (0, 0);
|
||||
let cmd = enter_advance_cmd(&app);
|
||||
run_cmd(&mut app, &cmd);
|
||||
assert_eq!(app.model.active_view().selected, (1, 0));
|
||||
assert_eq!(app.workbook.active_view().selected, (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_advance_wraps_to_top_of_next_column() {
|
||||
let mut app = two_col_model();
|
||||
// row_max = 2 (A,B,C), col 0 → should wrap to (0, 1)
|
||||
app.model.active_view_mut().selected = (2, 0);
|
||||
app.workbook.active_view_mut().selected = (2, 0);
|
||||
let cmd = enter_advance_cmd(&app);
|
||||
run_cmd(&mut app, &cmd);
|
||||
assert_eq!(app.model.active_view().selected, (0, 1));
|
||||
assert_eq!(app.workbook.active_view().selected, (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_advance_stays_at_bottom_right() {
|
||||
let mut app = two_col_model();
|
||||
app.model.active_view_mut().selected = (2, 1);
|
||||
app.workbook.active_view_mut().selected = (2, 1);
|
||||
let cmd = enter_advance_cmd(&app);
|
||||
run_cmd(&mut app, &cmd);
|
||||
assert_eq!(app.model.active_view().selected, (2, 1));
|
||||
assert_eq!(app.workbook.active_view().selected, (2, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -535,22 +527,22 @@ mod tests {
|
||||
// each → column widths ~31 chars. With term_width=80, row header ~4,
|
||||
// data area ~76 → only ~2 columns actually fit. But the rough estimate
|
||||
// (80−30)/12 = 4 over-counts, so viewport_effects never scrolls.
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Row").unwrap();
|
||||
m.add_category("Col").unwrap();
|
||||
m.category_mut("Row").unwrap().add_item("R1");
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Row").unwrap();
|
||||
wb.add_category("Col").unwrap();
|
||||
wb.model.category_mut("Row").unwrap().add_item("R1");
|
||||
for i in 0..8 {
|
||||
let name = format!("VeryLongColumnItemName_{i:03}");
|
||||
m.category_mut("Col").unwrap().add_item(&name);
|
||||
wb.model.category_mut("Col").unwrap().add_item(&name);
|
||||
}
|
||||
// Populate a value so the model isn't empty
|
||||
// Populate a value so the workbook isn't empty
|
||||
let key = CellKey::new(vec![
|
||||
("Row".to_string(), "R1".to_string()),
|
||||
("Col".to_string(), "VeryLongColumnItemName_000".to_string()),
|
||||
]);
|
||||
m.set_cell(key, CellValue::Number(1.0));
|
||||
wb.model.set_cell(key, CellValue::Number(1.0));
|
||||
|
||||
let mut app = App::new(m, None);
|
||||
let mut app = App::new(wb, None);
|
||||
app.term_width = 80;
|
||||
|
||||
// Press 'l' (right) 3 times to move cursor to column 3.
|
||||
@@ -563,34 +555,34 @@ mod tests {
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
app.model.active_view().selected.1,
|
||||
app.workbook.active_view().selected.1,
|
||||
3,
|
||||
"cursor should be at column 3"
|
||||
);
|
||||
assert!(
|
||||
app.model.active_view().col_offset > 0,
|
||||
app.workbook.active_view().col_offset > 0,
|
||||
"col_offset should scroll when cursor moves past visible area (only ~2 cols fit \
|
||||
in 80-char terminal with 26-char-wide columns), but col_offset is {}",
|
||||
app.model.active_view().col_offset
|
||||
app.workbook.active_view().col_offset
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_jumps_to_first_col() {
|
||||
let mut app = two_col_model();
|
||||
app.model.active_view_mut().selected = (1, 1);
|
||||
app.workbook.active_view_mut().selected = (1, 1);
|
||||
app.handle_key(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected, (1, 0));
|
||||
assert_eq!(app.workbook.active_view().selected, (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_jumps_to_last_col() {
|
||||
let mut app = two_col_model();
|
||||
app.model.active_view_mut().selected = (1, 0);
|
||||
app.workbook.active_view_mut().selected = (1, 0);
|
||||
app.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected, (1, 1));
|
||||
assert_eq!(app.workbook.active_view().selected, (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -598,38 +590,40 @@ mod tests {
|
||||
let mut app = two_col_model();
|
||||
// Add enough rows
|
||||
for i in 0..30 {
|
||||
app.model
|
||||
app.workbook
|
||||
.model
|
||||
.category_mut("Row")
|
||||
.unwrap()
|
||||
.add_item(format!("R{i}"));
|
||||
}
|
||||
app.term_height = 28; // ~20 visible rows → delta = 15
|
||||
app.model.active_view_mut().selected = (0, 0);
|
||||
app.workbook.active_view_mut().selected = (0, 0);
|
||||
app.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected.1, 0, "column preserved");
|
||||
assert_eq!(app.workbook.active_view().selected.1, 0, "column preserved");
|
||||
assert!(
|
||||
app.model.active_view().selected.0 > 0,
|
||||
app.workbook.active_view().selected.0 > 0,
|
||||
"row should advance on PageDown"
|
||||
);
|
||||
// 3/4 of ~20 = 15
|
||||
assert_eq!(app.model.active_view().selected.0, 15);
|
||||
assert_eq!(app.workbook.active_view().selected.0, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_up_scrolls_backward() {
|
||||
let mut app = two_col_model();
|
||||
for i in 0..30 {
|
||||
app.model
|
||||
app.workbook
|
||||
.model
|
||||
.category_mut("Row")
|
||||
.unwrap()
|
||||
.add_item(format!("R{i}"));
|
||||
}
|
||||
app.term_height = 28;
|
||||
app.model.active_view_mut().selected = (20, 0);
|
||||
app.workbook.active_view_mut().selected = (20, 0);
|
||||
app.handle_key(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected.0, 5);
|
||||
assert_eq!(app.workbook.active_view().selected.0, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -637,21 +631,22 @@ mod tests {
|
||||
let mut app = two_col_model();
|
||||
// Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12.
|
||||
for i in 0..10 {
|
||||
app.model
|
||||
app.workbook
|
||||
.model
|
||||
.category_mut("Row")
|
||||
.unwrap()
|
||||
.add_item(format!("R{i}"));
|
||||
}
|
||||
app.term_height = 13; // ~5 visible rows
|
||||
app.model.active_view_mut().selected = (0, 0);
|
||||
app.workbook.active_view_mut().selected = (0, 0);
|
||||
// G jumps to last row (row 12)
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('G'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
let last = app.model.active_view().selected.0;
|
||||
let last = app.workbook.active_view().selected.0;
|
||||
assert_eq!(last, 12, "should be at last row");
|
||||
// With only ~5 visible rows and 13 rows, offset should scroll.
|
||||
// Bug: hardcoded 20 means `12 >= 0 + 20` is false → no scroll.
|
||||
let offset = app.model.active_view().row_offset;
|
||||
let offset = app.workbook.active_view().row_offset;
|
||||
assert!(
|
||||
offset > 0,
|
||||
"row_offset should scroll when last row is beyond visible area, but is {offset}"
|
||||
@@ -662,33 +657,34 @@ mod tests {
|
||||
fn ctrl_d_scrolls_viewport_with_small_terminal() {
|
||||
let mut app = two_col_model();
|
||||
for i in 0..30 {
|
||||
app.model
|
||||
app.workbook
|
||||
.model
|
||||
.category_mut("Row")
|
||||
.unwrap()
|
||||
.add_item(format!("R{i}"));
|
||||
}
|
||||
app.term_height = 13; // ~5 visible rows
|
||||
app.model.active_view_mut().selected = (0, 0);
|
||||
app.workbook.active_view_mut().selected = (0, 0);
|
||||
// Ctrl+d scrolls by 5 rows
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected.0, 5);
|
||||
assert_eq!(app.workbook.active_view().selected.0, 5);
|
||||
// Press Ctrl+d again — now at row 10 with only 5 visible rows,
|
||||
// row_offset should have scrolled (not stay at 0 due to hardcoded 20)
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL))
|
||||
.unwrap();
|
||||
assert_eq!(app.model.active_view().selected.0, 10);
|
||||
assert_eq!(app.workbook.active_view().selected.0, 10);
|
||||
assert!(
|
||||
app.model.active_view().row_offset > 0,
|
||||
app.workbook.active_view().row_offset > 0,
|
||||
"row_offset should scroll with small terminal, but is {}",
|
||||
app.model.active_view().row_offset
|
||||
app.workbook.active_view().row_offset
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_in_edit_mode_commits_and_moves_right() {
|
||||
let mut app = two_col_model();
|
||||
app.model.active_view_mut().selected = (0, 0);
|
||||
app.workbook.active_view_mut().selected = (0, 0);
|
||||
// Enter edit mode
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
@@ -706,7 +702,7 @@ mod tests {
|
||||
app.mode
|
||||
);
|
||||
assert_eq!(
|
||||
app.model.active_view().selected.1,
|
||||
app.workbook.active_view().selected.1,
|
||||
1,
|
||||
"should have moved to column 1"
|
||||
);
|
||||
@@ -735,7 +731,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn fresh_model_is_empty() {
|
||||
let app = App::new(Model::new("T"), None);
|
||||
let app = App::new(Workbook::new("T"), None);
|
||||
assert!(
|
||||
app.is_empty_model(),
|
||||
"a brand-new model with only virtual categories should be empty"
|
||||
@@ -744,9 +740,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn model_with_user_category_is_not_empty() {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Sales").unwrap();
|
||||
let app = App::new(m, None);
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Sales").unwrap();
|
||||
let app = App::new(wb, None);
|
||||
assert!(
|
||||
!app.is_empty_model(),
|
||||
"a model with a user-defined category should not be empty"
|
||||
@@ -757,7 +753,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_page_next_advances_page() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 0;
|
||||
|
||||
@@ -768,7 +764,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_page_prev_goes_back() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 2;
|
||||
|
||||
@@ -779,7 +775,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_page_clamps_at_zero() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = 0;
|
||||
|
||||
@@ -792,7 +788,7 @@ mod tests {
|
||||
fn help_page_clamps_at_max() {
|
||||
use crate::ui::help::HELP_PAGE_COUNT;
|
||||
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
app.help_page = HELP_PAGE_COUNT - 1;
|
||||
|
||||
@@ -809,7 +805,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_q_returns_to_normal() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE))
|
||||
@@ -822,7 +818,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_esc_returns_to_normal() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||
@@ -835,7 +831,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn help_colon_enters_command_mode() {
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
app.mode = AppMode::Help;
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
|
||||
@@ -852,7 +848,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_item_to_nonexistent_category_sets_status() {
|
||||
use crate::ui::effect::Effect;
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
let effect = crate::ui::effect::AddItem {
|
||||
category: "Nonexistent".to_string(),
|
||||
item: "x".to_string(),
|
||||
@@ -868,7 +864,7 @@ mod tests {
|
||||
#[test]
|
||||
fn add_formula_with_bad_syntax_sets_status() {
|
||||
use crate::ui::effect::Effect;
|
||||
let mut app = App::new(Model::new("T"), None);
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
let effect = crate::ui::effect::AddFormula {
|
||||
raw: "!!!invalid".to_string(),
|
||||
target_category: "X".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user