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:
@@ -40,15 +40,18 @@ fn form_b_first_value_skips_serial_column() {
|
||||
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||
panic!("expected Prose at first Form B value slot, got {:?}", a.hint)
|
||||
});
|
||||
// First column slot must name `Name`, not the skipped
|
||||
// `id`.
|
||||
// The value slot itself must be keyed on `Name` — the first
|
||||
// non-auto column — not on the skipped `id`.
|
||||
assert!(
|
||||
prose.contains("Name"),
|
||||
"Form B should advance to first non-auto column `Name`, got prose: {prose:?}",
|
||||
prose.starts_with("for `Name`"),
|
||||
"Form B's first value slot should be for `Name`, got prose: {prose:?}",
|
||||
);
|
||||
// `id` appears only inside the trailing pedagogical note
|
||||
// (`id` auto-generated — skipped here …), never as the slot
|
||||
// the user is being prompted to fill.
|
||||
assert!(
|
||||
!prose.contains("`id`"),
|
||||
"Form B should NOT prompt for the auto-gen `id`, got prose: {prose:?}",
|
||||
!prose.starts_with("for `id`"),
|
||||
"Form B must not prompt for the auto-gen `id` as a value slot, got prose: {prose:?}",
|
||||
);
|
||||
crate::snap!("form_b_first_value_serial_pk", a);
|
||||
}
|
||||
@@ -218,6 +221,87 @@ fn form_b_text_pk_with_correct_values_parses() {
|
||||
// position.
|
||||
// =========================================================
|
||||
|
||||
// =========================================================
|
||||
// Pedagogical Form-A pointer (handoff-12 §2.2).
|
||||
//
|
||||
// At the FIRST value slot of a Form B insert whose table has
|
||||
// auto-generated columns, the hint must mention the skipped
|
||||
// column(s) and point at the explicit-column form.
|
||||
// =========================================================
|
||||
|
||||
#[test]
|
||||
fn form_b_first_slot_mentions_skipped_serial_column() {
|
||||
let schema = schema_serial_pk();
|
||||
let a = assess_at_end("insert into Customers values (", &schema);
|
||||
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||
panic!("expected Prose at first Form B slot, got {:?}", a.hint)
|
||||
});
|
||||
// Names the skipped auto-gen column.
|
||||
assert!(
|
||||
prose.contains("`id`"),
|
||||
"first-slot hint should mention skipped `id`, got: {prose:?}",
|
||||
);
|
||||
// Points at the explicit-column escape hatch.
|
||||
assert!(
|
||||
prose.contains("auto-generated") && prose.contains("list columns"),
|
||||
"first-slot hint should explain the Form-A escape, got: {prose:?}",
|
||||
);
|
||||
crate::snap!("form_b_first_slot_skip_note", a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_b_second_slot_omits_skip_note() {
|
||||
// The note fires once, at the first slot only — not at
|
||||
// every comma.
|
||||
let schema = schema_serial_pk();
|
||||
let a = assess_at_end(
|
||||
"insert into Customers values ('Alice', ",
|
||||
&schema,
|
||||
);
|
||||
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||
panic!("expected Prose at second slot, got {:?}", a.hint)
|
||||
});
|
||||
assert!(
|
||||
!prose.contains("auto-generated"),
|
||||
"second-slot hint must NOT repeat the skip note, got: {prose:?}",
|
||||
);
|
||||
crate::snap!("form_b_second_slot_no_skip_note", a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_b_text_pk_has_no_skip_note() {
|
||||
// No auto-gen columns → no skip note.
|
||||
let schema = schema_text_pk();
|
||||
let a = assess_at_end("insert into Items values (", &schema);
|
||||
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||
panic!("expected Prose, got {:?}", a.hint)
|
||||
});
|
||||
assert!(
|
||||
!prose.contains("auto-generated"),
|
||||
"text-PK table has no auto-gen column — no skip note expected, got: {prose:?}",
|
||||
);
|
||||
crate::snap!("form_b_text_pk_no_skip_note", a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_a_first_slot_has_no_skip_note() {
|
||||
// Form A lists columns explicitly — the user is in control,
|
||||
// no pedagogical pointer needed.
|
||||
let schema = schema_serial_pk();
|
||||
let a = assess_at_end(
|
||||
"insert into Customers (Name) values (",
|
||||
&schema,
|
||||
);
|
||||
let prose = hint_prose(&a).unwrap_or_else(|| {
|
||||
panic!("expected Prose, got {:?}", a.hint)
|
||||
});
|
||||
assert!(
|
||||
!prose.contains("auto-generated"),
|
||||
"Form A must not show the Form-B skip note, got: {prose:?}",
|
||||
);
|
||||
crate::snap!("form_a_no_skip_note", a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_b_advances_through_every_type_first_to_real() {
|
||||
// Things' second column is `r:real`. After typing the
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: tests/typing_surface/insert_form_b.rs
|
||||
description: "input=\"insert into Customers (Name) values (\" cursor=37"
|
||||
expression: "& a"
|
||||
---
|
||||
Assessment {
|
||||
input: "insert into Customers (Name) values (",
|
||||
cursor: 37,
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
Completion {
|
||||
replaced_range: (
|
||||
37,
|
||||
37,
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
parse_result: Err(
|
||||
"Invalid(at_eof)",
|
||||
),
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: tests/typing_surface/insert_form_b.rs
|
||||
description: "input=\"insert into Customers values (\" cursor=30"
|
||||
expression: "& a"
|
||||
---
|
||||
Assessment {
|
||||
input: "insert into Customers values (",
|
||||
cursor: 30,
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
Completion {
|
||||
replaced_range: (
|
||||
30,
|
||||
30,
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
parse_result: Err(
|
||||
"Invalid(at_eof)",
|
||||
),
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ Assessment {
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `k`: Type an integer (e.g. 42, -7) or null",
|
||||
"for `k`: Type an integer (e.g. 42, -7) or null (`sid`, `auto` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ Assessment {
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null",
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: tests/typing_surface/insert_form_b.rs
|
||||
description: "input=\"insert into Customers values ('Alice', \" cursor=39"
|
||||
expression: "& a"
|
||||
---
|
||||
Assessment {
|
||||
input: "insert into Customers values ('Alice', ",
|
||||
cursor: 39,
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Email`: Type a quoted string (e.g. 'Alice') or null",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
Completion {
|
||||
replaced_range: (
|
||||
39,
|
||||
39,
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
parse_result: Err(
|
||||
"Invalid(at_eof)",
|
||||
),
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: tests/typing_surface/insert_form_b.rs
|
||||
description: "input=\"insert into Items values (\" cursor=26"
|
||||
expression: "& a"
|
||||
---
|
||||
Assessment {
|
||||
input: "insert into Items values (",
|
||||
cursor: 26,
|
||||
state: IncompleteAtEof,
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Code`: Type a quoted string (e.g. 'Alice') or null",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
Completion {
|
||||
replaced_range: (
|
||||
26,
|
||||
26,
|
||||
),
|
||||
partial_prefix: "",
|
||||
candidates: [
|
||||
Candidate {
|
||||
text: "null",
|
||||
kind: Keyword,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
parse_result: Err(
|
||||
"Invalid(at_eof)",
|
||||
),
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ Assessment {
|
||||
),
|
||||
hint: Some(
|
||||
Prose(
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null",
|
||||
"for `Name`: Type a quoted string (e.g. 'Alice') or null (`id` auto-generated — skipped here; list columns explicitly, e.g. `insert into T (...) values (...)`, to set it.)",
|
||||
),
|
||||
),
|
||||
completion: Some(
|
||||
|
||||
Reference in New Issue
Block a user