hint: show the matching usage template for multi-form commands
A parse error in `add index …` showed the `add column` usage: `add` and `drop` are multi-form commands, and both the ambient hint and the submit-time usage block picked the first-listed form unconditionally. New `grammar::usage_key_for_input` disambiguates by the form word after the entry keyword — `column` / `index` / `table` / `relationship`, or the leading digit of `add 1:n …`. The ambient hint now shows that one form; `render_usage_block` shows the committed form's usage and falls back to the whole family only for a bare `add` / `drop` with no form chosen.
This commit is contained in:
+12
-1
@@ -1812,7 +1812,18 @@ fn parse_error_message(err: &ParseError) -> String {
|
|||||||
/// `drop` show every variant. Otherwise the fallback lists every
|
/// `drop` show every variant. Otherwise the fallback lists every
|
||||||
/// entry keyword alphabetically.
|
/// entry keyword alphabetically.
|
||||||
fn render_usage_block(input: &str) -> String {
|
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:");
|
let mut out = String::from("usage:");
|
||||||
for key in catalog_keys {
|
for key in catalog_keys {
|
||||||
let template = crate::friendly::translate(key, &[]);
|
let template = crate::friendly::translate(key, &[]);
|
||||||
|
|||||||
@@ -414,6 +414,42 @@ pub fn usage_keys_for_input(source: &str) -> Option<(&'static str, &'static [&'s
|
|||||||
Some((node.entry.primary, node.usage_ids))
|
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
|
/// Every command-entry word in the registry, sorted alphabetically
|
||||||
/// by primary literal. Replaces `dsl::usage::entry_keywords_alphabetised`
|
/// by primary literal. Replaces `dsl::usage::entry_keywords_alphabetised`
|
||||||
/// which read the same data through the legacy `usage::REGISTRY`.
|
/// which read the same data through the legacy `usage::REGISTRY`.
|
||||||
|
|||||||
+23
-2
@@ -341,8 +341,10 @@ pub fn ambient_hint(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let _ = position;
|
let _ = position;
|
||||||
let usage = crate::dsl::grammar::usage_keys_for_input(input)
|
// The form the user has committed to drives the
|
||||||
.and_then(|(_, keys)| keys.first().copied())
|
// 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, &[]));
|
.map(|key| crate::friendly::translate(key, &[]));
|
||||||
Some(AmbientHint::Prose(match usage {
|
Some(AmbientHint::Prose(match usage {
|
||||||
Some(u) => crate::t!(
|
Some(u) => crate::t!(
|
||||||
@@ -876,6 +878,25 @@ mod tests {
|
|||||||
assert_eq!(cs, vec!["data".to_string(), "table".to_string()]);
|
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]
|
#[test]
|
||||||
fn ambient_hint_for_definite_error_includes_usage_template() {
|
fn ambient_hint_for_definite_error_includes_usage_template() {
|
||||||
let h = prose("insert into T extra", 19).expect("prose hint");
|
let h = prose("insert into T extra", 19).expect("prose hint");
|
||||||
|
|||||||
Reference in New Issue
Block a user