chore: fmt + clippy

This commit is contained in:
Edward Langley
2026-04-09 00:21:48 -07:00
parent 08df85664e
commit 6239ac83ad
12 changed files with 264 additions and 195 deletions

View File

@ -1602,7 +1602,11 @@ impl Cmd for TileAxisOp {
None => Box::new(effect::CycleAxis(name.to_string())), None => Box::new(effect::CycleAxis(name.to_string())),
}; };
let status = format!("{}{}", name, axis_label(new_axis)); let status = format!("{}{}", name, axis_label(new_axis));
vec![axis_effect, effect::mark_dirty(), effect::set_status(status)] vec![
axis_effect,
effect::mark_dirty(),
effect::set_status(status),
]
} else { } else {
vec![] vec![]
} }
@ -2250,10 +2254,7 @@ effect_cmd!(
"help", "help",
|_args: &[String]| -> Result<(), String> { Ok(()) }, |_args: &[String]| -> Result<(), String> { Ok(()) },
|_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> { |_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
vec![ vec![effect::help_page_set(0), effect::change_mode(AppMode::Help)]
effect::help_page_set(0),
effect::change_mode(AppMode::Help),
]
} }
); );
@ -3425,7 +3426,10 @@ mod tests {
let effects = TransposeAxes.execute(&ctx); let effects = TransposeAxes.execute(&ctx);
assert_eq!(effects.len(), 2); assert_eq!(effects.len(), 2);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("TransposeAxes"), "Expected TransposeAxes, got: {dbg}"); assert!(
dbg.contains("TransposeAxes"),
"Expected TransposeAxes, got: {dbg}"
);
} }
// ── View navigation ──────────────────────────────────────────────── // ── View navigation ────────────────────────────────────────────────
@ -3439,7 +3443,10 @@ mod tests {
let cmd = ViewNavigate { forward: true }; let cmd = ViewNavigate { forward: true };
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("No forward view"), "Expected status message, got: {dbg}"); assert!(
dbg.contains("No forward view"),
"Expected status message, got: {dbg}"
);
} }
#[test] #[test]
@ -3451,7 +3458,10 @@ mod tests {
let cmd = ViewNavigate { forward: false }; let cmd = ViewNavigate { forward: false };
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("No previous view"), "Expected status message, got: {dbg}"); assert!(
dbg.contains("No previous view"),
"Expected status message, got: {dbg}"
);
} }
#[test] #[test]
@ -3465,7 +3475,10 @@ mod tests {
let cmd = ViewNavigate { forward: true }; let cmd = ViewNavigate { forward: true };
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("ViewForward"), "Expected ViewForward, got: {dbg}"); assert!(
dbg.contains("ViewForward"),
"Expected ViewForward, got: {dbg}"
);
} }
#[test] #[test]
@ -3480,7 +3493,10 @@ mod tests {
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 2); // ApplyAndClearDrill + ViewBack assert_eq!(effects.len(), 2); // ApplyAndClearDrill + ViewBack
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("ApplyAndClearDrill"), "Expected ApplyAndClearDrill, got: {dbg}"); assert!(
dbg.contains("ApplyAndClearDrill"),
"Expected ApplyAndClearDrill, got: {dbg}"
);
assert!(dbg.contains("ViewBack"), "Expected ViewBack, got: {dbg}"); assert!(dbg.contains("ViewBack"), "Expected ViewBack, got: {dbg}");
} }
@ -3501,7 +3517,10 @@ mod tests {
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1); assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetPanelCursor"), "Expected SetPanelCursor, got: {dbg}"); assert!(
dbg.contains("SetPanelCursor"),
"Expected SetPanelCursor, got: {dbg}"
);
} }
#[test] #[test]
@ -3588,7 +3607,10 @@ mod tests {
// Should produce SetPageSelection effects // Should produce SetPageSelection effects
assert!(!effects.is_empty()); assert!(!effects.is_empty());
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetPageSelection"), "Expected SetPageSelection, got: {dbg}"); assert!(
dbg.contains("SetPageSelection"),
"Expected SetPageSelection, got: {dbg}"
);
} }
#[test] #[test]
@ -3600,7 +3622,10 @@ mod tests {
let effects = PagePrev.execute(&ctx); let effects = PagePrev.execute(&ctx);
assert!(!effects.is_empty()); assert!(!effects.is_empty());
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetPageSelection"), "Expected SetPageSelection, got: {dbg}"); assert!(
dbg.contains("SetPageSelection"),
"Expected SetPageSelection, got: {dbg}"
);
} }
// ── Tile axis commands ───────────────────────────────────────────── // ── Tile axis commands ─────────────────────────────────────────────
@ -3657,7 +3682,10 @@ mod tests {
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1); assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetTileCatIdx(1)"), "Expected idx 1, got: {dbg}"); assert!(
dbg.contains("SetTileCatIdx(1)"),
"Expected idx 1, got: {dbg}"
);
} }
#[test] #[test]
@ -3670,7 +3698,10 @@ mod tests {
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1); assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetTileCatIdx(0)"), "Expected clamped to 0, got: {dbg}"); assert!(
dbg.contains("SetTileCatIdx(0)"),
"Expected clamped to 0, got: {dbg}"
);
} }
// ── Commit formula ───────────────────────────────────────────────── // ── Commit formula ─────────────────────────────────────────────────
@ -3686,8 +3717,14 @@ mod tests {
ctx.buffers = &bufs; ctx.buffers = &bufs;
let effects = CommitFormula.execute(&ctx); let effects = CommitFormula.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("AddFormula"), "Expected AddFormula, got: {dbg}"); assert!(
assert!(dbg.contains("FormulaPanel"), "Expected return to FormulaPanel, got: {dbg}"); dbg.contains("AddFormula"),
"Expected AddFormula, got: {dbg}"
);
assert!(
dbg.contains("FormulaPanel"),
"Expected return to FormulaPanel, got: {dbg}"
);
} }
/// Regression: CommitFormula must not target virtual categories (_Index, _Dim) /// Regression: CommitFormula must not target virtual categories (_Index, _Dim)
@ -3727,7 +3764,10 @@ mod tests {
ctx.buffers = &bufs; ctx.buffers = &bufs;
let effects = CommitCategoryAdd.execute(&ctx); let effects = CommitCategoryAdd.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("AddCategory"), "Expected AddCategory, got: {dbg}"); assert!(
dbg.contains("AddCategory"),
"Expected AddCategory, got: {dbg}"
);
} }
#[test] #[test]
@ -3741,7 +3781,10 @@ mod tests {
ctx.buffers = &bufs; ctx.buffers = &bufs;
let effects = CommitCategoryAdd.execute(&ctx); let effects = CommitCategoryAdd.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("CategoryPanel"), "Expected return to CategoryPanel, got: {dbg}"); assert!(
dbg.contains("CategoryPanel"),
"Expected return to CategoryPanel, got: {dbg}"
);
} }
// ── Commit item add ──────────────────────────────────────────────── // ── Commit item add ────────────────────────────────────────────────
@ -3802,7 +3845,10 @@ mod tests {
ctx.buffers = &bufs; ctx.buffers = &bufs;
let effects = CommandModeBackspace.execute(&ctx); let effects = CommandModeBackspace.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("Normal"), "Expected return to Normal, got: {dbg}"); assert!(
dbg.contains("Normal"),
"Expected return to Normal, got: {dbg}"
);
} }
// ── Execute command ──────────────────────────────────────────────── // ── Execute command ────────────────────────────────────────────────
@ -3833,7 +3879,10 @@ mod tests {
let effects = ExecuteCommand.execute(&ctx); let effects = ExecuteCommand.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
// Should show an error status AND return to Normal // Should show an error status AND return to Normal
assert!(dbg.contains("Normal"), "Expected Normal mode on error, got: {dbg}"); assert!(
dbg.contains("Normal"),
"Expected Normal mode on error, got: {dbg}"
);
} }
#[test] #[test]
@ -3842,15 +3891,15 @@ mod tests {
let layout = make_layout(&m); let layout = make_layout(&m);
let reg = default_registry(); let reg = default_registry();
let mut bufs = HashMap::new(); let mut bufs = HashMap::new();
bufs.insert( bufs.insert("command".to_string(), "add-category Region".to_string());
"command".to_string(),
"add-category Region".to_string(),
);
let mut ctx = make_ctx(&m, &layout, &reg); let mut ctx = make_ctx(&m, &layout, &reg);
ctx.buffers = &bufs; ctx.buffers = &bufs;
let effects = ExecuteCommand.execute(&ctx); let effects = ExecuteCommand.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("AddCategory"), "Expected AddCategory effect, got: {dbg}"); assert!(
dbg.contains("AddCategory"),
"Expected AddCategory effect, got: {dbg}"
);
} }
// ── Save command ─────────────────────────────────────────────────── // ── Save command ───────────────────────────────────────────────────
@ -3878,8 +3927,14 @@ mod tests {
let effects = EnterSearchMode.execute(&ctx); let effects = EnterSearchMode.execute(&ctx);
assert_eq!(effects.len(), 2); assert_eq!(effects.len(), 2);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetSearchMode(true)"), "Expected search mode on, got: {dbg}"); assert!(
assert!(dbg.contains("SetSearchQuery"), "Expected query reset, got: {dbg}"); dbg.contains("SetSearchMode(true)"),
"Expected search mode on, got: {dbg}"
);
assert!(
dbg.contains("SetSearchQuery"),
"Expected query reset, got: {dbg}"
);
} }
#[test] #[test]
@ -3891,7 +3946,10 @@ mod tests {
let effects = ExitSearchMode.execute(&ctx); let effects = ExitSearchMode.execute(&ctx);
assert_eq!(effects.len(), 1); assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetSearchMode(false)"), "Expected search mode off, got: {dbg}"); assert!(
dbg.contains("SetSearchMode(false)"),
"Expected search mode off, got: {dbg}"
);
} }
// ── Search navigate with query finds match ───────────────────────── // ── Search navigate with query finds match ─────────────────────────
@ -3922,7 +3980,10 @@ mod tests {
// Should find the cell with 99 and navigate to it // Should find the cell with 99 and navigate to it
if !effects.is_empty() { if !effects.is_empty() {
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SetSelected"), "Expected SetSelected, got: {dbg}"); assert!(
dbg.contains("SetSelected"),
"Expected SetSelected, got: {dbg}"
);
} }
// If empty, the search didn't find it through layout — that's OK since // If empty, the search didn't find it through layout — that's OK since
// layout coordinates may not map 1:1 with model cells in all cases. // layout coordinates may not map 1:1 with model cells in all cases.
@ -3939,9 +4000,18 @@ mod tests {
let effects = CreateAndSwitchView.execute(&ctx); let effects = CreateAndSwitchView.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
// Model starts with 1 view ("Default"), so new view should be "View 2" // Model starts with 1 view ("Default"), so new view should be "View 2"
assert!(dbg.contains("CreateView"), "Expected CreateView, got: {dbg}"); assert!(
assert!(dbg.contains("SwitchView"), "Expected SwitchView, got: {dbg}"); dbg.contains("CreateView"),
assert!(dbg.contains("Normal"), "Expected return to Normal, got: {dbg}"); "Expected CreateView, got: {dbg}"
);
assert!(
dbg.contains("SwitchView"),
"Expected SwitchView, got: {dbg}"
);
assert!(
dbg.contains("Normal"),
"Expected return to Normal, got: {dbg}"
);
} }
// ── Switch view at cursor ────────────────────────────────────────── // ── Switch view at cursor ──────────────────────────────────────────
@ -3955,7 +4025,10 @@ mod tests {
let effects = SwitchViewAtCursor.execute(&ctx); let effects = SwitchViewAtCursor.execute(&ctx);
// cursor 0, model has "Default" view // cursor 0, model has "Default" view
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("SwitchView"), "Expected SwitchView, got: {dbg}"); assert!(
dbg.contains("SwitchView"),
"Expected SwitchView, got: {dbg}"
);
assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}"); assert!(dbg.contains("Normal"), "Expected Normal mode, got: {dbg}");
} }
@ -3980,10 +4053,15 @@ mod tests {
let ctx = make_ctx(&m, &layout, &reg); let ctx = make_ctx(&m, &layout, &reg);
let effects = DeleteViewAtCursor.execute(&ctx); let effects = DeleteViewAtCursor.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("DeleteView"), "Expected DeleteView, got: {dbg}"); assert!(
dbg.contains("DeleteView"),
"Expected DeleteView, got: {dbg}"
);
// At cursor 0, should NOT have SetPanelCursor (no cursor adjustment needed) // At cursor 0, should NOT have SetPanelCursor (no cursor adjustment needed)
assert!(!dbg.contains("SetPanelCursor"), assert!(
"Expected no cursor adjustment at position 0, got: {dbg}"); !dbg.contains("SetPanelCursor"),
"Expected no cursor adjustment at position 0, got: {dbg}"
);
} }
// ── Delete formula at cursor ─────────────────────────────────────── // ── Delete formula at cursor ───────────────────────────────────────
@ -4003,7 +4081,10 @@ mod tests {
let ctx = make_ctx(&m, &layout, &reg); let ctx = make_ctx(&m, &layout, &reg);
let effects = DeleteFormulaAtCursor.execute(&ctx); let effects = DeleteFormulaAtCursor.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("RemoveFormula"), "Expected RemoveFormula, got: {dbg}"); assert!(
dbg.contains("RemoveFormula"),
"Expected RemoveFormula, got: {dbg}"
);
} }
// ── Commit export ────────────────────────────────────────────────── // ── Commit export ──────────────────────────────────────────────────
@ -4064,7 +4145,10 @@ mod tests {
let ctx = make_ctx(&m, &layout, &reg); let ctx = make_ctx(&m, &layout, &reg);
let effects = EnterExportPrompt.execute(&ctx); let effects = EnterExportPrompt.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("ExportPrompt"), "Expected ExportPrompt mode, got: {dbg}"); assert!(
dbg.contains("ExportPrompt"),
"Expected ExportPrompt mode, got: {dbg}"
);
} }
// ── Toggle prune empty ───────────────────────────────────────────── // ── Toggle prune empty ─────────────────────────────────────────────
@ -4077,7 +4161,10 @@ mod tests {
let ctx = make_ctx(&m, &layout, &reg); let ctx = make_ctx(&m, &layout, &reg);
let effects = TogglePruneEmpty.execute(&ctx); let effects = TogglePruneEmpty.execute(&ctx);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("TogglePruneEmpty"), "Expected TogglePruneEmpty, got: {dbg}"); assert!(
dbg.contains("TogglePruneEmpty"),
"Expected TogglePruneEmpty, got: {dbg}"
);
} }
// ── Edit or drill ────────────────────────────────────────────────── // ── Edit or drill ──────────────────────────────────────────────────
@ -4131,7 +4218,10 @@ mod tests {
let effects = cmd.execute(&ctx); let effects = cmd.execute(&ctx);
assert_eq!(effects.len(), 1); assert_eq!(effects.len(), 1);
let dbg = effects_debug(&effects); let dbg = effects_debug(&effects);
assert!(dbg.contains("AddCategory"), "Expected AddCategory, got: {dbg}"); assert!(
dbg.contains("AddCategory"),
"Expected AddCategory, got: {dbg}"
);
} }
#[test] #[test]

