WIP improvise-2bac: interactive arm is parse-then-contextualize
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
@@ -139,9 +139,12 @@ pub trait Cmd: Debug + Send + Sync {
|
||||
/// Factory that constructs a Cmd from text arguments (headless/script).
|
||||
pub type ParseFn = fn(&[String]) -> Result<Box<dyn Cmd>, String>;
|
||||
|
||||
/// Factory constructing a Cmd from the interactive context. It takes both the keymap
|
||||
/// args and the context, so a command can combine text args with runtime state.
|
||||
pub type InteractiveFn = fn(&[String], &CmdContext) -> Result<Box<dyn Cmd>, String>;
|
||||
/// Fills the context-only fields of a command `parse` already built. It never
|
||||
/// sees the args, so it cannot discard them — `context/code/cli.md` §8.
|
||||
pub type ContextualizeFn<C> = fn(C, &CmdContext) -> C;
|
||||
|
||||
/// Builds a command from context, reached ONLY for args `parse` refused.
|
||||
pub type FallbackFn<C> = fn(&[String], &CmdContext) -> Result<C, String>;
|
||||
|
||||
type BoxParseFn = Box<dyn Fn(&[String]) -> Result<Box<dyn Cmd>, String>>;
|
||||
type BoxInteractiveFn = Box<dyn Fn(&[String], &CmdContext) -> Result<Box<dyn Cmd>, String>>;
|
||||
@@ -183,13 +186,26 @@ impl CmdRegistry {
|
||||
name
|
||||
}
|
||||
|
||||
/// Register a command with both a text parser and an interactive constructor.
|
||||
/// The name is derived from a prototype command instance.
|
||||
pub fn register(&mut self, prototype: &dyn Cmd, parse: ParseFn, interactive: InteractiveFn) {
|
||||
/// Register a command whose interactive arm is `parse` then `contextualize`, so
|
||||
/// args `parse` accepts are honoured whole — `context/code/cli.md` §8.
|
||||
pub fn register<C: Cmd + 'static>(
|
||||
&mut self,
|
||||
prototype: &C,
|
||||
parse: fn(&[String]) -> Result<C, String>,
|
||||
contextualize: ContextualizeFn<C>,
|
||||
fallback: Option<FallbackFn<C>>,
|
||||
) {
|
||||
self.entries.push(CmdEntry {
|
||||
name: prototype.name(),
|
||||
parse: Box::new(parse),
|
||||
interactive: Box::new(interactive),
|
||||
parse: Box::new(move |args| parse(args).map(|c| Box::new(c) as Box<dyn Cmd>)),
|
||||
interactive: Box::new(move |args, ctx| {
|
||||
let built = match (parse(args), fallback) {
|
||||
(Ok(cmd), _) => Ok(contextualize(cmd, ctx)),
|
||||
(Err(_), Some(fallback)) => fallback(args, ctx),
|
||||
(Err(err), None) => Err(err),
|
||||
};
|
||||
built.map(|c| Box::new(c) as Box<dyn Cmd>)
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ mod tests {
|
||||
let ctx = make_ctx(&m, &layout, ®);
|
||||
let cmd = TogglePanelAndFocus {
|
||||
panel: effect::Panel::Formula,
|
||||
open: true,
|
||||
focused: true,
|
||||
target: Some((true, true)),
|
||||
};
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 2);
|
||||
@@ -40,8 +39,7 @@ mod tests {
|
||||
let ctx = make_ctx(&m, &layout, ®);
|
||||
let cmd = TogglePanelAndFocus {
|
||||
panel: effect::Panel::Formula,
|
||||
open: false,
|
||||
focused: false,
|
||||
target: Some((false, false)),
|
||||
};
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 2);
|
||||
@@ -531,20 +529,24 @@ mod tests {
|
||||
#[derive(Debug)]
|
||||
pub struct TogglePanelAndFocus {
|
||||
pub panel: Panel,
|
||||
pub open: bool,
|
||||
pub focused: bool,
|
||||
/// Explicit `(open, focused)`; `None` toggles: close if open and focused, else open and focus.
|
||||
pub target: Option<(bool, bool)>,
|
||||
}
|
||||
impl Cmd for TogglePanelAndFocus {
|
||||
fn name(&self) -> &'static str {
|
||||
"toggle-panel-and-focus"
|
||||
}
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let (open, focused) = self.target.unwrap_or_else(|| {
|
||||
let shown = ctx.panel_open(self.panel) && ctx.mode.panel() == Some(self.panel);
|
||||
(!shown, !shown)
|
||||
});
|
||||
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||
effects.push(Box::new(effect::SetPanelOpen {
|
||||
panel: self.panel,
|
||||
open: self.open,
|
||||
open,
|
||||
}));
|
||||
if self.focused {
|
||||
if focused {
|
||||
effects.push(effect::change_mode(self.panel.mode()));
|
||||
} else {
|
||||
effects.push(effect::change_mode(AppMode::Normal));
|
||||
|
||||
@@ -22,15 +22,50 @@ use super::text_buffer::*;
|
||||
use super::tile::*;
|
||||
use super::wizard::*;
|
||||
|
||||
/// Explicit coordinates win; the cursor is the fallback — `context/code/cli.md`.
|
||||
fn key_from_args_or_cursor(args: &[String], ctx: &CmdContext) -> Result<CellKey, String> {
|
||||
if args.is_empty() {
|
||||
ctx.cell_key().clone().ok_or("no cell at cursor".into())
|
||||
} else {
|
||||
Ok(parse_cell_key_from_args(args))
|
||||
/// The cell under the cursor: what a coordinate command falls back to when given none.
|
||||
fn cursor_key(ctx: &CmdContext) -> Result<CellKey, String> {
|
||||
ctx.cell_key().clone().ok_or("no cell at cursor".into())
|
||||
}
|
||||
|
||||
/// A `Move` built from args takes the live cursor as its origin.
|
||||
fn move_from_cursor(cmd: Move, ctx: &CmdContext) -> Move {
|
||||
Move {
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
..cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// A commit built from args still advances from the live cursor.
|
||||
fn commit_from_cursor(cmd: CommitAndAdvance, ctx: &CmdContext) -> CommitAndAdvance {
|
||||
CommitAndAdvance {
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
..cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// `<mode>` alone: commit the edit buffer to the cell under the cursor.
|
||||
fn commit_at_cursor(
|
||||
args: &[String],
|
||||
ctx: &CmdContext,
|
||||
advance: AdvanceDir,
|
||||
) -> Result<CommitAndAdvance, String> {
|
||||
let [mode] = args else {
|
||||
return Err("takes a mode alone, or a mode, a value and coordinates".into());
|
||||
};
|
||||
Ok(CommitAndAdvance {
|
||||
edit_mode: parse_mode_name(mode)?,
|
||||
value: read_buffer(ctx, "edit"),
|
||||
key: cursor_key(ctx)?,
|
||||
advance,
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
/// For a command with no context-only fields: `parse`'s result is used as built.
|
||||
fn as_parsed<C>(cmd: C, _ctx: &CmdContext) -> C {
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Build the default command registry with all commands.
|
||||
/// Registry names MUST match the `Cmd::name()` return value.
|
||||
pub fn default_registry() -> CmdRegistry {
|
||||
@@ -51,14 +86,12 @@ pub fn default_registry() -> CmdRegistry {
|
||||
if args.is_empty() {
|
||||
return Err("clear-cell requires at least one Cat/Item coordinate".into());
|
||||
}
|
||||
Ok(Box::new(ClearCellCommand {
|
||||
Ok(ClearCellCommand {
|
||||
key: parse_cell_key_from_args(args),
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
let key = key_from_args_or_cursor(args, ctx)?;
|
||||
Ok(Box::new(ClearCellCommand { key }))
|
||||
})
|
||||
},
|
||||
as_parsed,
|
||||
Some(|_, ctx| Ok(ClearCellCommand { key: cursor_key(ctx)? })),
|
||||
);
|
||||
r.register_pure(&AddFormulaCmd(vec![]), AddFormulaCmd::parse);
|
||||
r.register_pure(&FitMeasure::prototype(), FitMeasure::parse);
|
||||
@@ -80,16 +113,8 @@ pub fn default_registry() -> CmdRegistry {
|
||||
r.register_pure(&ExportCmd(vec![]), ExportCmd::parse);
|
||||
r.register_pure(&WriteCmd(vec![]), WriteCmd::parse);
|
||||
r.register_pure(&HelpCmd(vec![]), HelpCmd::parse);
|
||||
r.register(
|
||||
&HelpPageNextCmd(vec![]),
|
||||
HelpPageNextCmd::parse,
|
||||
|_args, _ctx| Ok(Box::new(HelpPageNextCmd(vec![]))),
|
||||
);
|
||||
r.register(
|
||||
&HelpPagePrevCmd(vec![]),
|
||||
HelpPagePrevCmd::parse,
|
||||
|_args, _ctx| Ok(Box::new(HelpPagePrevCmd(vec![]))),
|
||||
);
|
||||
r.register_pure(&HelpPageNextCmd(vec![]), HelpPageNextCmd::parse);
|
||||
r.register_pure(&HelpPagePrevCmd(vec![]), HelpPagePrevCmd::parse);
|
||||
|
||||
// ── Navigation (unified Move) ──────────────────────────────────────
|
||||
r.register(
|
||||
@@ -102,22 +127,14 @@ pub fn default_registry() -> CmdRegistry {
|
||||
require_args(args, 2)?;
|
||||
let dr = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
let dc = args[1].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
Ok(Move {
|
||||
kind: MoveKind::Relative(dr, dc),
|
||||
cursor: CursorState::default(),
|
||||
cmd_name: "move-selection",
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 2)?;
|
||||
let dr = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
let dc = args[1].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
kind: MoveKind::Relative(dr, dc),
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
cmd_name: "move-selection",
|
||||
}))
|
||||
})
|
||||
},
|
||||
move_from_cursor,
|
||||
None,
|
||||
);
|
||||
// Jump-to-edge commands: first/last row/col
|
||||
macro_rules! reg_jump {
|
||||
@@ -133,7 +150,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
cmd_name: $name,
|
||||
},
|
||||
|_| {
|
||||
Ok(Box::new(Move {
|
||||
Ok(Move {
|
||||
kind: if $to_end {
|
||||
MoveKind::ToEnd($is_row)
|
||||
} else {
|
||||
@@ -141,19 +158,10 @@ pub fn default_registry() -> CmdRegistry {
|
||||
},
|
||||
cursor: CursorState::default(),
|
||||
cmd_name: $name,
|
||||
}))
|
||||
},
|
||||
|_, ctx| {
|
||||
Ok(Box::new(Move {
|
||||
kind: if $to_end {
|
||||
MoveKind::ToEnd($is_row)
|
||||
} else {
|
||||
MoveKind::ToStart($is_row)
|
||||
},
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
cmd_name: $name,
|
||||
}))
|
||||
})
|
||||
},
|
||||
move_from_cursor,
|
||||
None,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -170,21 +178,14 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|args| {
|
||||
require_args(args, 1)?;
|
||||
let n = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
Ok(Move {
|
||||
kind: MoveKind::Relative(n, 0),
|
||||
cursor: CursorState::default(),
|
||||
cmd_name: "scroll-rows",
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let n = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
kind: MoveKind::Relative(n, 0),
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
cmd_name: "scroll-rows",
|
||||
}))
|
||||
})
|
||||
},
|
||||
move_from_cursor,
|
||||
None,
|
||||
);
|
||||
r.register(
|
||||
&Move {
|
||||
@@ -195,28 +196,21 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|args| {
|
||||
require_args(args, 1)?;
|
||||
let dir = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
Ok(Move {
|
||||
kind: MoveKind::Page(dir),
|
||||
cursor: CursorState::default(),
|
||||
cmd_name: "page-scroll",
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let dir = args[0].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(Move {
|
||||
kind: MoveKind::Page(dir),
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
cmd_name: "page-scroll",
|
||||
}))
|
||||
})
|
||||
},
|
||||
move_from_cursor,
|
||||
None,
|
||||
);
|
||||
r.register(
|
||||
&EnterAdvance {
|
||||
cursor: CursorState::default(),
|
||||
},
|
||||
|_| {
|
||||
Ok(Box::new(EnterAdvance {
|
||||
Ok(EnterAdvance {
|
||||
cursor: CursorState {
|
||||
row: 0,
|
||||
col: 0,
|
||||
@@ -227,13 +221,12 @@ pub fn default_registry() -> CmdRegistry {
|
||||
visible_rows: 20,
|
||||
visible_cols: 8,
|
||||
},
|
||||
}))
|
||||
})
|
||||
},
|
||||
|_, ctx| {
|
||||
Ok(Box::new(EnterAdvance {
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
}))
|
||||
|_, ctx| EnterAdvance {
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
// ── Cell operations ──────────────────────────────────────────────────
|
||||
@@ -245,14 +238,12 @@ pub fn default_registry() -> CmdRegistry {
|
||||
if args.is_empty() {
|
||||
return Err("yank requires at least one Cat/Item coordinate".into());
|
||||
}
|
||||
Ok(Box::new(YankCell {
|
||||
Ok(YankCell {
|
||||
key: parse_cell_key_from_args(args),
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
let key = key_from_args_or_cursor(args, ctx)?;
|
||||
Ok(Box::new(YankCell { key }))
|
||||
})
|
||||
},
|
||||
as_parsed,
|
||||
Some(|_, ctx| Ok(YankCell { key: cursor_key(ctx)? })),
|
||||
);
|
||||
r.register(
|
||||
&PasteCell {
|
||||
@@ -262,14 +253,12 @@ pub fn default_registry() -> CmdRegistry {
|
||||
if args.is_empty() {
|
||||
return Err("paste requires at least one Cat/Item coordinate".into());
|
||||
}
|
||||
Ok(Box::new(PasteCell {
|
||||
Ok(PasteCell {
|
||||
key: parse_cell_key_from_args(args),
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
let key = key_from_args_or_cursor(args, ctx)?;
|
||||
Ok(Box::new(PasteCell { key }))
|
||||
})
|
||||
},
|
||||
as_parsed,
|
||||
Some(|_, ctx| Ok(PasteCell { key: cursor_key(ctx)? })),
|
||||
);
|
||||
// clear-cell is registered above (unified: ctx.cell_key() or explicit coords)
|
||||
|
||||
@@ -313,21 +302,15 @@ pub fn default_registry() -> CmdRegistry {
|
||||
if args.is_empty() {
|
||||
return Err("drill-into-cell requires Cat/Item coordinates".into());
|
||||
}
|
||||
Ok(Box::new(DrillIntoCell {
|
||||
Ok(DrillIntoCell {
|
||||
key: parse_cell_key_from_args(args),
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
let key = key_from_args_or_cursor(args, ctx)?;
|
||||
Ok(Box::new(DrillIntoCell { key }))
|
||||
})
|
||||
},
|
||||
as_parsed,
|
||||
Some(|_, ctx| Ok(DrillIntoCell { key: cursor_key(ctx)? })),
|
||||
);
|
||||
r.register_nullary(|| Box::new(ViewNavigate { forward: false }));
|
||||
r.register(
|
||||
&ViewNavigate { forward: true },
|
||||
|_| Ok(Box::new(ViewNavigate { forward: true })),
|
||||
|_, _| Ok(Box::new(ViewNavigate { forward: true })),
|
||||
);
|
||||
r.register_nullary(|| Box::new(ViewNavigate { forward: true }));
|
||||
r.register_pure(&EnterMode(AppMode::Normal), |args| {
|
||||
require_args(args, 1)?;
|
||||
Ok(Box::new(EnterMode(parse_mode_name(&args[0])?)))
|
||||
@@ -345,38 +328,20 @@ pub fn default_registry() -> CmdRegistry {
|
||||
r.register(
|
||||
&TogglePanelAndFocus {
|
||||
panel: Panel::Formula,
|
||||
open: true,
|
||||
focused: true,
|
||||
target: None,
|
||||
},
|
||||
|args| {
|
||||
// Parse: toggle-panel-and-focus <panel> [open] [focused]
|
||||
require_args(args, 1)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
let open = args.get(1).map(|s| s == "true").unwrap_or(true);
|
||||
let focused = args.get(2).map(|s| s == "true").unwrap_or(open);
|
||||
Ok(Box::new(TogglePanelAndFocus {
|
||||
panel,
|
||||
open,
|
||||
focused,
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
// Default interactive: if already open+focused -> close, else open+focus
|
||||
let currently_open = ctx.panel_open(panel);
|
||||
let currently_focused = ctx.mode.panel() == Some(panel);
|
||||
let (open, focused) = if currently_open && currently_focused {
|
||||
(false, false) // close
|
||||
} else {
|
||||
(true, true) // open + focus
|
||||
};
|
||||
Ok(Box::new(TogglePanelAndFocus {
|
||||
panel,
|
||||
open,
|
||||
focused,
|
||||
}))
|
||||
let target = args.get(1).map(|s| {
|
||||
let open = s == "true";
|
||||
(open, args.get(2).map(|s| s == "true").unwrap_or(open))
|
||||
});
|
||||
Ok(TogglePanelAndFocus { panel, target })
|
||||
},
|
||||
as_parsed,
|
||||
None,
|
||||
);
|
||||
r.register(
|
||||
&TogglePanelVisibility {
|
||||
@@ -386,20 +351,16 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|args| {
|
||||
require_args(args, 1)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
Ok(Box::new(TogglePanelVisibility {
|
||||
Ok(TogglePanelVisibility {
|
||||
panel,
|
||||
currently_open: false,
|
||||
}))
|
||||
})
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
let currently_open = ctx.panel_open(panel);
|
||||
Ok(Box::new(TogglePanelVisibility {
|
||||
panel,
|
||||
currently_open,
|
||||
}))
|
||||
|cmd, ctx| TogglePanelVisibility {
|
||||
currently_open: ctx.panel_open(cmd.panel),
|
||||
..cmd
|
||||
},
|
||||
None,
|
||||
);
|
||||
r.register(
|
||||
&CyclePanelFocus {
|
||||
@@ -408,19 +369,18 @@ pub fn default_registry() -> CmdRegistry {
|
||||
view_open: false,
|
||||
},
|
||||
|_| {
|
||||
Ok(Box::new(CyclePanelFocus {
|
||||
Ok(CyclePanelFocus {
|
||||
formula_open: false,
|
||||
category_open: false,
|
||||
view_open: false,
|
||||
}))
|
||||
})
|
||||
},
|
||||
|_, ctx| {
|
||||
Ok(Box::new(CyclePanelFocus {
|
||||
formula_open: ctx.formula_panel_open,
|
||||
category_open: ctx.category_panel_open,
|
||||
view_open: ctx.view_panel_open,
|
||||
}))
|
||||
|_, ctx| CyclePanelFocus {
|
||||
formula_open: ctx.formula_panel_open,
|
||||
category_open: ctx.category_panel_open,
|
||||
view_open: ctx.view_panel_open,
|
||||
},
|
||||
None,
|
||||
);
|
||||
r.register(
|
||||
&MovePanelCursor {
|
||||
@@ -433,29 +393,26 @@ pub fn default_registry() -> CmdRegistry {
|
||||
require_args(args, 2)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
let delta = args[1].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
Ok(Box::new(MovePanelCursor {
|
||||
Ok(MovePanelCursor {
|
||||
panel,
|
||||
delta,
|
||||
current: 0,
|
||||
max: 0,
|
||||
}))
|
||||
})
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 2)?;
|
||||
let panel = parse_panel(&args[0])?;
|
||||
let delta = args[1].parse::<i32>().map_err(|e| e.to_string())?;
|
||||
let (current, max) = match panel {
|
||||
|cmd, ctx| {
|
||||
let (current, max) = match cmd.panel {
|
||||
Panel::Formula => (ctx.formula_cursor, ctx.model.formulas().len()),
|
||||
Panel::Category => (ctx.cat_panel_cursor, ctx.cat_tree_len()),
|
||||
Panel::View => (ctx.view_panel_cursor, ctx.workbook.views.len()),
|
||||
};
|
||||
Ok(Box::new(MovePanelCursor {
|
||||
panel,
|
||||
delta,
|
||||
MovePanelCursor {
|
||||
current,
|
||||
max,
|
||||
}))
|
||||
..cmd
|
||||
}
|
||||
},
|
||||
None,
|
||||
);
|
||||
r.register_nullary(|| Box::new(DeleteFormulaAtCursor));
|
||||
r.register_nullary(|| Box::new(AddRecordRow));
|
||||
@@ -490,11 +447,7 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|
||||
// ── Grid operations ──────────────────────────────────────────────────
|
||||
r.register_nullary(|| Box::new(ToggleGroupAtCursor { is_row: true }));
|
||||
r.register(
|
||||
&ToggleGroupAtCursor { is_row: false },
|
||||
|_| Ok(Box::new(ToggleGroupAtCursor { is_row: false })),
|
||||
|_, _| Ok(Box::new(ToggleGroupAtCursor { is_row: false })),
|
||||
);
|
||||
r.register_nullary(|| Box::new(ToggleGroupAtCursor { is_row: false }));
|
||||
r.register_nullary(|| Box::new(HideSelectedRowItem));
|
||||
|
||||
// ── Text buffer ──────────────────────────────────────────────────────
|
||||
@@ -537,27 +490,16 @@ pub fn default_registry() -> CmdRegistry {
|
||||
return Err("commit-cell-edit requires a mode, value, and coords".into());
|
||||
}
|
||||
let edit_mode = parse_mode_name(&args[0])?;
|
||||
Ok(Box::new(CommitAndAdvance {
|
||||
Ok(CommitAndAdvance {
|
||||
key: parse_cell_key_from_args(&args[2..]),
|
||||
value: args[1].clone(),
|
||||
advance: AdvanceDir::Down,
|
||||
cursor: CursorState::default(),
|
||||
edit_mode,
|
||||
}))
|
||||
},
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let edit_mode = parse_mode_name(&args[0])?;
|
||||
let value = read_buffer(ctx, "edit");
|
||||
let key = ctx.cell_key().clone().ok_or("no cell at cursor")?;
|
||||
Ok(Box::new(CommitAndAdvance {
|
||||
key,
|
||||
value,
|
||||
advance: AdvanceDir::Down,
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
edit_mode,
|
||||
}))
|
||||
})
|
||||
},
|
||||
commit_from_cursor,
|
||||
Some(|args, ctx| commit_at_cursor(args, ctx, AdvanceDir::Down)),
|
||||
);
|
||||
r.register(
|
||||
&CommitAndAdvance {
|
||||
@@ -568,19 +510,8 @@ pub fn default_registry() -> CmdRegistry {
|
||||
edit_mode: AppMode::editing(),
|
||||
},
|
||||
|_| Err("commit-and-advance-right requires context".into()),
|
||||
|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let edit_mode = parse_mode_name(&args[0])?;
|
||||
let value = read_buffer(ctx, "edit");
|
||||
let key = ctx.cell_key().clone().ok_or("no cell at cursor")?;
|
||||
Ok(Box::new(CommitAndAdvance {
|
||||
key,
|
||||
value,
|
||||
advance: AdvanceDir::Right,
|
||||
cursor: CursorState::from_ctx(ctx),
|
||||
edit_mode,
|
||||
}))
|
||||
},
|
||||
commit_from_cursor,
|
||||
Some(|args, ctx| commit_at_cursor(args, ctx, AdvanceDir::Right)),
|
||||
);
|
||||
r.register(
|
||||
&CommitBuffer {
|
||||
@@ -589,13 +520,14 @@ pub fn default_registry() -> CmdRegistry {
|
||||
|args| {
|
||||
require_args(args, 1)?;
|
||||
let kind = parse_commit_kind(&args[0], &AppMode::Normal)?;
|
||||
Ok(Box::new(CommitBuffer { kind }))
|
||||
Ok(CommitBuffer { kind })
|
||||
},
|
||||
|args, ctx| {
|
||||
as_parsed,
|
||||
Some(|args, ctx| {
|
||||
require_args(args, 1)?;
|
||||
let kind = parse_commit_kind(&args[0], ctx.mode)?;
|
||||
Ok(Box::new(CommitBuffer { kind }))
|
||||
},
|
||||
Ok(CommitBuffer { kind })
|
||||
}),
|
||||
);
|
||||
r.register_nullary(|| Box::new(CommitExport));
|
||||
r.register_nullary(|| Box::new(CommitSave));
|
||||
@@ -880,6 +812,7 @@ mod panel_open_tests {
|
||||
ctx.registry
|
||||
.interactive("toggle-panel-and-focus", &args, &ctx)
|
||||
.expect("toggle-panel-and-focus resolves")
|
||||
.execute(&ctx)
|
||||
);
|
||||
assert!(
|
||||
dbg.contains("open: false"),
|
||||
@@ -897,6 +830,7 @@ mod panel_open_tests {
|
||||
ctx.registry
|
||||
.interactive("toggle-panel-and-focus", &args, &ctx)
|
||||
.expect("toggle-panel-and-focus resolves")
|
||||
.execute(&ctx)
|
||||
);
|
||||
assert!(
|
||||
dbg.contains("open: true"),
|
||||
|
||||
@@ -357,3 +357,82 @@ fn an_explicit_coordinate_still_beats_the_cursor() {
|
||||
"the named cell survived: {left:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// North=1, South=2, East=3 on the row axis; the cursor starts on North.
|
||||
fn three_cells(scratch: &Scratch, name: &str) -> PathBuf {
|
||||
let doc = three_rows(scratch, name);
|
||||
run_cmd(
|
||||
&[
|
||||
"set-cell 1 Region/North",
|
||||
"set-cell 2 Region/South",
|
||||
"set-cell 3 Region/East",
|
||||
],
|
||||
&doc,
|
||||
);
|
||||
assert_eq!(cell_lines(&doc).len(), 3, "the fixture wrote three cells");
|
||||
doc
|
||||
}
|
||||
|
||||
fn value_of(doc: &Path, item: &str) -> Option<String> {
|
||||
cell_lines(doc)
|
||||
.into_iter()
|
||||
.find(|l| l.contains(&format!("Region={item}")))
|
||||
.and_then(|l| l.rsplit(" = ").next().map(str::to_string))
|
||||
}
|
||||
|
||||
/// Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn commit_cell_edit_writes_the_named_cell_and_spares_the_cursors() {
|
||||
let scratch = Scratch::new("commit-explicit");
|
||||
let doc = three_cells(&scratch, "commit.improv");
|
||||
|
||||
let out = run_cmd(
|
||||
&["move-selection 1 0 . commit-cell-edit editing 5 Region/East"],
|
||||
&doc,
|
||||
);
|
||||
assert!(out.status.success(), "the line failed: {}", stderr_of(&out));
|
||||
let cells = cell_lines(&doc);
|
||||
assert_eq!(value_of(&doc, "East").as_deref(), Some("5"), "{cells:?}");
|
||||
assert_eq!(
|
||||
value_of(&doc, "South").as_deref(),
|
||||
Some("2"),
|
||||
"the cell under the cursor was touched: {cells:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
value_of(&doc, "North").as_deref(),
|
||||
Some("1"),
|
||||
"the origin cell was touched: {cells:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn a_line_whose_gate_passes_runs_whole() {
|
||||
let scratch = Scratch::new("commit-atomic");
|
||||
let doc = scratch.path("atomic.improv");
|
||||
let out = run_cmd(&["add-category Foo . commit-cell-edit editing 5 Foo/x"], &doc);
|
||||
assert!(
|
||||
!stderr_of(&out).contains("Parse error"),
|
||||
"a line the gate accepted was refused part-way: {}",
|
||||
stderr_of(&out)
|
||||
);
|
||||
let text = std::fs::read_to_string(&doc).expect("the document");
|
||||
assert!(
|
||||
text.lines().any(|l| l.contains("Foo=x") && l.ends_with("= 5")),
|
||||
"the commit did not land: {text}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Evidence: `context/test-evidence/cli.md`.
|
||||
#[test]
|
||||
fn a_malformed_commit_line_is_refused_whole() {
|
||||
let scratch = Scratch::new("commit-malformed");
|
||||
let doc = seeded(&scratch, "malformed.improv");
|
||||
let out = run_cmd(&["add-category Foo . commit-cell-edit editing 5"], &doc);
|
||||
assert!(!out.status.success(), "a malformed line exited 0");
|
||||
let text = std::fs::read_to_string(&doc).expect("the document");
|
||||
assert!(
|
||||
!text.contains("Foo"),
|
||||
"the first half of a refused line landed: {text}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user