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:
+98
-4
@@ -191,9 +191,11 @@ pub fn ambient_hint(
|
||||
let leading = hint_leading_slice(input, cursor);
|
||||
// ADR-0024 §Phase D §typed-value-slots: pass the schema so
|
||||
// the resolver can narrow value-slot prose per column type
|
||||
// (Date → "Type a date as 'YYYY-MM-DD'", etc.).
|
||||
let hint_mode = crate::dsl::walker::hint_mode_at_input_with_schema(leading, cache);
|
||||
match hint_mode {
|
||||
// (Date → "Type a date as 'YYYY-MM-DD'", etc.) and surface
|
||||
// the column name when the walker has it bound.
|
||||
let resolution =
|
||||
crate::dsl::walker::hint_resolution_at_input(leading, Some(cache));
|
||||
match resolution.as_ref().map(|r| r.mode) {
|
||||
Some(crate::dsl::grammar::HintMode::ProseOnly(key)) => {
|
||||
// The cursor sits at a slot where Tab candidates
|
||||
// would be actively misleading. Surface the catalog
|
||||
@@ -201,7 +203,16 @@ pub fn ambient_hint(
|
||||
// once the user starts typing a partial, normal
|
||||
// candidate completion (e.g. `n` → `null`) applies.
|
||||
if cursor_partial_is_empty(input, cursor) {
|
||||
return Some(AmbientHint::Prose(crate::friendly::translate(key, &[])));
|
||||
let detail = crate::friendly::translate(key, &[]);
|
||||
let composed = match resolution.and_then(|r| r.column) {
|
||||
Some(column) => crate::t!(
|
||||
"hint.value_slot_for_column",
|
||||
column = column,
|
||||
detail = detail
|
||||
),
|
||||
None => detail,
|
||||
};
|
||||
return Some(AmbientHint::Prose(composed));
|
||||
}
|
||||
}
|
||||
Some(crate::dsl::grammar::HintMode::ForceProse(_key)) => {
|
||||
@@ -662,6 +673,89 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_at_insert_first_value_mentions_column_name() {
|
||||
use crate::dsl::types::Type;
|
||||
let cache = schema_with_columns(
|
||||
"Customers",
|
||||
&[("id", Type::Int), ("Name", Type::Text)],
|
||||
);
|
||||
let input = "insert into Customers values (";
|
||||
match ambient_hint(input, input.len(), None, &cache) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(p.contains("id"), "expected column name `id`, got {p:?}");
|
||||
assert!(
|
||||
p.contains("integer"),
|
||||
"expected int prose, got {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_at_update_set_mentions_column_name() {
|
||||
use crate::dsl::types::Type;
|
||||
let cache = schema_with_columns(
|
||||
"Customers",
|
||||
&[("id", Type::Int), ("Email", Type::Text)],
|
||||
);
|
||||
let input = "update Customers set Email=";
|
||||
match ambient_hint(input, input.len(), None, &cache) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(
|
||||
p.contains("Email"),
|
||||
"expected column name `Email`, got {p:?}",
|
||||
);
|
||||
assert!(
|
||||
p.contains("quoted string"),
|
||||
"expected text prose, got {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_at_where_mentions_column_name() {
|
||||
use crate::dsl::types::Type;
|
||||
let cache = schema_with_columns("Events", &[("ts", Type::DateTime)]);
|
||||
let input = "delete from Events where ts=";
|
||||
match ambient_hint(input, input.len(), None, &cache) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(p.contains("ts"), "expected column name `ts`, got {p:?}");
|
||||
assert!(
|
||||
p.contains("YYYY-MM-DD"),
|
||||
"expected datetime prose, got {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_at_second_insert_value_mentions_second_column() {
|
||||
use crate::dsl::types::Type;
|
||||
let cache = schema_with_columns(
|
||||
"Customers",
|
||||
&[("id", Type::Int), ("Name", Type::Text)],
|
||||
);
|
||||
let input = "insert into Customers values (1, ";
|
||||
match ambient_hint(input, input.len(), None, &cache) {
|
||||
Some(AmbientHint::Prose(p)) => {
|
||||
assert!(
|
||||
p.contains("Name"),
|
||||
"expected second column `Name`, got {p:?}",
|
||||
);
|
||||
assert!(
|
||||
p.contains("quoted string"),
|
||||
"expected text prose, got {p:?}",
|
||||
);
|
||||
}
|
||||
other => panic!("expected Prose, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_hint_at_value_slot_falls_back_to_generic_without_schema() {
|
||||
// Empty cache: the walker can't resolve the column type
|
||||
|
||||
Reference in New Issue
Block a user