feat: H3 help <command> per-command detail + general reference
HELP node takes an optional single-word topic (BarePath);
AppCommand::Help { topic }. note_help_topic renders the help
block(s) of every command sharing that entry word (so `help
create` covers both create forms), plus `help types` and a
friendly "no help for X" pointer for unknown topics. Full help
gains a detail-hint footer. Catalogued help.detail_hint /
help.unknown_topic; parse-error matrix updated (help now takes a
topic, so the near-miss is the multi-word case). 9 integration
tests in tests/it/help_command.rs. Mark H3 [x].
This commit is contained in:
+15
-10
@@ -696,17 +696,22 @@ since ADR-0027.)
|
|||||||
there, so the payoff is small.
|
there, so the payoff is small.
|
||||||
- [ ] **H2** `hint` provides contextual help for the current
|
- [ ] **H2** `hint` provides contextual help for the current
|
||||||
input or the most recent error.
|
input or the most recent error.
|
||||||
- [/] **H3** `help` provides general reference and per-command
|
- [x] **H3** `help` provides general reference and per-command
|
||||||
help.
|
help.
|
||||||
*(Partial, verified 2026-06-07: the `help` command
|
*(Done 2026-06-07: the **general reference** is `help` (no arg) —
|
||||||
(`app.rs:2370` `note_help`) lists all currently-supported
|
intro + the full command list (REGISTRY × `help_id`, so new
|
||||||
commands by iterating the REGISTRY + translating each
|
commands appear automatically) + the type reference + a footer
|
||||||
`CommandNode.help_id`, plus an intro and a types reference, so a
|
pointing at the focused form. **Per-command help** is `help
|
||||||
new command appears automatically. **Missing:** `help <command>`
|
<command>` (H3's new piece): the HELP node took an optional
|
||||||
per-command detail — the HELP node's shape is `EMPTY_SEQ`, so it
|
single-word topic (`BarePath`), `AppCommand::Help { topic }`,
|
||||||
takes no argument — and a fuller "general reference". These two
|
and `note_help_topic` renders the block(s) of every command
|
||||||
are the remaining pieces; the `help_id` hook is already there
|
sharing that entry word — so `help create` covers both create
|
||||||
to build per-command detail on.)*
|
forms — plus `help types` for the type reference and a friendly
|
||||||
|
"no help for `X`" pointer for an unknown topic. Help/usage
|
||||||
|
strings catalogued + key-registered; 9 integration tests
|
||||||
|
(`tests/it/help_command.rs`). A richer *narrative* overview
|
||||||
|
(modes, the `:` escape, syntax conventions) is reference-docs
|
||||||
|
scope, tracked under **DOC1** — not part of H3.)*
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
|
|||||||
+48
-2
@@ -1321,8 +1321,11 @@ impl App {
|
|||||||
use crate::dsl::{AppCommand, MessagesValue, ModeValue};
|
use crate::dsl::{AppCommand, MessagesValue, ModeValue};
|
||||||
match cmd {
|
match cmd {
|
||||||
AppCommand::Quit => vec![Action::Quit],
|
AppCommand::Quit => vec![Action::Quit],
|
||||||
AppCommand::Help => {
|
AppCommand::Help { topic } => {
|
||||||
self.note_help();
|
match &topic {
|
||||||
|
Some(t) => self.note_help_topic(t),
|
||||||
|
None => self.note_help(),
|
||||||
|
}
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
AppCommand::Rebuild => vec![Action::PrepareRebuild],
|
AppCommand::Rebuild => vec![Action::PrepareRebuild],
|
||||||
@@ -2405,6 +2408,49 @@ impl App {
|
|||||||
.lines()
|
.lines()
|
||||||
.map(str::to_string),
|
.map(str::to_string),
|
||||||
);
|
);
|
||||||
|
// H3: point at the focused per-command form.
|
||||||
|
lines.push(crate::t!("help.detail_hint"));
|
||||||
|
for line in lines {
|
||||||
|
self.note_system(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focused per-command help (H3): `help <topic>`, where `topic`
|
||||||
|
/// is a command entry word (`insert`, `create`, `show`, …) or
|
||||||
|
/// the special `types`. Renders the help block(s) of every
|
||||||
|
/// command sharing that entry word — so `help create` covers
|
||||||
|
/// both the DSL and SQL create forms — or a friendly pointer
|
||||||
|
/// back to `help` when nothing matches.
|
||||||
|
fn note_help_topic(&mut self, topic: &str) {
|
||||||
|
use crate::dsl::grammar::REGISTRY;
|
||||||
|
|
||||||
|
let topic = topic.trim();
|
||||||
|
// `help types` re-shows just the type reference.
|
||||||
|
if topic.eq_ignore_ascii_case("types") {
|
||||||
|
for line in crate::t!("help.types_reference").lines() {
|
||||||
|
self.note_system(line.to_string());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines: Vec<String> = Vec::new();
|
||||||
|
for (command, _category) in REGISTRY {
|
||||||
|
let Some(help_id) = command.help_id else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if command.entry.matches(topic) {
|
||||||
|
let key = format!("help.{help_id}");
|
||||||
|
let body = crate::friendly::translate(&key, &[]);
|
||||||
|
lines.extend(body.lines().map(str::to_string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
// No command owns that entry word — name it and point
|
||||||
|
// back at the full list rather than failing silently.
|
||||||
|
self.note_system(crate::t!("help.unknown_topic", topic = topic));
|
||||||
|
return;
|
||||||
|
}
|
||||||
for line in lines {
|
for line in lines {
|
||||||
self.note_system(line);
|
self.note_system(line);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -492,8 +492,14 @@ pub enum Command {
|
|||||||
pub enum AppCommand {
|
pub enum AppCommand {
|
||||||
/// Exit cleanly. Accepts the `q` alias.
|
/// Exit cleanly. Accepts the `q` alias.
|
||||||
Quit,
|
Quit,
|
||||||
/// Show in-app help. Body comes from `help.in_app_body`.
|
/// Show in-app help (H3). With no `topic`, the full command
|
||||||
Help,
|
/// list + types reference; with a `topic` (a command entry
|
||||||
|
/// word like `insert` / `create` / `show`, or `types`), the
|
||||||
|
/// focused detail for that command (or command group sharing
|
||||||
|
/// the entry word).
|
||||||
|
Help {
|
||||||
|
topic: Option<String>,
|
||||||
|
},
|
||||||
/// Rebuild `playground.db` from `project.yaml` + data/, with
|
/// Rebuild `playground.db` from `project.yaml` + data/, with
|
||||||
/// confirmation modal.
|
/// confirmation modal.
|
||||||
Rebuild,
|
Rebuild,
|
||||||
@@ -910,7 +916,7 @@ impl Command {
|
|||||||
Self::SqlDelete { .. } => "delete from",
|
Self::SqlDelete { .. } => "delete from",
|
||||||
Self::App(app) => match app {
|
Self::App(app) => match app {
|
||||||
AppCommand::Quit => "quit",
|
AppCommand::Quit => "quit",
|
||||||
AppCommand::Help => "help",
|
AppCommand::Help { .. } => "help",
|
||||||
AppCommand::Rebuild => "rebuild",
|
AppCommand::Rebuild => "rebuild",
|
||||||
AppCommand::Save => "save",
|
AppCommand::Save => "save",
|
||||||
AppCommand::SaveAs => "save as",
|
AppCommand::SaveAs => "save as",
|
||||||
|
|||||||
+15
-3
@@ -80,6 +80,11 @@ const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGE
|
|||||||
const EXPORT_PATH_OPT: Node = Node::Optional(&Node::BarePath);
|
const EXPORT_PATH_OPT: Node = Node::Optional(&Node::BarePath);
|
||||||
const IMPORT_BODY_OPT: Node = Node::Optional(&IMPORT_PATH_AND_TARGET);
|
const IMPORT_BODY_OPT: Node = Node::Optional(&IMPORT_PATH_AND_TARGET);
|
||||||
|
|
||||||
|
// `help [<topic>]` (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 <value>`: known keywords are surfaced as `Word` children
|
// `mode <value>`: known keywords are surfaced as `Word` children
|
||||||
// so they appear in the walker's expected set (and feed the
|
// so they appear in the walker's expected set (and feed the
|
||||||
// completion engine's keyword candidates). The trailing `Ident`
|
// completion engine's keyword candidates). The trailing `Ident`
|
||||||
@@ -154,8 +159,15 @@ const fn build_quit(_path: &MatchedPath, _source: &str) -> Result<Command, Valid
|
|||||||
Ok(Command::App(AppCommand::Quit))
|
Ok(Command::App(AppCommand::Quit))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn build_help(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
fn build_help(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||||
Ok(Command::App(AppCommand::Help))
|
// 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<Command, ValidationError> {
|
const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||||
@@ -255,7 +267,7 @@ pub static QUIT: CommandNode = CommandNode {
|
|||||||
|
|
||||||
pub static HELP: CommandNode = CommandNode {
|
pub static HELP: CommandNode = CommandNode {
|
||||||
entry: Word::keyword("help"),
|
entry: Word::keyword("help"),
|
||||||
shape: EMPTY_SEQ,
|
shape: HELP_TOPIC_OPT,
|
||||||
ast_builder: build_help,
|
ast_builder: build_help,
|
||||||
help_id: Some("app.help"),
|
help_id: Some("app.help"),
|
||||||
usage_ids: &["parse.usage.help"],};
|
usage_ids: &["parse.usage.help"],};
|
||||||
|
|||||||
@@ -3088,7 +3088,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn walker_parses_help() {
|
fn walker_parses_help() {
|
||||||
assert_eq!(parse("help").unwrap(), Command::App(AppCommand::Help));
|
assert_eq!(parse("help").unwrap(), Command::App(AppCommand::Help { topic: None }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6644,7 +6644,7 @@ mod dispatch_3a_tests {
|
|||||||
// Distinct dummy commands so a test can tell which node a walk
|
// Distinct dummy commands so a test can tell which node a walk
|
||||||
// committed to (the outcome alone doesn't distinguish them).
|
// committed to (the outcome alone doesn't distinguish them).
|
||||||
fn dsl_builder(_: &MatchedPath, _: &str) -> Result<Command, ValidationError> {
|
fn dsl_builder(_: &MatchedPath, _: &str) -> Result<Command, ValidationError> {
|
||||||
Ok(Command::App(AppCommand::Help))
|
Ok(Command::App(AppCommand::Help { topic: None }))
|
||||||
}
|
}
|
||||||
fn sql_builder(_: &MatchedPath, _: &str) -> Result<Command, ValidationError> {
|
fn sql_builder(_: &MatchedPath, _: &str) -> Result<Command, ValidationError> {
|
||||||
Ok(Command::App(AppCommand::Quit))
|
Ok(Command::App(AppCommand::Quit))
|
||||||
@@ -6729,7 +6729,7 @@ mod dispatch_3a_tests {
|
|||||||
);
|
);
|
||||||
let (outcome, cmd) = dispatch("smk dsltail", Mode::Simple, &cands);
|
let (outcome, cmd) = dispatch("smk dsltail", Mode::Simple, &cands);
|
||||||
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
||||||
assert_eq!(cmd, Some(Command::App(AppCommand::Help)));
|
assert_eq!(cmd, Some(Command::App(AppCommand::Help { topic: None })));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Exit-gate case 2: Advanced + SQL input → SQL match ----
|
// ---- Exit-gate case 2: Advanced + SQL input → SQL match ----
|
||||||
@@ -6805,7 +6805,7 @@ mod dispatch_3a_tests {
|
|||||||
);
|
);
|
||||||
let (outcome, cmd) = dispatch("smk dsltail", Mode::Advanced, &cands);
|
let (outcome, cmd) = dispatch("smk dsltail", Mode::Advanced, &cands);
|
||||||
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
||||||
assert_eq!(cmd, Some(Command::App(AppCommand::Help)));
|
assert_eq!(cmd, Some(Command::App(AppCommand::Help { topic: None })));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In advanced mode a non-shared DSL entry word (no Advanced
|
/// In advanced mode a non-shared DSL entry word (no Advanced
|
||||||
|
|||||||
+1
-1
@@ -1181,7 +1181,7 @@ mod tests {
|
|||||||
// advanced` (verb + payload).
|
// advanced` (verb + payload).
|
||||||
for app in [
|
for app in [
|
||||||
AppCommand::Quit,
|
AppCommand::Quit,
|
||||||
AppCommand::Help,
|
AppCommand::Help { topic: None },
|
||||||
AppCommand::Rebuild,
|
AppCommand::Rebuild,
|
||||||
AppCommand::Save,
|
AppCommand::Save,
|
||||||
AppCommand::New,
|
AppCommand::New,
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("help.intro", &[]),
|
("help.intro", &[]),
|
||||||
("help.dsl_section", &[]),
|
("help.dsl_section", &[]),
|
||||||
("help.types_reference", &[]),
|
("help.types_reference", &[]),
|
||||||
|
("help.detail_hint", &[]),
|
||||||
|
("help.unknown_topic", &["topic"]),
|
||||||
("help.app.quit", &[]),
|
("help.app.quit", &[]),
|
||||||
("help.app.help", &[]),
|
("help.app.help", &[]),
|
||||||
("help.app.rebuild", &[]),
|
("help.app.rebuild", &[]),
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ help:
|
|||||||
# per line so scroll math stays accurate.
|
# per line so scroll math stays accurate.
|
||||||
intro: "Supported commands:"
|
intro: "Supported commands:"
|
||||||
dsl_section: "DSL data commands (in simple mode):"
|
dsl_section: "DSL data commands (in simple mode):"
|
||||||
|
# H3: footer on the full `help` list, and the not-found note
|
||||||
|
# for `help <topic>`. `{topic}` is the word the user typed.
|
||||||
|
detail_hint: "Type `help <command>` for detail on one command (e.g. `help insert`), or `help types` for the type reference."
|
||||||
|
unknown_topic: "No help for `{topic}`. Type `help` for the full command list, or `help types` for the type reference."
|
||||||
# Per-command help, keyed by `CommandNode.help_id`. Block
|
# Per-command help, keyed by `CommandNode.help_id`. Block
|
||||||
# scalars (`|-`) so the column alignment survives — the
|
# scalars (`|-`) so the column alignment survives — the
|
||||||
# double-quoted form trips a libyml scanner bug on long
|
# double-quoted form trips a libyml scanner bug on long
|
||||||
@@ -248,6 +252,7 @@ help:
|
|||||||
quit — exit the app
|
quit — exit the app
|
||||||
help: |-
|
help: |-
|
||||||
help — show this command list
|
help — show this command list
|
||||||
|
help <command> — detailed help for one command (e.g. `help insert`)
|
||||||
rebuild: |-
|
rebuild: |-
|
||||||
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
|
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
|
||||||
save: |-
|
save: |-
|
||||||
@@ -582,7 +587,7 @@ parse:
|
|||||||
# listing in `help.in_app_body` carries the user-facing
|
# listing in `help.in_app_body` carries the user-facing
|
||||||
# description.
|
# description.
|
||||||
quit: "quit"
|
quit: "quit"
|
||||||
help: "help"
|
help: "help [<command>]"
|
||||||
rebuild: "rebuild"
|
rebuild: "rebuild"
|
||||||
save: "save | save as"
|
save: "save | save as"
|
||||||
new: "new"
|
new: "new"
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
//! Integration tests for `help` and `help <command>` (H3).
|
||||||
|
//!
|
||||||
|
//! Covers:
|
||||||
|
//! - Parse layer: `help` → `Help { topic: None }`; `help insert`
|
||||||
|
//! → `Help { topic: Some("insert") }`.
|
||||||
|
//! - App behaviour: the full `help` ends with the detail hint;
|
||||||
|
//! `help <command>` renders that command's block (and every
|
||||||
|
//! form sharing the entry word); `help types` renders the type
|
||||||
|
//! reference; an unknown topic gets a friendly pointer back.
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
|
||||||
|
use rdbms_playground::app::App;
|
||||||
|
use rdbms_playground::dsl::{parse_command, AppCommand, Command};
|
||||||
|
use rdbms_playground::event::AppEvent;
|
||||||
|
|
||||||
|
const fn key(code: KeyCode) -> AppEvent {
|
||||||
|
AppEvent::Key(KeyEvent {
|
||||||
|
code,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
state: crossterm::event::KeyEventState::NONE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_str(app: &mut App, s: &str) {
|
||||||
|
for c in s.chars() {
|
||||||
|
app.update(key(KeyCode::Char(c)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Submit `input` to a fresh app and collect all output text.
|
||||||
|
fn output_for(input: &str) -> Vec<String> {
|
||||||
|
let mut app = App::new();
|
||||||
|
type_str(&mut app, input);
|
||||||
|
app.update(key(KeyCode::Enter));
|
||||||
|
app.output.iter().map(|l| l.text.clone()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Parse layer
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_help_parses_with_no_topic() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_command("help").expect("parses"),
|
||||||
|
Command::App(AppCommand::Help { topic: None }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_with_topic_captures_the_word() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_command("help insert").expect("parses"),
|
||||||
|
Command::App(AppCommand::Help {
|
||||||
|
topic: Some("insert".to_string())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_topic_is_a_single_word_multi_word_is_a_parse_error() {
|
||||||
|
// Entry-word topics cover multi-word commands (`help create`),
|
||||||
|
// so a second word is trailing junk, not a longer topic.
|
||||||
|
assert!(parse_command("help foo bar").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// App behaviour
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_help_lists_commands_and_ends_with_the_detail_hint() {
|
||||||
|
let out = output_for("help");
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|l| l == "Supported commands:"),
|
||||||
|
"intro present: {out:?}",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|l| l.contains("help <command>")),
|
||||||
|
"detail-hint footer present: {out:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_insert_renders_the_insert_block() {
|
||||||
|
let out = output_for("help insert");
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|l| l.contains("insert into")),
|
||||||
|
"insert help shown: {out:?}",
|
||||||
|
);
|
||||||
|
// Focused: it must NOT dump the whole list — the intro header
|
||||||
|
// belongs to the full `help` only.
|
||||||
|
assert!(
|
||||||
|
!out.iter().any(|l| l == "Supported commands:"),
|
||||||
|
"focused help omits the full-list intro: {out:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_create_covers_every_form_sharing_the_entry_word() {
|
||||||
|
// `create` is the entry word for both the DSL `create table`
|
||||||
|
// and the advanced SQL `CREATE TABLE` — `help create` shows
|
||||||
|
// both blocks.
|
||||||
|
let out = output_for("help create");
|
||||||
|
let joined = out.join("\n");
|
||||||
|
assert!(
|
||||||
|
joined.contains("create table"),
|
||||||
|
"DSL create form shown: {out:?}",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
joined.to_lowercase().matches("create").count() >= 2,
|
||||||
|
"more than one create form shown: {out:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_types_renders_the_type_reference() {
|
||||||
|
let out = output_for("help types");
|
||||||
|
let joined = out.join("\n").to_lowercase();
|
||||||
|
// The type reference names the playground types.
|
||||||
|
assert!(
|
||||||
|
joined.contains("serial") || joined.contains("shortid"),
|
||||||
|
"type reference shown: {out:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_unknown_topic_points_back_to_the_full_list() {
|
||||||
|
let out = output_for("help wibble");
|
||||||
|
assert!(
|
||||||
|
out.iter()
|
||||||
|
.any(|l| l.contains("No help for") && l.contains("wibble")),
|
||||||
|
"names the unknown topic: {out:?}",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.iter().any(|l| l.contains("Type `help`")),
|
||||||
|
"points back at the full list: {out:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ mod case_insensitive_names;
|
|||||||
mod column_op_guards;
|
mod column_op_guards;
|
||||||
mod engine_vocabulary_audit;
|
mod engine_vocabulary_audit;
|
||||||
mod friendly_enrichment;
|
mod friendly_enrichment;
|
||||||
|
mod help_command;
|
||||||
mod iteration2_persistence;
|
mod iteration2_persistence;
|
||||||
mod iteration3_rebuild;
|
mod iteration3_rebuild;
|
||||||
mod iteration4a_rebuild_command;
|
mod iteration4a_rebuild_command;
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ fn near_miss_matrix_simple_mode() {
|
|||||||
// trailing junk with "expected end of input" + their usage
|
// trailing junk with "expected end of input" + their usage
|
||||||
// (audited 2026-06-05); locked here as regression insurance.
|
// (audited 2026-06-05); locked here as regression insurance.
|
||||||
("quit now", &["after `quit`, expected end of input", " quit"]),
|
("quit now", &["after `quit`, expected end of input", " quit"]),
|
||||||
("help foo", &["after `help`, expected end of input", " help"]),
|
// `help` now takes an optional single-word topic (H3), so
|
||||||
|
// `help foo` parses (topic lookup); only a *multi-word*
|
||||||
|
// topic is the near-miss that rejects trailing junk.
|
||||||
|
("help foo bar", &["after `help foo`, expected end of input", "help [<command>]"]),
|
||||||
("rebuild now", &["after `rebuild`, expected end of input", " rebuild"]),
|
("rebuild now", &["after `rebuild`, expected end of input", " rebuild"]),
|
||||||
("new foo", &["after `new`, expected end of input", " new"]),
|
("new foo", &["after `new`, expected end of input", " new"]),
|
||||||
("load foo", &["after `load`, expected end of input", " load"]),
|
("load foo", &["after `load`, expected end of input", " load"]),
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
|||||||
SqlDelete { .. } => "SqlDelete".into(),
|
SqlDelete { .. } => "SqlDelete".into(),
|
||||||
App(app) => match app {
|
App(app) => match app {
|
||||||
AppCommand::Quit => "App(Quit)".into(),
|
AppCommand::Quit => "App(Quit)".into(),
|
||||||
AppCommand::Help => "App(Help)".into(),
|
AppCommand::Help { .. } => "App(Help)".into(),
|
||||||
AppCommand::Rebuild => "App(Rebuild)".into(),
|
AppCommand::Rebuild => "App(Rebuild)".into(),
|
||||||
AppCommand::Save => "App(Save)".into(),
|
AppCommand::Save => "App(Save)".into(),
|
||||||
AppCommand::SaveAs => "App(SaveAs)".into(),
|
AppCommand::SaveAs => "App(SaveAs)".into(),
|
||||||
|
|||||||
Reference in New Issue
Block a user