diff --git a/src/app.rs b/src/app.rs index 0deae18..44a5060 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1812,7 +1812,18 @@ fn parse_error_message(err: &ParseError) -> String { /// `drop` show every variant. Otherwise the fallback lists every /// entry keyword alphabetically. fn render_usage_block(input: &str) -> String { - if let Some((_word, catalog_keys)) = crate::dsl::grammar::usage_keys_for_input(input) { + // A multi-form command that has committed to a form + // (`add index …`) shows only that form's usage; a bare + // multi-form entry word (`add`) shows the whole family. + let catalog_keys: Vec<&'static str> = + crate::dsl::grammar::usage_key_for_input(input) + .map(|key| vec![key]) + .or_else(|| { + crate::dsl::grammar::usage_keys_for_input(input) + .map(|(_word, all)| all.to_vec()) + }) + .unwrap_or_default(); + if !catalog_keys.is_empty() { let mut out = String::from("usage:"); for key in catalog_keys { let template = crate::friendly::translate(key, &[]); diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 0416fcb..723548f 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -414,6 +414,42 @@ pub fn usage_keys_for_input(source: &str) -> Option<(&'static str, &'static [&'s Some((node.entry.primary, node.usage_ids)) } +/// The single usage template most relevant to `source`, when +/// one is determinable. +/// +/// A single-form command resolves to its one usage key. A +/// multi-form command (`add`, `drop`) disambiguates by the +/// form word after the entry keyword — so a parse error in +/// `add index …` resolves to the `add index` usage rather than +/// the first-listed `add column`. Returns `None` for a bare +/// multi-form entry word (`add` with nothing after it), where +/// no form has been chosen — the caller decides whether to +/// show the whole family or nothing. +#[must_use] +pub fn usage_key_for_input(source: &str) -> Option<&'static str> { + use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace}; + let (_entry, keys) = usage_keys_for_input(source)?; + let first = *keys.first()?; + if keys.len() == 1 { + return Some(first); + } + // Multi-form: the form is named by the token right after + // the entry keyword. + let start = skip_whitespace(source, 0); + let (_, entry_end) = consume_ident(source, start)?; + let after = skip_whitespace(source, entry_end); + // The `add 1:n relationship` form opens with a digit. + if source.as_bytes().get(after).is_some_and(u8::is_ascii_digit) { + return keys.iter().copied().find(|k| k.ends_with("relationship")); + } + // Otherwise the form word is an identifier — `column`, + // `index`, `table`, `relationship` — matched against the + // usage key's suffix. + let (s, e) = consume_ident(source, after)?; + let form = source[s..e].to_ascii_lowercase(); + keys.iter().copied().find(|k| k.ends_with(form.as_str())) +} + /// Every command-entry word in the registry, sorted alphabetically /// by primary literal. Replaces `dsl::usage::entry_keywords_alphabetised` /// which read the same data through the legacy `usage::REGISTRY`. diff --git a/src/input_render.rs b/src/input_render.rs index 3c839a1..fcc653b 100644 --- a/src/input_render.rs +++ b/src/input_render.rs @@ -341,8 +341,10 @@ pub fn ambient_hint( } } else { let _ = position; - let usage = crate::dsl::grammar::usage_keys_for_input(input) - .and_then(|(_, keys)| keys.first().copied()) + // The form the user has committed to drives the + // usage template — `add index …` shows the + // `add index` usage, not the first `add` form. + let usage = crate::dsl::grammar::usage_key_for_input(input) .map(|key| crate::friendly::translate(key, &[])); Some(AmbientHint::Prose(match usage { Some(u) => crate::t!( @@ -876,6 +878,25 @@ mod tests { assert_eq!(cs, vec!["data".to_string(), "table".to_string()]); } + #[test] + fn ambient_hint_usage_matches_the_add_form_typed() { + // A trailing-junk error after `add index …` must show + // the `add index` usage — `add` is a multi-form + // command and the hint used to always show the first + // form (`add column`). + let input = "add index on Customers (barg):"; + let h = prose(input, input.len()).expect("prose hint"); + assert!(h.contains("usage:"), "got {h:?}"); + assert!( + h.contains("add index"), + "should show the `add index` usage, got {h:?}", + ); + assert!( + !h.contains("add column"), + "should not show the `add column` usage, got {h:?}", + ); + } + #[test] fn ambient_hint_for_definite_error_includes_usage_template() { let h = prose("insert into T extra", 19).expect("prose hint");