add panel toggling and new command implementations
- Implemented a suite of new commands for panel visibility, editing, export prompts, search navigation, page cycling, and grid operations. - Updated tests to cover new command behavior. - Adjusted command context usage accordingly. Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
This commit is contained in:
@ -315,14 +315,479 @@ impl Cmd for EnterSearchMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Panel commands ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Toggle a panel's visibility; if it opens, focus it (enter its mode).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SetPendingKey(pub char);
|
pub struct TogglePanelAndFocus(pub Panel);
|
||||||
impl Cmd for SetPendingKey {
|
impl Cmd for TogglePanelAndFocus {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"set-pending-key"
|
"toggle-panel-and-focus"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let currently_open = match self.0 {
|
||||||
|
Panel::Formula => ctx.formula_panel_open,
|
||||||
|
Panel::Category => ctx.category_panel_open,
|
||||||
|
Panel::View => ctx.view_panel_open,
|
||||||
|
};
|
||||||
|
let new_open = !currently_open;
|
||||||
|
let mut effects: Vec<Box<dyn Effect>> = vec![Box::new(effect::SetPanelOpen {
|
||||||
|
panel: self.0,
|
||||||
|
open: new_open,
|
||||||
|
})];
|
||||||
|
if new_open {
|
||||||
|
let mode = match self.0 {
|
||||||
|
Panel::Formula => AppMode::FormulaPanel,
|
||||||
|
Panel::Category => AppMode::CategoryPanel,
|
||||||
|
Panel::View => AppMode::ViewPanel,
|
||||||
|
};
|
||||||
|
effects.push(effect::change_mode(mode));
|
||||||
|
}
|
||||||
|
effects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle a panel's visibility without changing mode.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TogglePanelVisibility(pub Panel);
|
||||||
|
impl Cmd for TogglePanelVisibility {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"toggle-panel-visibility"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let currently_open = match self.0 {
|
||||||
|
Panel::Formula => ctx.formula_panel_open,
|
||||||
|
Panel::Category => ctx.category_panel_open,
|
||||||
|
Panel::View => ctx.view_panel_open,
|
||||||
|
};
|
||||||
|
vec![Box::new(effect::SetPanelOpen {
|
||||||
|
panel: self.0,
|
||||||
|
open: !currently_open,
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tab through open panels, entering the first open panel's mode.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CyclePanelFocus;
|
||||||
|
impl Cmd for CyclePanelFocus {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"cycle-panel-focus"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
if ctx.formula_panel_open {
|
||||||
|
vec![effect::change_mode(AppMode::FormulaPanel)]
|
||||||
|
} else if ctx.category_panel_open {
|
||||||
|
vec![effect::change_mode(AppMode::CategoryPanel)]
|
||||||
|
} else if ctx.view_panel_open {
|
||||||
|
vec![effect::change_mode(AppMode::ViewPanel)]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Editing entry ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Read the current cell value and enter Editing mode with it as the buffer.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnterEditMode;
|
||||||
|
impl Cmd for EnterEditMode {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"enter-edit-mode"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let (ri, ci) = ctx.selected;
|
||||||
|
let current = layout
|
||||||
|
.cell_key(ri, ci)
|
||||||
|
.and_then(|k| ctx.model.get_cell(&k).cloned())
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
vec![effect::change_mode(AppMode::Editing { buffer: current })]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnterAdvance;
|
||||||
|
impl Cmd for EnterAdvance {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"enter-advance"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let row_max = layout.row_count().saturating_sub(1);
|
||||||
|
let col_max = layout.col_count().saturating_sub(1);
|
||||||
|
let (r, c) = ctx.selected;
|
||||||
|
let (nr, nc) = if r < row_max {
|
||||||
|
(r + 1, c)
|
||||||
|
} else if c < col_max {
|
||||||
|
(0, c + 1)
|
||||||
|
} else {
|
||||||
|
(r, c) // already at bottom-right; stay
|
||||||
|
};
|
||||||
|
let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(nr, nc)];
|
||||||
|
let mut row_offset = ctx.row_offset;
|
||||||
|
let mut col_offset = ctx.col_offset;
|
||||||
|
if nr < row_offset {
|
||||||
|
row_offset = nr;
|
||||||
|
}
|
||||||
|
if nr >= row_offset + 20 {
|
||||||
|
row_offset = nr.saturating_sub(19);
|
||||||
|
}
|
||||||
|
if nc < col_offset {
|
||||||
|
col_offset = nc;
|
||||||
|
}
|
||||||
|
if nc >= col_offset + 8 {
|
||||||
|
col_offset = nc.saturating_sub(7);
|
||||||
|
}
|
||||||
|
if row_offset != ctx.row_offset {
|
||||||
|
effects.push(Box::new(effect::SetRowOffset(row_offset)));
|
||||||
|
}
|
||||||
|
if col_offset != ctx.col_offset {
|
||||||
|
effects.push(Box::new(effect::SetColOffset(col_offset)));
|
||||||
|
}
|
||||||
|
effects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter export prompt mode.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnterExportPrompt;
|
||||||
|
impl Cmd for EnterExportPrompt {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"enter-export-prompt"
|
||||||
}
|
}
|
||||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
vec![Box::new(effect::SetPendingKey(Some(self.0)))]
|
vec![effect::change_mode(AppMode::ExportPrompt {
|
||||||
|
buffer: String::new(),
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search / navigation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Navigate to the next or previous search match.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SearchNavigate(pub bool);
|
||||||
|
impl Cmd for SearchNavigate {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"search-navigate"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let query = ctx.search_query.to_lowercase();
|
||||||
|
if query.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let (cur_row, cur_col) = ctx.selected;
|
||||||
|
let total_rows = layout.row_count().max(1);
|
||||||
|
let total_cols = layout.col_count().max(1);
|
||||||
|
let total = total_rows * total_cols;
|
||||||
|
let cur_flat = cur_row * total_cols + cur_col;
|
||||||
|
|
||||||
|
let matches: Vec<usize> = (0..total)
|
||||||
|
.filter(|&flat| {
|
||||||
|
let ri = flat / total_cols;
|
||||||
|
let ci = flat % total_cols;
|
||||||
|
let key = match layout.cell_key(ri, ci) {
|
||||||
|
Some(k) => k,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
let s = match ctx.model.evaluate_aggregated(&key, &layout.none_cats) {
|
||||||
|
Some(CellValue::Number(n)) => format!("{n}"),
|
||||||
|
Some(CellValue::Text(t)) => t,
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
s.to_lowercase().contains(&query)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if matches.is_empty() {
|
||||||
|
return vec![effect::set_status(format!(
|
||||||
|
"No matches for '{}'",
|
||||||
|
ctx.search_query
|
||||||
|
))];
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_flat = if self.0 {
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.find(|&&f| f > cur_flat)
|
||||||
|
.or_else(|| matches.first())
|
||||||
|
.copied()
|
||||||
|
} else {
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|&&f| f < cur_flat)
|
||||||
|
.or_else(|| matches.last())
|
||||||
|
.copied()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(flat) = target_flat {
|
||||||
|
let ri = flat / total_cols;
|
||||||
|
let ci = flat % total_cols;
|
||||||
|
let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(ri, ci)];
|
||||||
|
if ri < ctx.row_offset {
|
||||||
|
effects.push(Box::new(effect::SetRowOffset(ri)));
|
||||||
|
}
|
||||||
|
if ci < ctx.col_offset {
|
||||||
|
effects.push(Box::new(effect::SetColOffset(ci)));
|
||||||
|
}
|
||||||
|
effects.push(effect::set_status(format!(
|
||||||
|
"Match {}/{} for '{}'",
|
||||||
|
matches.iter().position(|&f| f == flat).unwrap_or(0) + 1,
|
||||||
|
matches.len(),
|
||||||
|
ctx.search_query,
|
||||||
|
)));
|
||||||
|
effects
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If search query is active, navigate backward; otherwise open CategoryAdd.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SearchOrCategoryAdd;
|
||||||
|
impl Cmd for SearchOrCategoryAdd {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"search-or-category-add"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
if !ctx.search_query.is_empty() {
|
||||||
|
SearchNavigate(false).execute(ctx)
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::SetPanelOpen {
|
||||||
|
panel: Panel::Category,
|
||||||
|
open: true,
|
||||||
|
}),
|
||||||
|
effect::change_mode(AppMode::CategoryAdd {
|
||||||
|
buffer: String::new(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page navigation ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Advance to the next page (odometer-style cycling).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PageNext;
|
||||||
|
impl Cmd for PageNext {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"page-next"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let data = page_cat_data(ctx);
|
||||||
|
if data.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
|
||||||
|
let mut carry = true;
|
||||||
|
for i in (0..data.len()).rev() {
|
||||||
|
if !carry {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
indices[i] += 1;
|
||||||
|
if indices[i] >= data[i].1.len() {
|
||||||
|
indices[i] = 0;
|
||||||
|
} else {
|
||||||
|
carry = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (cat, items, _))| {
|
||||||
|
Box::new(effect::SetPageSelection {
|
||||||
|
category: cat.clone(),
|
||||||
|
item: items[indices[i]].clone(),
|
||||||
|
}) as Box<dyn Effect>
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go to the previous page (odometer-style cycling).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PagePrev;
|
||||||
|
impl Cmd for PagePrev {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"page-prev"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let data = page_cat_data(ctx);
|
||||||
|
if data.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
|
||||||
|
let mut borrow = true;
|
||||||
|
for i in (0..data.len()).rev() {
|
||||||
|
if !borrow {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if indices[i] == 0 {
|
||||||
|
indices[i] = data[i].1.len().saturating_sub(1);
|
||||||
|
} else {
|
||||||
|
indices[i] -= 1;
|
||||||
|
borrow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (cat, items, _))| {
|
||||||
|
Box::new(effect::SetPageSelection {
|
||||||
|
category: cat.clone(),
|
||||||
|
item: items[indices[i]].clone(),
|
||||||
|
}) as Box<dyn Effect>
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gather (cat_name, items, current_idx) for page-axis categories.
|
||||||
|
fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, usize)> {
|
||||||
|
let view = ctx.model.active_view();
|
||||||
|
let page_cats: Vec<String> = view
|
||||||
|
.categories_on(Axis::Page)
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
page_cats
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|cat| {
|
||||||
|
let items: Vec<String> = ctx
|
||||||
|
.model
|
||||||
|
.category(&cat)
|
||||||
|
.map(|c| {
|
||||||
|
c.ordered_item_names()
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if items.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let current = view
|
||||||
|
.page_selection(&cat)
|
||||||
|
.map(String::from)
|
||||||
|
.or_else(|| items.first().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let idx = items.iter().position(|i| *i == current).unwrap_or(0);
|
||||||
|
Some((cat, items, idx))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Grid operations ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Toggle the row group collapse under the cursor.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToggleGroupUnderCursor;
|
||||||
|
impl Cmd for ToggleGroupUnderCursor {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"toggle-group-under-cursor"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let sel_row = ctx.selected.0;
|
||||||
|
let Some((cat, group)) = layout.row_group_for(sel_row) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
Box::new(effect::ToggleGroup {
|
||||||
|
category: cat,
|
||||||
|
group,
|
||||||
|
}),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle the column group collapse under the cursor.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToggleColGroupUnderCursor;
|
||||||
|
impl Cmd for ToggleColGroupUnderCursor {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"toggle-col-group-under-cursor"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let sel_col = ctx.selected.1;
|
||||||
|
let Some((cat, group)) = layout.col_group_for(sel_col) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
// After toggling, col_count may shrink — clamp selection
|
||||||
|
// We return ToggleGroup + MarkDirty; selection clamping will need
|
||||||
|
// to happen in the effect or in a follow-up pass
|
||||||
|
vec![
|
||||||
|
Box::new(effect::ToggleGroup {
|
||||||
|
category: cat,
|
||||||
|
group,
|
||||||
|
}),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the row item at the cursor.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HideSelectedRowItem;
|
||||||
|
impl Cmd for HideSelectedRowItem {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"hide-selected-row-item"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||||
|
let Some(cat_name) = layout.row_cats.first().cloned() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let sel_row = ctx.selected.0;
|
||||||
|
let Some(items) = layout
|
||||||
|
.row_items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| {
|
||||||
|
if let AxisEntry::DataItem(v) = e {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.nth(sel_row)
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let item_name = items[0].clone();
|
||||||
|
vec![
|
||||||
|
Box::new(effect::HideItem {
|
||||||
|
category: cat_name,
|
||||||
|
item: item_name,
|
||||||
|
}),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter tile select mode.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnterTileSelect;
|
||||||
|
impl Cmd for EnterTileSelect {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"enter-tile-select"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let count = ctx.model.category_names().len();
|
||||||
|
if count > 0 {
|
||||||
|
vec![effect::change_mode(AppMode::TileSelect { cat_idx: 0 })]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,9 +807,11 @@ mod tests {
|
|||||||
col_offset: view.col_offset,
|
col_offset: view.col_offset,
|
||||||
search_query: "",
|
search_query: "",
|
||||||
yanked: &None,
|
yanked: &None,
|
||||||
pending_key: None,
|
|
||||||
dirty: false,
|
dirty: false,
|
||||||
file_path_set: false,
|
file_path_set: false,
|
||||||
|
formula_panel_open: false,
|
||||||
|
category_panel_open: false,
|
||||||
|
view_panel_open: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,7 +868,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 = format!("{:?}", effects[0]);
|
let dbg = format!("{:?}", effects[0]);
|
||||||
assert!(dbg.contains("ChangeMode"), "Expected ChangeMode, got: {dbg}");
|
assert!(
|
||||||
|
dbg.contains("ChangeMode"),
|
||||||
|
"Expected ChangeMode, got: {dbg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -431,4 +901,133 @@ mod tests {
|
|||||||
let effects = cmd.execute(&ctx);
|
let effects = cmd.execute(&ctx);
|
||||||
assert_eq!(effects.len(), 2); // SetYanked + SetStatus
|
assert_eq!(effects.len(), 2); // SetYanked + SetStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_panel_and_focus_opens_and_enters_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = TogglePanelAndFocus(effect::Panel::Formula);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2); // SetPanelOpen + ChangeMode
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel"),
|
||||||
|
"Expected FormulaPanel mode, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_panel_and_focus_closes_when_open() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let mut ctx = make_ctx(&m);
|
||||||
|
ctx.formula_panel_open = true;
|
||||||
|
let cmd = TogglePanelAndFocus(effect::Panel::Formula);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1); // SetPanelOpen only, no mode change
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_advance_moves_down() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = EnterAdvance;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(!effects.is_empty());
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("SetSelected(1, 0)"),
|
||||||
|
"Expected row 1, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_navigate_with_empty_query_returns_nothing() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = SearchNavigate(true);
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_edit_mode_produces_editing_mode() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = EnterEditMode;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(dbg.contains("Editing"), "Expected Editing mode, got: {dbg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_tile_select_with_categories() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = EnterTileSelect;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("TileSelect"),
|
||||||
|
"Expected TileSelect mode, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_tile_select_no_categories() {
|
||||||
|
let m = Model::new("Empty");
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = EnterTileSelect;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_group_under_cursor_returns_empty_without_groups() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = ToggleGroupUnderCursor;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
// No groups defined, so nothing to toggle
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_or_category_add_without_query_opens_category_add() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = SearchOrCategoryAdd;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 2); // SetPanelOpen + ChangeMode
|
||||||
|
let dbg = format!("{:?}", effects[1]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("CategoryAdd"),
|
||||||
|
"Expected CategoryAdd, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_panel_focus_with_no_panels_open() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let ctx = make_ctx(&m);
|
||||||
|
let cmd = CyclePanelFocus;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert!(effects.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_panel_focus_with_formula_panel_open() {
|
||||||
|
let m = two_cat_model();
|
||||||
|
let mut ctx = make_ctx(&m);
|
||||||
|
ctx.formula_panel_open = true;
|
||||||
|
let cmd = CyclePanelFocus;
|
||||||
|
let effects = cmd.execute(&ctx);
|
||||||
|
assert_eq!(effects.len(), 1);
|
||||||
|
let dbg = format!("{:?}", effects[0]);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("FormulaPanel"),
|
||||||
|
"Expected FormulaPanel, got: {dbg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user