round-5 follow-up: completion + i18n sweep
Four user-reported gaps from the round-4 testing pass:
1. Empty-prompt hint reworded from "(no active hint)" to
"Type a command — press Tab for options, `help` for a
list" (6 snapshots updated to reflect 80-col truncation).
2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
new, load, export, import, mode, messages) now flow through
the DSL parser:
- 15 new keywords + catalog token entries
- new Command::App(AppCommand) AST with 11 variants
- parse-first dispatch in submit() (app commands work in
both simple and advanced modes)
- pre-chumsky source-slice for `export <path>` /
`import <zip> [as <target>]` mirrors the replay precedent
- UsageEntry registry entries so parse errors surface
relevant usage templates
- `mode <bad>` / `messages <bad>` use try_map for the
friendly "unknown mode/messages" wording
3. DSL completion gaps:
- `1:n` surfaces as a composite candidate at `add `
- --all-rows / --create-fk / --force-conversion /
--dont-convert surface as new CandidateKind::Flag
candidates (coloured with tok_flag in hint panel)
- filter_clause .labelled() wrap removed so chumsky's
expected-set surfaces the constituent options
4. Hardcoded user-facing strings migrated to catalog:
- 4 parser custom errors (incl. the known "tables need at
least one column" wart)
- UnknownType Display now via parse.custom.unknown_type
- UI panel titles + mode labels (Output / Hint / SIMPLE /
ADVANCED / Advanced:)
- app.rs cascade rendering (action labels + summary)
- runtime --resume CLI stderr
- db.rs change-column diagnostic tables (7 headers + 3
wrapper summaries + force-conversion hint)
Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.
Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
(DbError, ArgsError, ArchiveError, PersistenceError,
LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
implicitly via parse-first dispatch; broader ADR
amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
can't offer `as` because `save` parses bare; same shape
as `--create-fk` after a complete `add relationship`).
This commit is contained in:
@@ -142,6 +142,60 @@ pub enum Command {
|
||||
Replay {
|
||||
path: String,
|
||||
},
|
||||
/// App-lifecycle command (per ADR-0003). These work in both
|
||||
/// simple and advanced modes; the dispatcher branches on the
|
||||
/// `Command::App(...)` variant before mode-specific routing.
|
||||
/// Folded into the DSL parser so they participate in Tab
|
||||
/// completion + parse-error usage templates alongside the
|
||||
/// data commands.
|
||||
App(AppCommand),
|
||||
}
|
||||
|
||||
/// App-level commands surfaced through the DSL parser. These do
|
||||
/// not touch the database schema or data — they affect app
|
||||
/// lifecycle, mode, persistence, and verbosity.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AppCommand {
|
||||
/// Exit cleanly. Accepts the `q` alias.
|
||||
Quit,
|
||||
/// Show in-app help. Body comes from `help.in_app_body`.
|
||||
Help,
|
||||
/// Rebuild `playground.db` from `project.yaml` + data/, with
|
||||
/// confirmation modal.
|
||||
Rebuild,
|
||||
/// Save the current project under a name (modal-driven).
|
||||
Save,
|
||||
/// Save the current project as a copy under a new path
|
||||
/// (modal-driven).
|
||||
SaveAs,
|
||||
/// Close current, create a fresh temp project.
|
||||
New,
|
||||
/// Open the project picker modal.
|
||||
Load,
|
||||
/// Write a zip of project.yaml + data/. `path` is the user-
|
||||
/// typed target (may be a name under the data root or an
|
||||
/// absolute path). `None` opens the path prompt modal.
|
||||
Export { path: Option<String> },
|
||||
/// Unpack a zip into a new project and switch to it.
|
||||
/// `target` overrides the project name (default: taken from
|
||||
/// the zip).
|
||||
Import { path: String, target: Option<String> },
|
||||
/// Switch the persistent input mode.
|
||||
Mode { value: ModeValue },
|
||||
/// Show or set the messages verbosity.
|
||||
Messages { value: Option<MessagesValue> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeValue {
|
||||
Simple,
|
||||
Advanced,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MessagesValue {
|
||||
Short,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
/// Conversion mode for `change column …` (ADR-0017 §5).
|
||||
@@ -218,6 +272,19 @@ impl Command {
|
||||
Self::Delete { .. } => "delete from",
|
||||
Self::ShowData { .. } => "show data",
|
||||
Self::Replay { .. } => "replay",
|
||||
Self::App(app) => match app {
|
||||
AppCommand::Quit => "quit",
|
||||
AppCommand::Help => "help",
|
||||
AppCommand::Rebuild => "rebuild",
|
||||
AppCommand::Save => "save",
|
||||
AppCommand::SaveAs => "save as",
|
||||
AppCommand::New => "new",
|
||||
AppCommand::Load => "load",
|
||||
AppCommand::Export { .. } => "export",
|
||||
AppCommand::Import { .. } => "import",
|
||||
AppCommand::Mode { .. } => "mode",
|
||||
AppCommand::Messages { .. } => "messages",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +321,11 @@ impl Command {
|
||||
// Replay isn't tied to a single table; the path is
|
||||
// the most identifying thing for log output.
|
||||
Self::Replay { path } => path,
|
||||
// App commands aren't tied to schema entities — the
|
||||
// verb is the most identifying thing. The
|
||||
// display_subject override below provides a richer
|
||||
// form when one exists.
|
||||
Self::App(_) => "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,31 @@ define_keywords! {
|
||||
Restrict => "restrict",
|
||||
Action => "action",
|
||||
No => "no",
|
||||
// App-lifecycle commands (folded into the DSL parser so they
|
||||
// surface in Tab completion and the parse-error usage
|
||||
// templates). The dispatch handlers in app.rs branch on the
|
||||
// parsed `Command::App(...)` variant before mode-specific
|
||||
// routing so these work in both simple and advanced modes
|
||||
// (per ADR-0003).
|
||||
Quit => "quit",
|
||||
Q => "q",
|
||||
Help => "help",
|
||||
Rebuild => "rebuild",
|
||||
Save => "save",
|
||||
New => "new",
|
||||
Load => "load",
|
||||
Export => "export",
|
||||
Import => "import",
|
||||
Mode => "mode",
|
||||
Messages => "messages",
|
||||
// Value vocabulary for `mode <value>` and `messages <value>`.
|
||||
// Free as identifier-shapes outside their slots (no command
|
||||
// uses `simple` / `advanced` / `short` / `verbose` as an
|
||||
// entity name today).
|
||||
Simple => "simple",
|
||||
Advanced => "advanced",
|
||||
Short => "short",
|
||||
Verbose => "verbose",
|
||||
}
|
||||
|
||||
macro_rules! define_punct {
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ pub mod value;
|
||||
|
||||
pub use action::ReferentialAction;
|
||||
pub use command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, RelationshipSelector, RowFilter,
|
||||
AppCommand, ChangeColumnMode, ColumnSpec, Command, MessagesValue, ModeValue,
|
||||
RelationshipSelector, RowFilter,
|
||||
};
|
||||
pub use parser::{ParseError, parse_command};
|
||||
pub use types::Type;
|
||||
|
||||
+153
-13
@@ -15,7 +15,8 @@ use chumsky::prelude::*;
|
||||
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, ColumnSpec, Command, RelationshipSelector, RowFilter,
|
||||
AppCommand, ChangeColumnMode, ColumnSpec, Command, MessagesValue, ModeValue,
|
||||
RelationshipSelector, RowFilter,
|
||||
};
|
||||
use crate::dsl::ident_slot::IdentSlot;
|
||||
use crate::dsl::keyword::{Keyword, Punct};
|
||||
@@ -101,6 +102,9 @@ pub fn parse_tokens(tokens: &[Token], source: &str) -> Result<Command, ParseErro
|
||||
if let Some(result) = try_parse_replay_with_bare_path(tokens, source) {
|
||||
return result;
|
||||
}
|
||||
if let Some(result) = try_parse_app_path_command(tokens, source) {
|
||||
return result;
|
||||
}
|
||||
match command_parser().parse(tokens).into_result() {
|
||||
Ok(cmd) => Ok(cmd),
|
||||
Err(errs) => Err(into_parse_error(&errs, tokens, source)),
|
||||
@@ -139,7 +143,7 @@ fn try_parse_replay_with_bare_path(
|
||||
// of error chumsky would (positioned where the path
|
||||
// should have started).
|
||||
return Some(Err(ParseError::Invalid {
|
||||
message: "expected a path after `replay`".to_string(),
|
||||
message: crate::t!("parse.custom.replay_path_expected"),
|
||||
position: after_replay,
|
||||
at_eof: true,
|
||||
expected: vec!["path".to_string()],
|
||||
@@ -150,6 +154,65 @@ fn try_parse_replay_with_bare_path(
|
||||
}))
|
||||
}
|
||||
|
||||
/// `export <path>` / `import <path> [as <target>]` source-slice
|
||||
/// special case. Same rationale as `try_parse_replay_with_bare_path`
|
||||
/// — bare paths contain `/`, `.`, `~` which the lexer would either
|
||||
/// split into separate tokens or refuse outright.
|
||||
///
|
||||
/// Returns `None` for the bare-keyword forms (`export`, `import`
|
||||
/// alone), letting the regular chumsky path handle them and
|
||||
/// surface the no-arg `Command::App(...)` variant.
|
||||
fn try_parse_app_path_command(
|
||||
tokens: &[Token],
|
||||
source: &str,
|
||||
) -> Option<Result<Command, ParseError>> {
|
||||
use crate::dsl::command::AppCommand;
|
||||
let first = tokens.first()?;
|
||||
let kw = match &first.kind {
|
||||
TokenKind::Keyword(Keyword::Export) => Keyword::Export,
|
||||
TokenKind::Keyword(Keyword::Import) => Keyword::Import,
|
||||
_ => return None,
|
||||
};
|
||||
let after = first.span.1;
|
||||
let rest = source[after..].trim();
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match kw {
|
||||
Keyword::Export => Some(Ok(Command::App(AppCommand::Export {
|
||||
path: Some(rest.to_string()),
|
||||
}))),
|
||||
Keyword::Import => {
|
||||
// Trailing `as` with no target is a recognised user
|
||||
// mistake — surface the usage hint as a parse error
|
||||
// (catalog wording stays in sync with the existing
|
||||
// dispatch-time error).
|
||||
if rest == "as" || rest.ends_with(" as") {
|
||||
return Some(Err(ParseError::Invalid {
|
||||
message: crate::t!("project.import_empty_target"),
|
||||
position: after + rest.len(),
|
||||
at_eof: true,
|
||||
expected: Vec::new(),
|
||||
}));
|
||||
}
|
||||
let (path, target) = match rest.split_once(" as ") {
|
||||
Some((p, t)) => (p.trim().to_string(), Some(t.trim().to_string())),
|
||||
None => (rest.to_string(), None),
|
||||
};
|
||||
if path.is_empty() {
|
||||
return Some(Err(ParseError::Invalid {
|
||||
message: crate::t!("project.import_usage"),
|
||||
position: after,
|
||||
at_eof: true,
|
||||
expected: vec!["path".to_string()],
|
||||
}));
|
||||
}
|
||||
Some(Ok(Command::App(AppCommand::Import { path, target })))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Token-aware combinator helpers (ADR-0020 §5)
|
||||
// =========================================================
|
||||
@@ -287,10 +350,7 @@ fn command_parser<'a>()
|
||||
if pk_specs.is_empty() {
|
||||
return Err(Rich::custom(
|
||||
span,
|
||||
"tables need at least one column. Add `with pk` for a default \
|
||||
`id INTEGER PRIMARY KEY`, or `with pk <name>:<type>` to choose. \
|
||||
Use a comma-separated list for compound primary keys."
|
||||
.to_string(),
|
||||
crate::t!("parse.custom.create_table_needs_pk"),
|
||||
));
|
||||
}
|
||||
let columns: Vec<ColumnSpec> = pk_specs
|
||||
@@ -390,6 +450,66 @@ fn command_parser<'a>()
|
||||
.ignore_then(string_payload())
|
||||
.map(|path| Command::Replay { path });
|
||||
|
||||
// ---- App-lifecycle commands -----------------------------
|
||||
// No-arg variants and the keyword-value variants. Path-
|
||||
// bearing variants (`export <path>`, `import <zip> [as
|
||||
// <target>]`) are handled by `try_parse_app_path_command`
|
||||
// BEFORE chumsky runs; the bare-keyword forms below
|
||||
// surface the `Path: None` / no-source variants for
|
||||
// empty-prompt completion + usage rendering.
|
||||
let quit_cmd = choice((kw(Keyword::Quit), kw(Keyword::Q)))
|
||||
.map(|()| Command::App(AppCommand::Quit));
|
||||
let help_cmd = kw(Keyword::Help).map(|()| Command::App(AppCommand::Help));
|
||||
let rebuild_cmd =
|
||||
kw(Keyword::Rebuild).map(|()| Command::App(AppCommand::Rebuild));
|
||||
// `save as` must be tried before bare `save` (more specific).
|
||||
let save_as_cmd = kw(Keyword::Save)
|
||||
.then_ignore(kw(Keyword::As))
|
||||
.map(|()| Command::App(AppCommand::SaveAs));
|
||||
let save_cmd = kw(Keyword::Save).map(|()| Command::App(AppCommand::Save));
|
||||
let new_cmd = kw(Keyword::New).map(|()| Command::App(AppCommand::New));
|
||||
let load_cmd = kw(Keyword::Load).map(|()| Command::App(AppCommand::Load));
|
||||
let export_no_arg =
|
||||
kw(Keyword::Export).map(|()| Command::App(AppCommand::Export { path: None }));
|
||||
let import_no_arg = kw(Keyword::Import).map(|()| {
|
||||
Command::App(AppCommand::Import {
|
||||
path: String::new(),
|
||||
target: None,
|
||||
})
|
||||
});
|
||||
// `mode <value>` and `messages [<value>]` accept either the
|
||||
// known keyword forms or any identifier — the identifier
|
||||
// branch funnels through `try_map` into a friendly
|
||||
// `mode.unknown` / `messages.unknown` error rather than the
|
||||
// generic structural-error wording. Mirrors the type-name
|
||||
// pattern in `type_keyword` (ADR-0020 §4).
|
||||
let known_mode = choice((
|
||||
kw(Keyword::Simple).to(ModeValue::Simple),
|
||||
kw(Keyword::Advanced).to(ModeValue::Advanced),
|
||||
));
|
||||
let unknown_mode = ident_inner().try_map(|s, span| {
|
||||
Err::<ModeValue, _>(Rich::custom(
|
||||
span,
|
||||
crate::t!("mode.unknown", value = s),
|
||||
))
|
||||
});
|
||||
let mode_cmd = kw(Keyword::Mode)
|
||||
.ignore_then(choice((known_mode, unknown_mode)))
|
||||
.map(|value| Command::App(AppCommand::Mode { value }));
|
||||
let known_messages = choice((
|
||||
kw(Keyword::Short).to(MessagesValue::Short),
|
||||
kw(Keyword::Verbose).to(MessagesValue::Verbose),
|
||||
));
|
||||
let unknown_messages = ident_inner().try_map(|s, span| {
|
||||
Err::<MessagesValue, _>(Rich::custom(
|
||||
span,
|
||||
crate::t!("messages.unknown", value = s),
|
||||
))
|
||||
});
|
||||
let messages_cmd = kw(Keyword::Messages)
|
||||
.ignore_then(choice((known_messages, unknown_messages)).or_not())
|
||||
.map(|value| Command::App(AppCommand::Messages { value }));
|
||||
|
||||
choice((
|
||||
create_table,
|
||||
// `drop column` and `drop relationship` come before
|
||||
@@ -408,6 +528,19 @@ fn command_parser<'a>()
|
||||
update_cmd,
|
||||
delete_cmd,
|
||||
replay,
|
||||
// App commands. `save as` before bare `save`; everything
|
||||
// else order-agnostic.
|
||||
quit_cmd,
|
||||
help_cmd,
|
||||
rebuild_cmd,
|
||||
save_as_cmd,
|
||||
save_cmd,
|
||||
new_cmd,
|
||||
load_cmd,
|
||||
export_no_arg,
|
||||
import_no_arg,
|
||||
mode_cmd,
|
||||
messages_cmd,
|
||||
))
|
||||
.then_ignore(end())
|
||||
}
|
||||
@@ -505,9 +638,15 @@ fn filter_clause<'a>()
|
||||
|
||||
let all_rows = flag("all-rows").to(RowFilter::AllRows);
|
||||
|
||||
where_clause
|
||||
.or(all_rows)
|
||||
.labelled("where clause or --all-rows")
|
||||
// No `.labelled()` wrap here: chumsky's expected-set then
|
||||
// surfaces the constituent options (`` `where` ``,
|
||||
// `` `--all-rows` ``) individually instead of collapsing
|
||||
// them to a single descriptive label. The completion
|
||||
// engine needs the constituents to offer Tab candidates
|
||||
// (ADR-0022 §8); the resulting error prose ("expected `,`,
|
||||
// `where`, or `--all-rows`") reads cleanly enough without
|
||||
// hand-wrapping.
|
||||
where_clause.or(all_rows)
|
||||
}
|
||||
|
||||
fn value_literal<'a>()
|
||||
@@ -625,7 +764,10 @@ fn referential_clauses<'a>() -> impl Parser<
|
||||
if slot.is_some() {
|
||||
return Err(Rich::custom(
|
||||
span,
|
||||
format!("`on {target}` specified twice"),
|
||||
crate::t!(
|
||||
"parse.custom.on_action_specified_twice",
|
||||
target = target,
|
||||
),
|
||||
));
|
||||
}
|
||||
*slot = Some(action);
|
||||
@@ -683,9 +825,7 @@ fn change_column_flags<'a>()
|
||||
[single] => Ok(*single),
|
||||
_ => Err(Rich::custom(
|
||||
span,
|
||||
"`--force-conversion` and `--dont-convert` are mutually \
|
||||
exclusive — pick one."
|
||||
.to_string(),
|
||||
crate::t!("parse.custom.change_column_flags_exclusive"),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
+18
-4
@@ -130,15 +130,29 @@ impl fmt::Display for Type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when parsing a type keyword that isn't
|
||||
/// recognised.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("unknown type '{found}' (expected one of: {expected})")]
|
||||
/// Error returned when parsing a type keyword that isn't recognised.
|
||||
///
|
||||
/// Display formatting flows through the i18n catalog
|
||||
/// (`parse.custom.unknown_type`); call sites that do
|
||||
/// `err.to_string()` get the localised wording for free.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UnknownType {
|
||||
pub found: String,
|
||||
pub expected: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for UnknownType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&crate::t!(
|
||||
"parse.custom.unknown_type",
|
||||
found = self.found,
|
||||
expected = self.expected,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UnknownType {}
|
||||
|
||||
impl FromStr for Type {
|
||||
type Err = UnknownType;
|
||||
|
||||
|
||||
+72
-8
@@ -92,6 +92,53 @@ pub const REGISTRY: &[UsageEntry] = &[
|
||||
entry: Keyword::Replay,
|
||||
catalog_key: "parse.usage.replay",
|
||||
},
|
||||
// App-lifecycle commands. Registered alongside DSL commands
|
||||
// so parse-error rendering surfaces a relevant usage block
|
||||
// when (e.g.) the user types `mode foo` or `import` alone.
|
||||
UsageEntry {
|
||||
entry: Keyword::Quit,
|
||||
catalog_key: "parse.usage.quit",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Q,
|
||||
catalog_key: "parse.usage.quit",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Help,
|
||||
catalog_key: "parse.usage.help",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Rebuild,
|
||||
catalog_key: "parse.usage.rebuild",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Save,
|
||||
catalog_key: "parse.usage.save",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::New,
|
||||
catalog_key: "parse.usage.new",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Load,
|
||||
catalog_key: "parse.usage.load",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Export,
|
||||
catalog_key: "parse.usage.export",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Import,
|
||||
catalog_key: "parse.usage.import",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Mode,
|
||||
catalog_key: "parse.usage.mode",
|
||||
},
|
||||
UsageEntry {
|
||||
entry: Keyword::Messages,
|
||||
catalog_key: "parse.usage.messages",
|
||||
},
|
||||
];
|
||||
|
||||
/// Find the entry-keyword whose grammar to illustrate.
|
||||
@@ -155,11 +202,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_command_has_a_registry_entry() {
|
||||
// The parser recognises ten command-entry keywords
|
||||
// (ADR-0009 + ADR-0006 + ADR-0014). Each MUST be
|
||||
// represented in the registry — otherwise a parse error
|
||||
// for that command renders no usage block and the H1a
|
||||
// pedagogy gap reopens for that family.
|
||||
// Every command-entry keyword recognised by the parser
|
||||
// MUST be represented in the registry — otherwise a
|
||||
// parse error for that command renders no usage block
|
||||
// and the H1a pedagogy gap reopens for that family.
|
||||
// Round 5 added the app-lifecycle entry keywords
|
||||
// alongside the original ten DSL entry keywords.
|
||||
for entry in [
|
||||
Keyword::Create,
|
||||
Keyword::Drop,
|
||||
@@ -171,6 +219,17 @@ mod tests {
|
||||
Keyword::Update,
|
||||
Keyword::Delete,
|
||||
Keyword::Replay,
|
||||
Keyword::Quit,
|
||||
Keyword::Q,
|
||||
Keyword::Help,
|
||||
Keyword::Rebuild,
|
||||
Keyword::Save,
|
||||
Keyword::New,
|
||||
Keyword::Load,
|
||||
Keyword::Export,
|
||||
Keyword::Import,
|
||||
Keyword::Mode,
|
||||
Keyword::Messages,
|
||||
] {
|
||||
assert!(
|
||||
REGISTRY.iter().any(|e| e.entry == entry),
|
||||
@@ -246,14 +305,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_keywords_alphabetised_returns_ten_unique_sorted_commands() {
|
||||
fn entry_keywords_alphabetised_returns_unique_sorted_commands() {
|
||||
let keys = entry_keywords_alphabetised();
|
||||
let names: Vec<&str> = keys.iter().map(|k| k.as_str()).collect();
|
||||
// Ten DSL entries plus the eleven app-lifecycle entries
|
||||
// registered in REGISTRY (quit/q are two keywords with
|
||||
// the same usage template; both surface here).
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"add", "change", "create", "delete", "drop", "insert",
|
||||
"rename", "replay", "show", "update",
|
||||
"add", "change", "create", "delete", "drop", "export",
|
||||
"help", "import", "insert", "load", "messages", "mode",
|
||||
"new", "q", "quit", "rebuild", "rename", "replay",
|
||||
"save", "show", "update",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user