//! App-lifecycle command nodes (ADR-0024 §migration Phase A). //! //! Eleven commands: quit, help, rebuild, save (+ save as), new, //! load, export, import, mode, messages. //! //! Each block is one `CommandNode`: entry keyword, shape, AST //! builder, help / usage references. The ast_builders match //! against the `MatchedPath` items in declaration order. use crate::dsl::command::{AppCommand, Command, CopyScope, MessagesValue, ModeValue}; use crate::dsl::grammar::{ CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError, Word, }; use crate::dsl::walker::outcome::{MatchedKind, MatchedPath}; // --- Validators ---------------------------------------------------- // // The catch-all `Ident` branches in `mode ` / // `messages ` exist solely to convert any out-of-set // identifier into a friendly `mode.unknown` / `messages.unknown` // catalog wording. The known values are `Word` siblings in the // same `Choice`, so they're never reached on the happy path — // these validators always fail. fn validate_unknown_mode(value: &str) -> Result<(), ValidationError> { Err(ValidationError { message_key: "mode.unknown", args: vec![("value", value.to_string())], }) } fn validate_unknown_messages(value: &str) -> Result<(), ValidationError> { Err(ValidationError { message_key: "messages.unknown", args: vec![("value", value.to_string())], }) } fn validate_unknown_copy(value: &str) -> Result<(), ValidationError> { Err(ValidationError { message_key: "copy.unknown", args: vec![("value", value.to_string())], }) } const UNKNOWN_MODE_VALIDATOR: IdentValidator = validate_unknown_mode; const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages; const UNKNOWN_COPY_VALIDATOR: IdentValidator = validate_unknown_copy; // --- Shapes (constants are referenced by Optional/Choice slices) -- const SAVE_AS_WORD: Node = Node::Word(Word::keyword("as")); const IMPORT_TARGET_IDENT: Node = Node::Ident { source: IdentSource::NewName, role: "target", validator: None, highlight_override: None, writes_table: false, writes_column: false, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }; const IMPORT_TARGET: Node = Node::Hinted { mode: HintMode::ForceProse("hint.ambient_typing_name"), inner: &IMPORT_TARGET_IDENT, }; const IMPORT_AS_TARGET: Node = Node::Seq(&[ Node::Word(Word::keyword("as")), IMPORT_TARGET, ]); const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET); const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGET_OPT]); const EXPORT_PATH_OPT: Node = Node::Optional(&Node::BarePath); const IMPORT_BODY_OPT: Node = Node::Optional(&IMPORT_PATH_AND_TARGET); // `help []` (H3): an optional single-word topic — a command // entry word (`insert`, `create`, `show`, …) or `types`. Captured // as a `BarePath` so any keyword-shaped word is accepted verbatim. const HELP_TOPIC_OPT: Node = Node::Optional(&Node::BarePath); // `mode `: known keywords are surfaced as `Word` children // so they appear in the walker's expected set (and feed the // completion engine's keyword candidates). The trailing `Ident` // child catches any other identifier shape and funnels it into // the friendly `mode.unknown` validator. const MODE_CHOICES: &[Node] = &[ Node::Word(Word::keyword("simple")), Node::Word(Word::keyword("advanced")), Node::Ident { source: IdentSource::Free, role: "mode_value", validator: Some(UNKNOWN_MODE_VALIDATOR), highlight_override: None, writes_table: false, writes_column: false, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }, ]; const MODE_VALUE: Node = Node::Choice(MODE_CHOICES); const MESSAGES_CHOICES: &[Node] = &[ Node::Word(Word::keyword("short")), Node::Word(Word::keyword("verbose")), Node::Ident { source: IdentSource::Free, role: "messages_value", validator: Some(UNKNOWN_MESSAGES_VALIDATOR), highlight_override: None, writes_table: false, writes_column: false, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }, ]; const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES); const MESSAGES_VALUE_OPT: Node = Node::Optional(&MESSAGES_VALUE); // `copy [all|last]`: same shape as `messages` — known scope words are // `Word` siblings (so they reach completion + the expected set); the // trailing catch-all `Ident` funnels any other word into the friendly // `copy.unknown` validator. Bare `copy` (no value) means `all`. const COPY_CHOICES: &[Node] = &[ Node::Word(Word::keyword("all")), Node::Word(Word::keyword("last")), Node::Ident { source: IdentSource::Free, role: "copy_value", validator: Some(UNKNOWN_COPY_VALIDATOR), highlight_override: None, writes_table: false, writes_column: false, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }, ]; const COPY_VALUE: Node = Node::Choice(COPY_CHOICES); const COPY_VALUE_OPT: Node = Node::Optional(©_VALUE); const EMPTY_SEQ: Node = Node::Seq(&[]); const SAVE_AS_OPT: Node = Node::Optional(&SAVE_AS_WORD); // --- AST builders -------------------------------------------------- const fn build_quit(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Quit)) } fn build_help(path: &MatchedPath, _source: &str) -> Result { // Optional single-word topic (a command entry word, or // `types`) captured as a `BarePath` — `help insert`, // `help create`, `help show`. Multi-word commands share an // entry word, so `help create` covers every create form. let topic = path .find(|i| matches!(i.kind, MatchedKind::BarePath)) .map(|i| i.text.clone()); Ok(Command::App(AppCommand::Help { topic })) } const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Rebuild)) } const fn build_undo(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Undo)) } const fn build_hint(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Hint)) } const fn build_redo(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Redo)) } fn build_save(path: &MatchedPath, _source: &str) -> Result { if path.contains_word("as") { Ok(Command::App(AppCommand::SaveAs)) } else { Ok(Command::App(AppCommand::Save)) } } const fn build_new(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::New)) } const fn build_load(_path: &MatchedPath, _source: &str) -> Result { Ok(Command::App(AppCommand::Load)) } fn build_export(path: &MatchedPath, _source: &str) -> Result { let bare = path .find(|i| matches!(i.kind, MatchedKind::BarePath)) .map(|i| i.text.clone()); Ok(Command::App(AppCommand::Export { path: bare })) } fn build_import(path: &MatchedPath, _source: &str) -> Result { let bare_path = path .find(|i| matches!(i.kind, MatchedKind::BarePath)) .map(|i| i.text.clone()) .unwrap_or_default(); let target = path .find(|i| matches!(&i.kind, MatchedKind::Ident { role, .. } if *role == "target")) .map(|i| i.text.clone()); Ok(Command::App(AppCommand::Import { path: bare_path, target, })) } fn build_mode(path: &MatchedPath, _source: &str) -> Result { // The Choice surfaces the matched value as either a `Word` // (known) or an `Ident` (unknown). The unknown branch's // validator always errors, so reaching the AST builder // implies one of the Word branches matched. let value = if path.contains_word("simple") { ModeValue::Simple } else if path.contains_word("advanced") { ModeValue::Advanced } else { ModeValue::Simple }; Ok(Command::App(AppCommand::Mode { value })) } fn build_messages(path: &MatchedPath, _source: &str) -> Result { let value = if path.contains_word("short") { Some(MessagesValue::Short) } else if path.contains_word("verbose") { Some(MessagesValue::Verbose) } else { None }; Ok(Command::App(AppCommand::Messages { value })) } fn build_copy(path: &MatchedPath, _source: &str) -> Result { // The unknown-value branch's validator always errors, so reaching // here means either a known scope word or a bare `copy` (= all). let scope = if path.contains_word("last") { CopyScope::Last } else { CopyScope::All }; Ok(Command::App(AppCommand::Copy { scope })) } // --- Command nodes ------------------------------------------------- pub static QUIT: CommandNode = CommandNode { entry: Word::keyword("quit"), shape: EMPTY_SEQ, ast_builder: build_quit, help_id: Some("app.quit"), hint_ids: &["quit"], usage_ids: &["parse.usage.quit"],}; pub static HELP: CommandNode = CommandNode { entry: Word::keyword("help"), shape: HELP_TOPIC_OPT, ast_builder: build_help, help_id: Some("app.help"), hint_ids: &["help"], usage_ids: &["parse.usage.help"],}; pub static HINT: CommandNode = CommandNode { entry: Word::keyword("hint"), shape: EMPTY_SEQ, ast_builder: build_hint, help_id: Some("app.hint"), // hint_id assigned in Phase C with the tier-3 corpus (ADR-0053). hint_ids: &["hint"], usage_ids: &["parse.usage.hint"],}; pub static REBUILD: CommandNode = CommandNode { entry: Word::keyword("rebuild"), shape: EMPTY_SEQ, ast_builder: build_rebuild, help_id: Some("app.rebuild"), hint_ids: &["rebuild"], usage_ids: &["parse.usage.rebuild"],}; pub static SAVE: CommandNode = CommandNode { entry: Word::keyword("save"), shape: SAVE_AS_OPT, ast_builder: build_save, help_id: Some("app.save"), hint_ids: &["save"], usage_ids: &["parse.usage.save"],}; pub static NEW: CommandNode = CommandNode { entry: Word::keyword("new"), shape: EMPTY_SEQ, ast_builder: build_new, help_id: Some("app.new"), hint_ids: &["new"], usage_ids: &["parse.usage.new"],}; pub static LOAD: CommandNode = CommandNode { entry: Word::keyword("load"), shape: EMPTY_SEQ, ast_builder: build_load, help_id: Some("app.load"), hint_ids: &["load"], usage_ids: &["parse.usage.load"],}; pub static EXPORT: CommandNode = CommandNode { entry: Word::keyword("export"), shape: EXPORT_PATH_OPT, ast_builder: build_export, help_id: Some("app.export"), hint_ids: &["export"], usage_ids: &["parse.usage.export"],}; pub static IMPORT: CommandNode = CommandNode { entry: Word::keyword("import"), shape: IMPORT_BODY_OPT, ast_builder: build_import, help_id: Some("app.import"), hint_ids: &["import"], usage_ids: &["parse.usage.import"],}; pub static MODE: CommandNode = CommandNode { entry: Word::keyword("mode"), shape: MODE_VALUE, ast_builder: build_mode, help_id: Some("app.mode"), hint_ids: &["mode"], usage_ids: &["parse.usage.mode"],}; pub static MESSAGES: CommandNode = CommandNode { entry: Word::keyword("messages"), shape: MESSAGES_VALUE_OPT, ast_builder: build_messages, help_id: Some("app.messages"), hint_ids: &["messages"], usage_ids: &["parse.usage.messages"],}; pub static UNDO: CommandNode = CommandNode { entry: Word::keyword("undo"), shape: EMPTY_SEQ, ast_builder: build_undo, help_id: Some("app.undo"), hint_ids: &["undo"], usage_ids: &["parse.usage.undo"],}; pub static REDO: CommandNode = CommandNode { entry: Word::keyword("redo"), shape: EMPTY_SEQ, ast_builder: build_redo, help_id: Some("app.redo"), hint_ids: &["redo"], usage_ids: &["parse.usage.redo"],}; pub static COPY: CommandNode = CommandNode { entry: Word::keyword("copy"), shape: COPY_VALUE_OPT, ast_builder: build_copy, help_id: Some("app.copy"), hint_ids: &["copy"], usage_ids: &["parse.usage.copy"],};