//! The popups driven by their key tables' commands, as the app drives them. use super::*; use crate::input::key::KeyName; use crate::input::keymap::DEFAULT_LEADER; use crate::model::test_support::garden; use crate::model::{Spot, Tags}; use crate::view::test_support::Setup; const CANVAS: &[Context] = &[Context::Canvas, Context::Normal, Context::Motions]; fn typed(command: Command) -> Typed { Typed { command, count: None, register: None } } /// Types a search into a list popup, the way `/` and the keys after it do. fn search(popup: &mut Popup, text: &str, ctx: &ViewContext<'_>) { let keymap = Keymap::new(DEFAULT_LEADER); popup.command(&typed(Command::Search), &mut Registers::default(), ctx); for c in text.chars() { popup.typing(Key::char(c), &keymap, ctx); } } /// Enter, while a search is typed. fn enter(popup: &mut Popup, ctx: &ViewContext<'_>) -> Vec { popup.typing(Key::plain(KeyName::Enter), &Keymap::new(DEFAULT_LEADER), ctx) } fn app(action: AppAction) -> Action { Action::App(action) } #[test] fn help_finds_keys_and_commands_and_runs_them() { let ((doc, g), setup) = (garden(), Setup::default()); let ctx = setup.ctx(&doc, g.root); let keymap = Keymap::new(DEFAULT_LEADER); let help = || Popup::help(&keymap, CANVAS, "canvas", "", &setup.theme); let mut popup = help(); search(&mut popup, "dd", &ctx); let dd = Command::Operate(Operator::Delete, Target::Current); assert_eq!(enter(&mut popup, &ctx), [app(AppAction::LeaveMode), app(AppAction::Run(dd))]); let mut popup = help(); search(&mut popup, "zoom in", &ctx); assert_eq!(enter(&mut popup, &ctx), [app(AppAction::LeaveMode), app(AppAction::Run(Command::ZoomIn))]); // A key that waits for more is typed, so the rest can follow. let mut popup = help(); search(&mut popup, "mark this", &ctx); let m = vec![Key::char('m')]; assert_eq!(enter(&mut popup, &ctx), [app(AppAction::LeaveMode), app(AppAction::Type(m))]); // A command that needs more opens the command line with it typed. let mut popup = Popup::help(&keymap, CANVAS, "canvas", ":color", &setup.theme); let confirm = popup.command(&typed(Command::Confirm), &mut Registers::default(), &ctx); assert_eq!(confirm, [app(AppAction::LeaveMode), app(AppAction::CommandLineWith("color ".into()))]); assert_eq!(popup.label(), "HELP"); } #[test] fn the_menu_shows_the_keys_that_do_the_same() { let keymap = Keymap::new(DEFAULT_LEADER); let tables = keymap.tables(CANVAS); assert_eq!(keys_for(&tables, Command::AddChild).as_deref(), Some("a")); assert_eq!(keys_for(&tables, Command::PickColor).as_deref(), Some("c")); assert_eq!(keys_for(&tables, Command::Operate(Operator::Delete, Target::Current)).as_deref(), Some("dd")); // On the empty canvas, the first thing is a new idea right there. let ((doc, g), setup) = (garden(), Setup::default()); let ctx = setup.ctx(&doc, g.root); let spot = CanvasPos::new(60, 30); let mut menu = Popup::menu(MenuFor::Canvas(spot), "canvas", Position::new(5, 5), &keymap, CANVAS, &setup.theme); let actions = menu.command(&typed(Command::Confirm), &mut Registers::default(), &ctx); assert_eq!(actions[0], app(AppAction::LeaveMode)); let Action::Doc(crate::app::action::DocAction::Edit { op, .. }) = &actions[1] else { panic!("an add") }; let EditOp::Add { node, spot: Spot::Loose } = op.as_ref() else { panic!("a loose node") }; assert_eq!(node.pos, spot, "where the click was"); } #[test] fn pickers_set_what_is_picked() { let ((mut doc, g), setup) = (garden(), Setup::default()); doc.apply(&EditOp::SetTags { id: g.beds, tags: Tags::parse("#wood").unwrap() }).unwrap(); let ctx = setup.ctx(&doc, g.soil); let pick = |kind| Popup::picker(kind, &ctx, &[], 0).unwrap(); let mut colors = pick(PickKind::Color); search(&mut colors, "teal", &ctx); let teal = ColorName::parse("teal").ok(); assert_eq!( enter(&mut colors, &ctx), [app(AppAction::LeaveMode), Action::edit(EditOp::SetColor { id: g.soil, color: teal })] ); let mut borders = pick(PickKind::Border); search(&mut borders, "dashed", &ctx); let dashed = Some(Border::Dashed); assert_eq!( enter(&mut borders, &ctx), [app(AppAction::LeaveMode), Action::edit(EditOp::SetBorder { id: g.soil, border: dashed })] ); let no_styles = Popup::picker(PickKind::Style, &ctx, &[], 0).unwrap_err(); assert_eq!( no_styles.text, "this theme has no named styles yet: theme files come in part 8b of the build" ); // Tags go on and off, and the picker stays open; a new one is added. let mut tags = pick(PickKind::Tags); let wood = Tag::parse("wood").unwrap(); let on = Action::edit(EditOp::Tag { id: g.soil, add: vec![wood.clone()], remove: Vec::new() }); let off = Action::edit(EditOp::Tag { id: g.soil, add: Vec::new(), remove: vec![wood] }); let mut registers = Registers::default(); assert_eq!(tags.command(&typed(Command::Confirm), &mut registers, &ctx), [on]); assert_eq!(tags.command(&typed(Command::Confirm), &mut registers, &ctx), [off]); search(&mut tags, "spring", &ctx); let spring = Tag::parse("spring").unwrap(); assert_eq!( enter(&mut tags, &ctx), [Action::edit(EditOp::Tag { id: g.soil, add: vec![spring], remove: Vec::new() })] ); // Documents: it starts on the one shown; the next one is a j away, and // one opened lately two more (past its heading). let row = |name: &str, open| DocumentRow { name: name.into(), path: format!("/notes/{name}.cum").into(), open }; let documents = [row("Garden plans", true), row("Kitchen", true), row("Reading", false)]; let mut picker = Popup::picker(PickKind::Document, &ctx, &documents, 0).unwrap(); let down = typed(Command::Move(crate::input::command::Motion::Down)); picker.command(&down, &mut registers, &ctx); assert_eq!( picker.command(&typed(Command::Confirm), &mut registers, &ctx), [app(AppAction::LeaveMode), app(AppAction::SwitchDocument(DocumentChoice::At(1)))] ); picker.command(&down, &mut registers, &ctx); let path = Some(std::path::Path::new("/notes/Reading.cum").display().to_string()); assert_eq!( picker.command(&typed(Command::Confirm), &mut registers, &ctx), [app(AppAction::LeaveMode), app(AppAction::Edit { path, force: false })] ); } #[test] fn the_card_edits_the_body_and_closes() { let ((doc, g), setup) = (garden(), Setup::default()); let ctx = setup.ctx(&doc, g.beds); let mut card = Popup::card(g.beds); assert_eq!((card.label(), card.contexts()), ("CARD", &[Context::Card][..])); let mut registers = Registers::default(); let start = StartEdit::Body { at: EditAt::End, in_card: true }; assert_eq!( card.command(&typed(Command::EditBody), &mut registers, &ctx), [app(AppAction::StartEdit(start))] ); assert_eq!(card.command(&typed(Command::Escape), &mut registers, &ctx), [app(AppAction::LeaveMode)]); }