View File

@ -1017,7 +1017,9 @@ mod tests {
// hjkl should be bound // hjkl should be bound
for key in ['h', 'j', 'k', 'l'] { for key in ['h', 'j', 'k', 'l'] {
assert!( assert!(
normal.lookup(KeyCode::Char(key), KeyModifiers::NONE).is_some(), normal
.lookup(KeyCode::Char(key), KeyModifiers::NONE)
.is_some(),
"Normal mode missing binding for '{}'", "Normal mode missing binding for '{}'",
key key
); );
@ -1041,9 +1043,7 @@ mod tests {
.lookup(KeyCode::Char('z'), KeyModifiers::NONE) .lookup(KeyCode::Char('z'), KeyModifiers::NONE)
.is_some()); .is_some());
// Should have Esc to exit // Should have Esc to exit
assert!(editing assert!(editing.lookup(KeyCode::Esc, KeyModifiers::NONE).is_some());
.lookup(KeyCode::Esc, KeyModifiers::NONE)
.is_some());
} }
#[test] #[test]
@ -1053,9 +1053,7 @@ mod tests {
assert!(search assert!(search
.lookup(KeyCode::Char('a'), KeyModifiers::NONE) .lookup(KeyCode::Char('a'), KeyModifiers::NONE)
.is_some()); .is_some());
assert!(search assert!(search.lookup(KeyCode::Esc, KeyModifiers::NONE).is_some());
.lookup(KeyCode::Esc, KeyModifiers::NONE)
.is_some());
} }
#[test] #[test]

