ADR-0024 Phase D: include column name in value-slot hint prose
User-facing improvement: typing into a value slot now surfaces
the column name in the hint. The hint at `insert into Customers
values (` (first column id:int) reads "for `id`: Type an
integer (e.g. 42, -7) or null" instead of the generic
"Type an integer …" prose. After `1, ` the panel updates to
the second column ("for `Name`: Type a quoted string …"). The
same applies to `update T set Email=` and `delete from T where
ts=` — the catalog wrapper threads the column name through.
Implementation:
**`Node::TypedValueSlot.column_name: Option<&'static str>`**
(new field, `src/dsl/grammar/mod.rs`). When `Some`, walker
writes `WalkContext::pending_value_column` on entry; clears
along with `pending_value_type` on inner success.
**Walker driver writes both names** (`src/dsl/walker/driver.rs`):
- `Node::TypedValueSlot` dispatch reads `column_name` and
populates `pending_value_column`.
- `Ident { writes_column: true }` dispatch also writes
`pending_value_column` (using the schema-canonical name when
available, falling back to the user's spelling) so update
set / where positions surface the column name.
**Shared sub-grammars** (`src/dsl/grammar/shared.rs`):
- New `slot_for_column(ty, name)` builds a `TypedValueSlot`
with the embedded leaked column name. Used by
`column_value_list`.
- New `slot_inner_for_type(ty)` returns just the Choice
(without TypedValueSlot wrapper) for slot_for_column to
rebuild.
- `column_value_list` factory now constructs per-column slots
via `slot_for_column(col.user_type, &col.name)`. Each slot
leaks its column name string with the same per-walk Box::leak
pattern the rest of dynamic dispatch uses.
**`WalkContext::pending_value_column: Option<String>`** (new
field, `src/dsl/walker/context.rs`). Pairs with
`pending_value_type` to give the hint resolver both pieces.
**Single-walk hint resolver** (`src/dsl/walker/mod.rs`):
- New `HintResolution { mode: HintMode, column: Option<String> }`
struct.
- New `hint_resolution_at_input(source, schema) -> Option<
HintResolution>` runs one walk and reports both pieces. The
ambient_hint dispatch composes per-column prose from the
result.
- Existing `hint_mode_at_input` / `hint_mode_at_input_with_schema`
preserved as thinner wrappers for tests / future callers
that don't need the column name.
**Catalog wrapper** (`src/friendly/strings/en-US.yaml`,
`src/friendly/keys.rs`):
- New `hint.value_slot_for_column: "for `{column}`: {detail}"`
prefixes the per-type prose with the actual column name when
the walker has it bound. Schemaless fallback continues to use
the generic value-literal prose with no column prefix.
**ambient_hint composes** (`src/input_render.rs`): consults
`hint_resolution_at_input`; when `column` is `Some`, wraps the
type prose through `hint.value_slot_for_column`; otherwise
emits the bare type prose.
Tests (846 total, 0 failing):
- 4 new input_render tests assert column names appear in the
prose at insert/update/where positions plus the
second-insert-value position (proves column tracking advances
with comma).
- All existing tests pass unchanged — the column-name addition
is layered on top of the type-only prose path.
Clippy clean.
This commit is contained in:
+89
-3
@@ -64,6 +64,80 @@ pub fn hint_mode_at_input_with_schema(
|
||||
hint_mode_at_input_inner(source, Some(schema))
|
||||
}
|
||||
|
||||
/// Resolution of the hint-panel mode at the cursor, plus the
|
||||
/// column name (if known) the cursor's value slot is keyed on.
|
||||
///
|
||||
/// Returned by [`hint_resolution_at_input`]. The renderer
|
||||
/// composes per-column prose ("for `Email`: Type a quoted
|
||||
/// string …") when `column` is `Some`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HintResolution {
|
||||
pub mode: crate::dsl::grammar::HintMode,
|
||||
pub column: Option<String>,
|
||||
}
|
||||
|
||||
/// Single-walk hint resolver (ADR-0024 §Phase D §typed-value-slots).
|
||||
///
|
||||
/// Walks `source` against `schema`, then reports both the
|
||||
/// resolved `HintMode` and the walker's `pending_value_column`
|
||||
/// (if any). Returns `None` when no HintMode applies.
|
||||
#[must_use]
|
||||
pub fn hint_resolution_at_input(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> Option<HintResolution> {
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(ty) = pending_type {
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)),
|
||||
column: pending_column,
|
||||
});
|
||||
}
|
||||
|
||||
let has_word = |w: &str| {
|
||||
expected
|
||||
.iter()
|
||||
.any(|e| matches!(e, Expectation::Word(x) if *x == w))
|
||||
};
|
||||
let value_literal_slot = has_word("null")
|
||||
&& has_word("true")
|
||||
&& has_word("false")
|
||||
&& expected.iter().any(|e| matches!(e, Expectation::NumberLit))
|
||||
&& expected.iter().any(|e| matches!(e, Expectation::StringLit));
|
||||
if value_literal_slot {
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
||||
column: None,
|
||||
});
|
||||
}
|
||||
|
||||
let new_name_slot = expected.iter().any(|e| {
|
||||
matches!(
|
||||
e,
|
||||
Expectation::Ident {
|
||||
source: IdentSource::NewName,
|
||||
..
|
||||
}
|
||||
)
|
||||
});
|
||||
if new_name_slot {
|
||||
return Some(HintResolution {
|
||||
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
||||
column: None,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn hint_mode_at_input_inner(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
@@ -209,6 +283,18 @@ 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)
|
||||
}
|
||||
|
||||
fn expected_for_hint_with_full_ctx(
|
||||
source: &str,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> (
|
||||
Vec<outcome::Expectation>,
|
||||
Option<crate::dsl::types::Type>,
|
||||
Option<String>,
|
||||
) {
|
||||
use crate::dsl::grammar::REGISTRY;
|
||||
|
||||
if source.trim().is_empty() {
|
||||
@@ -216,7 +302,7 @@ fn expected_for_hint_with_ctx(
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return (expected, None);
|
||||
return (expected, None, None);
|
||||
}
|
||||
let mut ctx = schema.map_or_else(context::WalkContext::new, |s| {
|
||||
context::WalkContext::with_schema(s)
|
||||
@@ -227,7 +313,7 @@ fn expected_for_hint_with_ctx(
|
||||
.iter()
|
||||
.map(|c| outcome::Expectation::Word(c.entry.primary))
|
||||
.collect();
|
||||
return (expected, None);
|
||||
return (expected, None, None);
|
||||
};
|
||||
let expected = match result.outcome {
|
||||
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
|
||||
@@ -236,7 +322,7 @@ fn expected_for_hint_with_ctx(
|
||||
outcome::WalkOutcome::Incomplete { expected, .. }
|
||||
| outcome::WalkOutcome::Mismatch { expected, .. } => expected,
|
||||
};
|
||||
(expected, ctx.pending_value_type)
|
||||
(expected, ctx.pending_value_type, ctx.pending_value_column)
|
||||
}
|
||||
|
||||
/// Public walk entry. `bound` is `EndOfInput` for parse;
|
||||
|
||||
Reference in New Issue
Block a user