Hint: pedagogical Form-A pointer at Form B's first value slot
Handoff-12 §2.2: Form B `insert into T values (…)` silently skips auto-generated columns from the value list, so a user who wants to set a serial/shortid column explicitly could only discover Form A by reading help. Now the hint at the first Form B value slot appends a note naming the skipped column(s) and pointing at the explicit-column form. hint_resolution_at_input derives the skipped columns from the post-walk WalkContext (Form B = no user_listed_columns + table has serial/shortid columns) and reports them on HintResolution; the note fires only at the first slot so it doesn't repeat at every comma. ambient_hint composes it onto the per-column prose.
This commit is contained in:
+114
-23
@@ -74,6 +74,15 @@ pub fn hint_mode_at_input_with_schema(
|
||||
pub struct HintResolution {
|
||||
pub mode: crate::dsl::grammar::HintMode,
|
||||
pub column: Option<String>,
|
||||
/// Auto-generated columns (serial / shortid) that Form B
|
||||
/// `insert into <T> values (…)` silently skips from the
|
||||
/// value list (ADR-0018 §3). Populated *only* at the first
|
||||
/// value slot of a Form B insert whose table has such
|
||||
/// columns — empty everywhere else. The renderer appends a
|
||||
/// pedagogical note pointing the user at Form A so the
|
||||
/// skipped column is discoverable without reading help
|
||||
/// (handoff-12 §2.2).
|
||||
pub form_b_autogen_skipped: Vec<String>,
|
||||
}
|
||||
|
||||
/// Single-walk hint resolver (ADR-0024 §Phase D §typed-value-slots).
|
||||
@@ -89,16 +98,22 @@ pub fn hint_resolution_at_input(
|
||||
use crate::dsl::grammar::{HintMode, IdentSource};
|
||||
use crate::dsl::walker::outcome::Expectation;
|
||||
|
||||
let (expected, pending_type, pending_column) =
|
||||
expected_for_hint_with_full_ctx(source, schema);
|
||||
if expected.is_empty() {
|
||||
let snap = expected_for_hint_snapshot(source, schema);
|
||||
if snap.expected.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let expected = snap.expected;
|
||||
|
||||
if let Some(ty) = pending_type {
|
||||
if let Some(ty) = snap.pending_value_type {
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)),
|
||||
column: pending_column,
|
||||
form_b_autogen_skipped: form_b_autogen_skipped(
|
||||
source,
|
||||
snap.user_listed_columns.as_ref(),
|
||||
snap.current_table_columns.as_ref(),
|
||||
snap.pending_value_column.as_deref(),
|
||||
),
|
||||
column: snap.pending_value_column,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,6 +131,7 @@ pub fn hint_resolution_at_input(
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
||||
column: None,
|
||||
form_b_autogen_skipped: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,12 +148,60 @@ pub fn hint_resolution_at_input(
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
||||
column: None,
|
||||
form_b_autogen_skipped: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Auto-generated columns a Form B insert skips from its value
|
||||
/// list — but only when the cursor sits at the *first* value
|
||||
/// slot, so the pedagogical note fires once per command rather
|
||||
/// than at every comma.
|
||||
///
|
||||
/// Returns empty unless: the command is an `insert`; no explicit
|
||||
/// column list was given (Form B — `user_listed` is `None`); the
|
||||
/// table has serial / shortid columns; and `pending_column` is
|
||||
/// the first non-auto-generated column (the first slot).
|
||||
fn form_b_autogen_skipped(
|
||||
source: &str,
|
||||
user_listed: Option<&Vec<String>>,
|
||||
table_columns: Option<&Vec<crate::completion::TableColumn>>,
|
||||
pending_column: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
use crate::dsl::types::Type;
|
||||
|
||||
// Form A (explicit column list) and non-insert commands
|
||||
// (`update T set …` value slots also leave user_listed
|
||||
// None) are excluded — the note is insert-Form-B only.
|
||||
if user_listed.is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
if !source.trim_start().to_ascii_lowercase().starts_with("insert") {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(cols) = table_columns else {
|
||||
return Vec::new();
|
||||
};
|
||||
let is_auto = |t: Type| matches!(t, Type::Serial | Type::ShortId);
|
||||
let skipped: Vec<String> = cols
|
||||
.iter()
|
||||
.filter(|c| is_auto(c.user_type))
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
if skipped.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Fire only at the first value slot — i.e. when the slot's
|
||||
// column is the first non-auto-generated column.
|
||||
let first_non_auto = cols.iter().find(|c| !is_auto(c.user_type));
|
||||
match (first_non_auto, pending_column) {
|
||||
(Some(first), Some(pending)) if first.name == pending => skipped,
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hint_mode_at_input_inner(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
@@ -350,41 +414,62 @@ pub fn expected_at_input(source: &str) -> Vec<outcome::Expectation> {
|
||||
/// surfaced. Used by the hint resolver to distinguish "must
|
||||
/// type more" from "could continue", and to dispatch per-type
|
||||
/// prose when the cursor is inside a typed value slot.
|
||||
/// Post-walk snapshot the hint resolver needs: the strict
|
||||
/// expected set plus the `WalkContext` fields that survive the
|
||||
/// walk and feed per-column / pedagogical prose.
|
||||
struct HintWalkSnapshot {
|
||||
expected: Vec<outcome::Expectation>,
|
||||
pending_value_type: Option<crate::dsl::types::Type>,
|
||||
pending_value_column: Option<String>,
|
||||
current_table_columns: Option<Vec<crate::completion::TableColumn>>,
|
||||
/// `Some` when the input used Form A's explicit column list.
|
||||
/// `None` for Form B (`insert into T values …`) and for
|
||||
/// every non-insert command.
|
||||
user_listed_columns: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn expected_for_hint_with_ctx(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> (Vec<outcome::Expectation>, Option<crate::dsl::types::Type>) {
|
||||
let (expected, ty, _col) = expected_for_hint_with_full_ctx(source, schema);
|
||||
(expected, ty)
|
||||
let snap = expected_for_hint_snapshot(source, schema);
|
||||
(snap.expected, snap.pending_value_type)
|
||||
}
|
||||
|
||||
fn expected_for_hint_with_full_ctx(
|
||||
fn expected_for_hint_snapshot(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> (
|
||||
Vec<outcome::Expectation>,
|
||||
Option<crate::dsl::types::Type>,
|
||||
Option<String>,
|
||||
) {
|
||||
) -> HintWalkSnapshot {
|
||||
use crate::dsl::grammar::REGISTRY;
|
||||
|
||||
if source.trim().is_empty() {
|
||||
let expected = REGISTRY
|
||||
let entry_words = || -> Vec<outcome::Expectation> {
|
||||
REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return (expected, None, None);
|
||||
.collect()
|
||||
};
|
||||
|
||||
if source.trim().is_empty() {
|
||||
return HintWalkSnapshot {
|
||||
expected: entry_words(),
|
||||
pending_value_type: None,
|
||||
pending_value_column: None,
|
||||
current_table_columns: None,
|
||||
user_listed_columns: None,
|
||||
};
|
||||
}
|
||||
let mut ctx = schema.map_or_else(context::WalkContext::new, |s| {
|
||||
context::WalkContext::with_schema(s)
|
||||
});
|
||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
let Some(result) = result else {
|
||||
let expected = REGISTRY
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return (expected, None, None);
|
||||
return HintWalkSnapshot {
|
||||
expected: entry_words(),
|
||||
pending_value_type: None,
|
||||
pending_value_column: None,
|
||||
current_table_columns: None,
|
||||
user_listed_columns: None,
|
||||
};
|
||||
};
|
||||
let expected = match result.outcome {
|
||||
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
|
||||
@@ -393,7 +478,13 @@ fn expected_for_hint_with_full_ctx(
|
||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
||||
};
|
||||
(expected, ctx.pending_value_type, ctx.pending_value_column)
|
||||
HintWalkSnapshot {
|
||||
expected,
|
||||
pending_value_type: ctx.pending_value_type,
|
||||
pending_value_column: ctx.pending_value_column,
|
||||
current_table_columns: ctx.current_table_columns,
|
||||
user_listed_columns: ctx.user_listed_columns,
|
||||
}
|
||||
}
|
||||
|
||||
/// Public walk entry. `bound` is `EndOfInput` for parse;
|
||||
|
||||
Reference in New Issue
Block a user