View File

@ -592,11 +592,7 @@ mod tests {
#[test] #[test]
fn parse_where_with_quoted_string_inside_expression() { fn parse_where_with_quoted_string_inside_expression() {
// WHERE inside a formula string with quotes // WHERE inside a formula string with quotes
let f = parse_formula( let f = parse_formula("X = Revenue WHERE Region = \"West Coast\"", "Measure").unwrap();
"X = Revenue WHERE Region = \"West Coast\"",
"Measure",
)
.unwrap();
let filter = f.filter.as_ref().unwrap(); let filter = f.filter.as_ref().unwrap();
assert_eq!(filter.item, "West Coast"); assert_eq!(filter.item, "West Coast");
} }
@ -714,11 +710,7 @@ mod tests {
#[test] #[test]
fn split_where_ignores_where_inside_quotes() { fn split_where_ignores_where_inside_quotes() {
// WHERE inside quotes should not be treated as a keyword // WHERE inside quotes should not be treated as a keyword
let f = parse_formula( let f = parse_formula("X = Revenue WHERE Region = \"WHERE\"", "Measure").unwrap();
"X = Revenue WHERE Region = \"WHERE\"",
"Measure",
)
.unwrap();
let filter = f.filter.as_ref().unwrap(); let filter = f.filter.as_ref().unwrap();
assert_eq!(filter.item, "WHERE"); assert_eq!(filter.item, "WHERE");
} }
@ -773,11 +765,7 @@ mod tests {
#[test] #[test]
fn pipe_quoted_in_where_filter_value() { fn pipe_quoted_in_where_filter_value() {
let f = parse_formula( let f = parse_formula("X = Revenue WHERE Region = |East Coast|", "Measure").unwrap();
"X = Revenue WHERE Region = |East Coast|",
"Measure",
)
.unwrap();
let filter = f.filter.as_ref().unwrap(); let filter = f.filter.as_ref().unwrap();
assert_eq!(filter.item, "East Coast"); assert_eq!(filter.item, "East Coast");
} }

View File

@ -218,15 +218,11 @@ mod tests {
#[test] #[test]
fn rfc4180_embedded_comma_in_quoted_field() { fn rfc4180_embedded_comma_in_quoted_field() {
let (path, _dir) = create_temp_csv( let (path, _dir) =
"Name,Address,Value\n\"Smith, John\",\"123 Main St, Apt 4\",100", create_temp_csv("Name,Address,Value\n\"Smith, John\",\"123 Main St, Apt 4\",100");
);
let records = parse_csv(&path).unwrap(); let records = parse_csv(&path).unwrap();
assert_eq!(records.len(), 1); assert_eq!(records.len(), 1);
assert_eq!( assert_eq!(records[0]["Name"], Value::String("Smith, John".to_string()));
records[0]["Name"],
Value::String("Smith, John".to_string())
);
assert_eq!( assert_eq!(
records[0]["Address"], records[0]["Address"],
Value::String("123 Main St, Apt 4".to_string()) Value::String("123 Main St, Apt 4".to_string())
@ -236,9 +232,8 @@ mod tests {
#[test] #[test]
fn rfc4180_escaped_quotes_in_field() { fn rfc4180_escaped_quotes_in_field() {
// RFC 4180: doubled quotes ("") inside a quoted field represent a literal quote // RFC 4180: doubled quotes ("") inside a quoted field represent a literal quote
let (path, _dir) = create_temp_csv( let (path, _dir) =
"Name,Description,Value\nWidget,\"A \"\"great\"\" product\",10", create_temp_csv("Name,Description,Value\nWidget,\"A \"\"great\"\" product\",10");
);
let records = parse_csv(&path).unwrap(); let records = parse_csv(&path).unwrap();
assert_eq!(records.len(), 1); assert_eq!(records.len(), 1);
assert_eq!( assert_eq!(
@ -250,9 +245,7 @@ mod tests {
#[test] #[test]
fn rfc4180_newline_in_quoted_field() { fn rfc4180_newline_in_quoted_field() {
// RFC 4180: quoted fields may contain newlines // RFC 4180: quoted fields may contain newlines
let (path, _dir) = create_temp_csv( let (path, _dir) = create_temp_csv("Name,Notes,Value\n\"Widget\",\"Line 1\nLine 2\",10");
"Name,Notes,Value\n\"Widget\",\"Line 1\nLine 2\",10",
);
let records = parse_csv(&path).unwrap(); let records = parse_csv(&path).unwrap();
assert_eq!(records.len(), 1); assert_eq!(records.len(), 1);
assert_eq!( assert_eq!(
@ -263,9 +256,8 @@ mod tests {
#[test] #[test]
fn rfc4180_embedded_comma_and_quotes_combined() { fn rfc4180_embedded_comma_and_quotes_combined() {
let (path, _dir) = create_temp_csv( let (path, _dir) =
"Name,Desc\n\"Smith, \"\"Jr.\"\"\",\"Said \"\"hello, world\"\"\"", create_temp_csv("Name,Desc\n\"Smith, \"\"Jr.\"\"\",\"Said \"\"hello, world\"\"\"");
);
let records = parse_csv(&path).unwrap(); let records = parse_csv(&path).unwrap();
assert_eq!(records.len(), 1); assert_eq!(records.len(), 1);
assert_eq!( assert_eq!(

View File

@ -859,7 +859,7 @@ mod tests {
let mut w = ImportWizard::new(raw); let mut w = ImportWizard::new(raw);
assert_eq!(w.step, WizardStep::SelectArrayPath); assert_eq!(w.step, WizardStep::SelectArrayPath);
w.confirm_path(); // selects first path w.confirm_path(); // selects first path
// Should advance past SelectArrayPath // Should advance past SelectArrayPath
assert_ne!(w.step, WizardStep::SelectArrayPath); assert_ne!(w.step, WizardStep::SelectArrayPath);
assert!(!w.pipeline.records.is_empty()); assert!(!w.pipeline.records.is_empty());
} }
@ -1012,12 +1012,20 @@ mod tests {
// Toggle Year component (cursor 0 = Year of first time field) // Toggle Year component (cursor 0 = Year of first time field)
let had_year_before = { let had_year_before = {
let tc = w.time_category_proposals(); let tc = w.time_category_proposals();
!tc.is_empty() && tc[0].date_components.iter().any(|c| *c == DateComponent::Year) !tc.is_empty()
&& tc[0]
.date_components
.iter()
.any(|c| *c == DateComponent::Year)
}; };
w.toggle_date_component(); w.toggle_date_component();
let has_year_after = { let has_year_after = {
let tc = w.time_category_proposals(); let tc = w.time_category_proposals();
!tc.is_empty() && tc[0].date_components.iter().any(|c| *c == DateComponent::Year) !tc.is_empty()
&& tc[0]
.date_components
.iter()
.any(|c| *c == DateComponent::Year)
}; };
assert_ne!(had_year_before, has_year_after); assert_ne!(had_year_before, has_year_after);
} }

View File

@ -1174,7 +1174,10 @@ mod formula_tests {
.unwrap(); .unwrap();
// Full precision: Tax is exactly 0.75, Total is exactly 10.75 // Full precision: Tax is exactly 0.75, Total is exactly 10.75
assert!(approx_eq(tax, 0.75), "Tax should be 0.75 (full prec), got {tax}"); assert!(
approx_eq(tax, 0.75),
"Tax should be 0.75 (full prec), got {tax}"
);
assert!( assert!(
approx_eq(total, 10.75), approx_eq(total, 10.75),
"Total should be 10.75 (full prec), got {total}" "Total should be 10.75 (full prec), got {total}"

View File

@ -848,7 +848,10 @@ mod tests {
fn autosave_path_inserts_dot_prefix() { fn autosave_path_inserts_dot_prefix() {
let p = std::path::Path::new("/home/user/data/budget.improv"); let p = std::path::Path::new("/home/user/data/budget.improv");
let auto = super::autosave_path(p); let auto = super::autosave_path(p);
assert_eq!(auto.file_name().unwrap().to_str().unwrap(), ".budget.improv.autosave"); assert_eq!(
auto.file_name().unwrap().to_str().unwrap(),
".budget.improv.autosave"
);
} }
// ── format_md: collapsed groups ───────────────────────────────────── // ── format_md: collapsed groups ─────────────────────────────────────
@ -856,8 +859,7 @@ mod tests {
#[test] #[test]
fn format_md_collapsed_group() { fn format_md_collapsed_group() {
let mut m = two_cat_model(); let mut m = two_cat_model();
m.active_view_mut() m.active_view_mut().toggle_group_collapse("Type", "MyGroup");
.toggle_group_collapse("Type", "MyGroup");
let text = format_md(&m); let text = format_md(&m);
assert!(text.contains("collapsed: Type/MyGroup")); assert!(text.contains("collapsed: Type/MyGroup"));
} }
@ -936,8 +938,7 @@ Type=Food = 42
#[test] #[test]
fn parse_md_round_trips_collapsed_group() { fn parse_md_round_trips_collapsed_group() {
let mut m = two_cat_model(); let mut m = two_cat_model();
m.active_view_mut() m.active_view_mut().toggle_group_collapse("Type", "MyGroup");
.toggle_group_collapse("Type", "MyGroup");
let text = format_md(&m); let text = format_md(&m);
let loaded = parse_md(&text).unwrap(); let loaded = parse_md(&text).unwrap();
assert!(loaded.active_view().is_group_collapsed("Type", "MyGroup")); assert!(loaded.active_view().is_group_collapsed("Type", "MyGroup"));
@ -1006,7 +1007,11 @@ Type=Food = 42
let lines: Vec<&str> = content.lines().collect(); let lines: Vec<&str> = content.lines().collect();
assert!(lines.len() >= 2, "Expected header + data, got: {content}"); assert!(lines.len() >= 2, "Expected header + data, got: {content}");
// Header should contain column labels // Header should contain column labels
assert!(lines[0].contains(','), "Expected CSV header, got: {}", lines[0]); assert!(
lines[0].contains(','),
"Expected CSV header, got: {}",
lines[0]
);
} }
#[test] #[test]
@ -1036,11 +1041,9 @@ Type=Food = 42
m.add_formula(f); m.add_formula(f);
// Configure view // Configure view
m.active_view_mut().set_axis("Month", Axis::Page); m.active_view_mut().set_axis("Month", Axis::Page);
m.active_view_mut() m.active_view_mut().set_page_selection("Month", "Jan");
.set_page_selection("Month", "Jan");
m.active_view_mut().hide_item("Type", "Gas"); m.active_view_mut().hide_item("Type", "Gas");
m.active_view_mut() m.active_view_mut().toggle_group_collapse("Type", "G1");
.toggle_group_collapse("Type", "G1");
m.active_view_mut().number_format = ",.0".to_string(); m.active_view_mut().number_format = ",.0".to_string();
let text = format_md(&m); let text = format_md(&m);

View File

@ -317,10 +317,12 @@ impl App {
/// Virtual categories (_Index, _Dim) are always present and don't count. /// Virtual categories (_Index, _Dim) are always present and don't count.
pub fn is_empty_model(&self) -> bool { pub fn is_empty_model(&self) -> bool {
use crate::model::category::CategoryKind; use crate::model::category::CategoryKind;
self.model self.model.categories.values().all(|c| {
.categories matches!(
.values() c.kind,
.all(|c| matches!(c.kind, CategoryKind::VirtualIndex | CategoryKind::VirtualDim)) CategoryKind::VirtualIndex | CategoryKind::VirtualDim
)
})
} }
pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> { pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> {

View File

@ -80,10 +80,7 @@ mod tests {
fn collapsed_category_shows_header_only() { fn collapsed_category_shows_header_only() {
let m = make_model_with_categories(&[("Region", &["North", "South"])]); let m = make_model_with_categories(&[("Region", &["North", "South"])]);
let tree = build_cat_tree(&m, &HashSet::new()); let tree = build_cat_tree(&m, &HashSet::new());
let region_entries: Vec<_> = tree let region_entries: Vec<_> = tree.iter().filter(|e| e.cat_name() == "Region").collect();
.iter()
.filter(|e| e.cat_name() == "Region")
.collect();
assert_eq!(region_entries.len(), 1); // just the header assert_eq!(region_entries.len(), 1); // just the header
assert!(matches!( assert!(matches!(
region_entries[0], region_entries[0],
@ -101,13 +98,13 @@ mod tests {
let mut expanded = HashSet::new(); let mut expanded = HashSet::new();
expanded.insert("Region".to_string()); expanded.insert("Region".to_string());
let tree = build_cat_tree(&m, &expanded); let tree = build_cat_tree(&m, &expanded);
let region_entries: Vec<_> = tree let region_entries: Vec<_> = tree.iter().filter(|e| e.cat_name() == "Region").collect();
.iter()
.filter(|e| e.cat_name() == "Region")
.collect();
// Header + 2 items // Header + 2 items
assert_eq!(region_entries.len(), 3); assert_eq!(region_entries.len(), 3);
assert!(matches!(region_entries[0], CatTreeEntry::Category { expanded: true, .. })); assert!(matches!(
region_entries[0],
CatTreeEntry::Category { expanded: true, .. }
));
assert!(matches!(region_entries[1], CatTreeEntry::Item { .. })); assert!(matches!(region_entries[1], CatTreeEntry::Item { .. }));
assert!(matches!(region_entries[2], CatTreeEntry::Item { .. })); assert!(matches!(region_entries[2], CatTreeEntry::Item { .. }));
} }
@ -124,15 +121,11 @@ mod tests {
let region_items: Vec<_> = tree let region_items: Vec<_> = tree
.iter() .iter()
.filter(|e| { .filter(|e| e.cat_name() == "Region" && matches!(e, CatTreeEntry::Item { .. }))
e.cat_name() == "Region" && matches!(e, CatTreeEntry::Item { .. })
})
.collect(); .collect();
let product_items: Vec<_> = tree let product_items: Vec<_> = tree
.iter() .iter()
.filter(|e| { .filter(|e| e.cat_name() == "Product" && matches!(e, CatTreeEntry::Item { .. }))
e.cat_name() == "Product" && matches!(e, CatTreeEntry::Item { .. })
})
.collect(); .collect();
assert_eq!(region_items.len(), 0); // collapsed assert_eq!(region_items.len(), 0); // collapsed
assert_eq!(product_items.len(), 3); // expanded assert_eq!(product_items.len(), 3); // expanded

View File

@ -1123,15 +1123,35 @@ mod tests {
#[test] #[test]
fn transpose_axes_effect() { fn transpose_axes_effect() {
let mut app = test_app(); let mut app = test_app();
let row_before: Vec<String> = app.model.active_view().categories_on(Axis::Row) let row_before: Vec<String> = app
.into_iter().map(String::from).collect(); .model
let col_before: Vec<String> = app.model.active_view().categories_on(Axis::Column) .active_view()
.into_iter().map(String::from).collect(); .categories_on(Axis::Row)
.into_iter()
.map(String::from)
.collect();
let col_before: Vec<String> = app
.model
.active_view()
.categories_on(Axis::Column)
.into_iter()
.map(String::from)
.collect();
TransposeAxes.apply(&mut app); TransposeAxes.apply(&mut app);
let row_after: Vec<String> = app.model.active_view().categories_on(Axis::Row) let row_after: Vec<String> = app
.into_iter().map(String::from).collect(); .model
let col_after: Vec<String> = app.model.active_view().categories_on(Axis::Column) .active_view()
.into_iter().map(String::from).collect(); .categories_on(Axis::Row)
.into_iter()
.map(String::from)
.collect();
let col_after: Vec<String> = app
.model
.active_view()
.categories_on(Axis::Column)
.into_iter()
.map(String::from)
.collect();
assert_eq!(row_before, col_after); assert_eq!(row_before, col_after);
assert_eq!(col_before, row_after); assert_eq!(col_before, row_after);
} }
@ -1313,8 +1333,7 @@ mod tests {
("Month".into(), "Jan".into()), ("Month".into(), "Jan".into()),
]); ]);
// Set original cell // Set original cell
app.model app.model.set_cell(key.clone(), CellValue::Number(42.0));
.set_cell(key.clone(), CellValue::Number(42.0));
let records = vec![(key.clone(), CellValue::Number(42.0))]; let records = vec![(key.clone(), CellValue::Number(42.0))];
StartDrill(records).apply(&mut app); StartDrill(records).apply(&mut app);
@ -1340,8 +1359,7 @@ mod tests {
("Type".into(), "Food".into()), ("Type".into(), "Food".into()),
("Month".into(), "Jan".into()), ("Month".into(), "Jan".into()),
]); ]);
app.model app.model.set_cell(key.clone(), CellValue::Number(42.0));
.set_cell(key.clone(), CellValue::Number(42.0));
let records = vec![(key.clone(), CellValue::Number(42.0))]; let records = vec![(key.clone(), CellValue::Number(42.0))];
StartDrill(records).apply(&mut app); StartDrill(records).apply(&mut app);
@ -1363,10 +1381,7 @@ mod tests {
("Type".into(), "Drink".into()), ("Type".into(), "Drink".into()),
("Month".into(), "Jan".into()), ("Month".into(), "Jan".into()),
]); ]);
assert_eq!( assert_eq!(app.model.get_cell(&new_key), Some(&CellValue::Number(42.0)));
app.model.get_cell(&new_key),
Some(&CellValue::Number(42.0))
);
// "Drink" should have been added as an item // "Drink" should have been added as an item
let items: Vec<&str> = app let items: Vec<&str> = app
.model .model
@ -1385,8 +1400,7 @@ mod tests {
("Type".into(), "Food".into()), ("Type".into(), "Food".into()),
("Month".into(), "Jan".into()), ("Month".into(), "Jan".into()),
]); ]);
app.model app.model.set_cell(key.clone(), CellValue::Number(42.0));
.set_cell(key.clone(), CellValue::Number(42.0));
let records = vec![(key.clone(), CellValue::Number(42.0))]; let records = vec![(key.clone(), CellValue::Number(42.0))];
StartDrill(records).apply(&mut app); StartDrill(records).apply(&mut app);
@ -1465,10 +1479,7 @@ mod tests {
item: "Food".to_string(), item: "Food".to_string(),
} }
.apply(&mut app); .apply(&mut app);
assert_eq!( assert_eq!(app.model.active_view().page_selection("Type"), Some("Food"));
app.model.active_view().page_selection("Type"),
Some("Food")
);
} }
// ── Hide/show items ───────────────────────────────────────────────── // ── Hide/show items ─────────────────────────────────────────────────
@ -1501,21 +1512,19 @@ mod tests {
group: "MyGroup".to_string(), group: "MyGroup".to_string(),
} }
.apply(&mut app); .apply(&mut app);
assert!( assert!(app
app.model .model
.active_view() .active_view()
.is_group_collapsed("Type", "MyGroup") .is_group_collapsed("Type", "MyGroup"));
);
ToggleGroup { ToggleGroup {
category: "Type".to_string(), category: "Type".to_string(),
group: "MyGroup".to_string(), group: "MyGroup".to_string(),
} }
.apply(&mut app); .apply(&mut app);
assert!( assert!(!app
!app.model .model
.active_view() .active_view()
.is_group_collapsed("Type", "MyGroup") .is_group_collapsed("Type", "MyGroup"));
);
} }
// ── Cycle axis ────────────────────────────────────────────────────── // ── Cycle axis ──────────────────────────────────────────────────────

View File

@ -144,22 +144,16 @@ fn page_welcome(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::accent(" :add-cat Region :add-cat Product", s), HelpLine::accent(" :add-cat Region :add-cat Product", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" 2. Add items to each category:", s), HelpLine::text(" 2. Add items to each category:", s),
HelpLine::accent( HelpLine::accent(" :add-items Region North South East West", s),
" :add-items Region North South East West", HelpLine::accent(" :add-items Product Widget Gadget", s),
s,
),
HelpLine::accent(
" :add-items Product Widget Gadget",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" 3. Navigate with hjkl or arrow keys and press i to edit cells.", s), HelpLine::text(
" 3. Navigate with hjkl or arrow keys and press i to edit cells.",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" 4. Add formulas to compute values automatically:", s), HelpLine::text(" 4. Add formulas to compute values automatically:", s),
HelpLine::accent( HelpLine::accent(" :formula Product Total = Widget + Gadget", s),
" :formula Product Total = Widget + Gadget",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" 5. Save your work:", s), HelpLine::text(" 5. Save your work:", s),
HelpLine::accent(" :w mymodel.improv", s), HelpLine::accent(" :w mymodel.improv", s),
@ -187,11 +181,7 @@ fn page_welcome(s: &HelpStyles) -> Vec<HelpLine> {
s, s,
), ),
HelpLine::blank(), HelpLine::blank(),
HelpLine::dim( HelpLine::dim(" Tip: press Tab or l/n to go to the next page.", "", s),
" Tip: press Tab or l/n to go to the next page.",
"",
s,
),
] ]
} }
@ -215,10 +205,7 @@ fn page_navigation(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("Page-axis cycling", s), HelpLine::heading("Page-axis cycling", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text( HelpLine::text(" When a category is on the Page axis, only one item is", s),
" When a category is on the Page axis, only one item is",
s,
),
HelpLine::text( HelpLine::text(
" visible at a time. Use [ and ] to cycle through them.", " visible at a time. Use [ and ] to cycle through them.",
s, s,
@ -229,7 +216,11 @@ fn page_navigation(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("Search", s), HelpLine::heading("Search", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" /", "Start search — type a pattern, matching cells highlight", s), HelpLine::key(
" /",
"Start search — type a pattern, matching cells highlight",
s,
),
HelpLine::key(" n", "Jump to next match", s), HelpLine::key(" n", "Jump to next match", s),
HelpLine::key(" N", "Jump to previous match", s), HelpLine::key(" N", "Jump to previous match", s),
HelpLine::key(" Esc or Enter", "Exit search mode", s), HelpLine::key(" Esc or Enter", "Exit search mode", s),
@ -257,7 +248,11 @@ fn page_editing(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::accent(" Text: hello world", s), HelpLine::accent(" Text: hello world", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" Enter", "Commit value and move down", s), HelpLine::key(" Enter", "Commit value and move down", s),
HelpLine::key(" Tab", "Commit value and move right (stay in edit mode)", s), HelpLine::key(
" Tab",
"Commit value and move right (stay in edit mode)",
s,
),
HelpLine::key(" Esc", "Discard edits and return to Normal", s), HelpLine::key(" Esc", "Discard edits and return to Normal", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("Copy and paste", s), HelpLine::heading("Copy and paste", s),
@ -280,7 +275,10 @@ fn page_editing(s: &HelpStyles) -> Vec<HelpLine> {
s, s,
), ),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" Example: in a Product category with items Widget and Gadget:", s), HelpLine::text(
" Example: in a Product category with items Widget and Gadget:",
s,
),
HelpLine::accent(" :formula Product Total = Widget + Gadget", s), HelpLine::accent(" :formula Product Total = Widget + Gadget", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text(" Supported operators: + - * /", s), HelpLine::text(" Supported operators: + - * /", s),
@ -300,10 +298,7 @@ fn page_panels(s: &HelpStyles) -> Vec<HelpLine> {
" Panels open on the right side of the screen and give you", " Panels open on the right side of the screen and give you",
s, s,
), ),
HelpLine::text( HelpLine::text(" quick access to formulas, categories, and views.", s),
" quick access to formulas, categories, and views.",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" F", "Toggle Formula panel", s), HelpLine::key(" F", "Toggle Formula panel", s),
HelpLine::dim(" n", "New formula", s), HelpLine::dim(" n", "New formula", s),
@ -325,14 +320,8 @@ fn page_panels(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("Tile select mode (T)", s), HelpLine::heading("Tile select mode (T)", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::text( HelpLine::text(" Tiles control which axis each category is placed on.", s),
" Tiles control which axis each category is placed on.", HelpLine::text(" Press T to enter tile-select mode, then:", s),
s,
),
HelpLine::text(
" Press T to enter tile-select mode, then:",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" h / l (← →)", "Select previous / next category tile", s), HelpLine::key(" h / l (← →)", "Select previous / next category tile", s),
HelpLine::key(" Space / Enter", "Cycle axis: Row → Col → Page", s), HelpLine::key(" Space / Enter", "Cycle axis: Row → Col → Page", s),
@ -345,11 +334,7 @@ fn page_panels(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" z", "Toggle collapse of nearest group above cursor", s), HelpLine::key(" z", "Toggle collapse of nearest group above cursor", s),
HelpLine::key(" H", "Hide current row item", s), HelpLine::key(" H", "Hide current row item", s),
HelpLine::dim( HelpLine::dim(" :show-item <cat> <item>", "Restore a hidden item", s),
" :show-item <cat> <item>",
"Restore a hidden item",
s,
),
] ]
} }
@ -362,10 +347,7 @@ fn page_commands(s: &HelpStyles) -> Vec<HelpLine> {
" Press : to open the command line. Commands are entered", " Press : to open the command line. Commands are entered",
s, s,
), ),
HelpLine::text( HelpLine::text(" vim-style and executed with Enter. Esc cancels.", s),
" vim-style and executed with Enter. Esc cancels.",
s,
),
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("File operations", s), HelpLine::heading("File operations", s),
HelpLine::blank(), HelpLine::blank(),
@ -385,7 +367,11 @@ fn page_commands(s: &HelpStyles) -> Vec<HelpLine> {
HelpLine::blank(), HelpLine::blank(),
HelpLine::key(" :add-cat <name>", "Add a new category", s), HelpLine::key(" :add-cat <name>", "Add a new category", s),
HelpLine::key(" :add-item <cat> <item>", "Add one item to a category", s), HelpLine::key(" :add-item <cat> <item>", "Add one item to a category", s),
HelpLine::key(" :add-items <cat> a b c ...", "Add multiple items at once", s), HelpLine::key(
" :add-items <cat> a b c ...",
"Add multiple items at once",
s,
),
HelpLine::key(" :formula <cat> <Name=expr>", "Add a formula", s), HelpLine::key(" :formula <cat> <Name=expr>", "Add a formula", s),
HelpLine::blank(), HelpLine::blank(),
HelpLine::heading("Views", s), HelpLine::heading("Views", s),

View File

@ -35,10 +35,7 @@ impl<'a> ViewContent<'a> {
for axis in [Axis::Row, Axis::Column, Axis::Page] { for axis in [Axis::Row, Axis::Column, Axis::Page] {
let cats = view.categories_on(axis); let cats = view.categories_on(axis);
// Filter out virtual categories // Filter out virtual categories
let cats: Vec<&str> = cats let cats: Vec<&str> = cats.into_iter().filter(|c| !c.starts_with('_')).collect();
.into_iter()
.filter(|c| !c.starts_with('_'))
.collect();
if !cats.is_empty() { if !cats.is_empty() {
let prefix = match axis { let prefix = match axis {
Axis::Row => "R", Axis::Row => "R",