diff --git a/docs/adr/0022-ambient-typing-assistance.md b/docs/adr/0022-ambient-typing-assistance.md
index 254b45e..1150d34 100644
--- a/docs/adr/0022-ambient-typing-assistance.md
+++ b/docs/adr/0022-ambient-typing-assistance.md
@@ -772,6 +772,58 @@ invalid_ident_does_not_fire_for_column_prefix_at_sql_expr_slot}`;
`theme::function_colour_is_distinct_from_keyword_identifier_and_type`.
See ADR-0031's status note for the grammar-side anchor.
+## Amendment 7 — optional positional args reach the hint panel (2026-06-12)
+
+Issue #26. At `seed
▮` the hint panel showed only the
+`set` / `--seed` continuation chips and never mentioned the
+**optional row count** — even though a count (`seed users 50`) is
+the most common next move. The count is a bare positional
+`NumberLit` with no keyword/candidate text, so the candidate ladder
+can't surface it; and `seed ` is already a *complete*
+command, so the hint resolver short-circuits (empty expected set).
+
+The existing `IntroProse` `HintMode` (ADR-0024 §HintMode-per-node;
+issue #4's CREATE-TABLE element hint) is the right tool — it shows
+prose that *introduces* a position whose first-class move has no
+candidate, with the keyword alternatives folded into the prose and
+Tab still cycling them. But it did not reach this position: a
+`Node::Hinted`'s mode lives in `pending_hint_mode`, which the very
+next match clears — including the **empty** match of a skipped
+`Optional`. The CREATE-TABLE element survives only because it sits
+in a *required* `Repeated(min:1)`; an optional positional followed
+by more optionals (the seed count) is cleared before the resolver
+reads it.
+
+### Mechanism
+
+A small, general carry: when `walk_optional` skips its inner (the
+inner didn't engage), it stashes any `IntroProse` key the inner
+left in `pending_hint_mode` into a new `WalkContext` field,
+`surviving_intro_hint: Option<(key, position)>`, **before** the
+empty match clears `pending_hint_mode`. The trailing optionals,
+which are not `IntroProse`, don't overwrite it. The hint snapshot
+keeps the key **only when `position == cursor`** (the slice end),
+so it shows while the cursor sits at the count slot but not once a
+later clause (`set …`) consumes input past it, nor once the count
+itself is supplied. The resolver returns that `IntroProse` even for
+an otherwise-complete command (ahead of the empty-expected
+short-circuit).
+
+The seed grammar wraps the count in
+`Hinted { IntroProse("hint.seed_count"), NumberLit }`; the prose
+names the count (with its default 20) plus the `.column`
+column-fill form and the `set` / `--seed` keywords (user-chosen
+scope: mention every option). Only `IntroProse` is carried —
+`ProseOnly` / `ForceProse` mark *active* slots and reach the
+resolver through the normal path, unchanged. The CREATE-TABLE
+element (in a `Repeated`, not an `Optional`) is untouched.
+
+This is a refinement of ADR-0024 §HintMode-per-node and a sibling
+of issue #4; no `AmbientHint` / renderer change. Covered by
+`input_render::{seed_count_is_advertised_at_the_optional_position,
+seed_count_hint_does_not_leak_once_the_count_or_a_clause_is_given,
+seed_count_hint_also_fires_after_a_column_fill_target}`.
+
## Out of scope
Deliberately deferred to keep this ADR shippable as a single
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 13a0fb0..9389da8 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -27,7 +27,7 @@ This directory contains the project's ADRs, recorded per
- [ADR-0019 — Friendly error layer (H1) and i18n message catalog](0019-friendly-error-layer-and-i18n.md)
- [ADR-0020 — Tokenization layer for the DSL parser](0020-tokenization-layer-for-the-dsl-parser.md) — **Superseded by ADR-0024 (never implemented).** Specified a `chumsky`-over-tokens architecture (separate lexer, `define_keywords!`, `&[Token]` grammar). ADR-0024 adopted a scannerless hand-rolled walker and removed `chumsky` entirely; the lexer/keyword/token model here does not exist. Kept as institutional memory of the path not taken.
- [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md) — **Mechanism superseded by ADR-0024; H1a scope continued in ADR-0042.** The *intent* (show the command's grammar at the point of error) shipped — `usage_ids` on each `CommandNode`, the `parse.usage.*` templates, and the `available_commands` fallback all exist — but via grammar nodes, not the `chumsky` `UsageEntry` registry / `parse.token.*` keys described here (which were never built).
-- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms; **Amendment 5 lets the hint panel grow for long prose hints** — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); `resolve_hint_lines` now pre-wraps prose and `render_right_column` sizes the panel to the line count (1 row default, up to `MAX_HINT_ROWS`=3, reclaimed when short) with a `clamp_wrapped` ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char `parse.usage.sql_create_table` synopsis to a terse one-liner (full grammar remains in `help.ddl.sql_create_table`); **Amendment 6 adds a curated SQL function-name list** (`src/dsl/sql_functions.rs`, `KNOWN_SQL_FUNCTIONS` — aggregates + common + broader scalars; `cast` deliberately excluded as its `CAST(x AS type)` syntax isn't a plain-call shape) as the single source of truth shared by two consumers at the `sql_expr_ident` slot (ADR-0031 §1): **issue #15** offers the functions as Tab candidates under a new `CandidateKind::Function` + ninth `Theme` colour `tok_function` (a blue distinct from keyword/identifier/type, parallel to Amendment 4's `tok_type`) so a learner discovers `sum`/`upper`/…; **issue #16** restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot — `invalid_ident_at_cursor` now bails only when the partial prefix-matches a known function, else falls through to the schema-column check, so `select Agx` warns again at typing time while `select sum` does not (the issue-#6 lockdown tests + the submit-time `unknown_column` diagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor
+- [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms; **Amendment 5 lets the hint panel grow for long prose hints** — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); `resolve_hint_lines` now pre-wraps prose and `render_right_column` sizes the panel to the line count (1 row default, up to `MAX_HINT_ROWS`=3, reclaimed when short) with a `clamp_wrapped` ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char `parse.usage.sql_create_table` synopsis to a terse one-liner (full grammar remains in `help.ddl.sql_create_table`); **Amendment 6 adds a curated SQL function-name list** (`src/dsl/sql_functions.rs`, `KNOWN_SQL_FUNCTIONS` — aggregates + common + broader scalars; `cast` deliberately excluded as its `CAST(x AS type)` syntax isn't a plain-call shape) as the single source of truth shared by two consumers at the `sql_expr_ident` slot (ADR-0031 §1): **issue #15** offers the functions as Tab candidates under a new `CandidateKind::Function` + ninth `Theme` colour `tok_function` (a blue distinct from keyword/identifier/type, parallel to Amendment 4's `tok_type`) so a learner discovers `sum`/`upper`/…; **issue #16** restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot — `invalid_ident_at_cursor` now bails only when the partial prefix-matches a known function, else falls through to the schema-column check, so `select Agx` warns again at typing time while `select sum` does not (the issue-#6 lockdown tests + the submit-time `unknown_column` diagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor; **Amendment 7 surfaces optional positional args in the hint panel** (issue #26): at `seed ▮` the optional row count (a bare `NumberLit` with no candidate) was invisible next to the `set`/`--seed` chips, and the resolver short-circuits on the already-complete command. Extends the issue-#4 `IntroProse` `HintMode` (ADR-0024 §HintMode-per-node) to survive trailing optionals: `walk_optional` stashes a skipped inner's `IntroProse` key into a new `WalkContext.surviving_intro_hint` (key + position) before the empty match clears `pending_hint_mode`, and the snapshot keeps it only when the skip position is the cursor (so it never leaks past a later-consumed `set …` clause or once the count is given); the resolver returns it ahead of the empty-expected short-circuit. The seed count is wrapped `Hinted{IntroProse("hint.seed_count")}`; prose names the count (default 20), the `.column` column-fill form, and `set`/`--seed` (user-chosen scope). Only `IntroProse` is carried; `ProseOnly`/`ForceProse` and the CREATE-TABLE element (a required `Repeated`) are untouched; no `AmbientHint`/renderer change
- [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024)
- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note)
- [ADR-0025 — Indexes](0025-indexes.md) — **Accepted** (**Amendment 1, 2026-05-25**: UNIQUE indexes admitted on the **advanced-mode** surface via `CREATE UNIQUE INDEX` — ADR-0035 §4d; the `IndexSchema.unique` flag round-trips through `project.yaml` with no new metadata table since the engine reports uniqueness natively; simple-mode `add unique index` stays deferred), `add index` / `drop index`, persistence, rebuild-table preservation, and items-list display (`C3` index portion + `S2`)
diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs
index b111075..3f14b93 100644
--- a/src/dsl/grammar/data.rs
+++ b/src/dsl/grammar/data.rs
@@ -438,6 +438,17 @@ const LIMIT_CLAUSE: Node = Node::Seq(LIMIT_CLAUSE_NODES);
const SEED_COUNT: Node = Node::NumberLit {
validator: Some(LIMIT_VALIDATOR),
};
+/// Issue #26: the row count is a bare positional number, so it produces
+/// no Tab candidate and was invisible in the hint panel at
+/// `seed ▮` (only `set` / `--seed` showed). Wrapping it in
+/// `IntroProse` advertises it (and the other options) in prose; the
+/// skipped-optional carry (`surviving_intro_hint`) makes the hint reach
+/// the resolver despite the trailing optionals. Tab still cycles the
+/// keyword candidates.
+const SEED_COUNT_HINTED: Node = Node::Hinted {
+ mode: crate::dsl::grammar::HintMode::IntroProse("hint.seed_count"),
+ inner: &SEED_COUNT,
+};
/// `--seed ` — a reproducible-generation flag carrying a numeric
/// seed (ADR-0048 D4). The only flag in the DSL that takes a value;
/// `build_seed` reads the number immediately after the flag.
@@ -567,7 +578,7 @@ const SEED_NODES: &[Node] = &[
// against this table.
TABLE_NAME_WRITES,
SEED_DOT_COLUMN,
- Node::Optional(&SEED_COUNT),
+ Node::Optional(&SEED_COUNT_HINTED),
Node::Optional(&SEED_SET_CLAUSE),
Node::Optional(&SEED_FLAG),
];
diff --git a/src/dsl/walker/context.rs b/src/dsl/walker/context.rs
index 807f41e..6306119 100644
--- a/src/dsl/walker/context.rs
+++ b/src/dsl/walker/context.rs
@@ -134,6 +134,17 @@ pub struct WalkContext<'a> {
/// resolver reads this directly instead of inferring the
/// slot kind from the shape of the expected set.
pub pending_hint_mode: Option,
+ /// An `IntroProse` hint captured from an *optional* slot that
+ /// the walk skipped (issue #26). Unlike `pending_hint_mode`
+ /// (cleared on the very next match — including the empty match
+ /// of a skipped `Optional`), this survives the trailing
+ /// optional siblings so the hint reaches the resolver for a
+ /// position like `seed ▮`, where the optional row
+ /// count is otherwise invisible. Carries the catalog key and
+ /// the byte position the optional was skipped at; the resolver
+ /// uses it only when that position is the cursor (so it doesn't
+ /// leak past a later-consumed clause).
+ pub surviving_intro_hint: Option<(&'static str, usize)>,
/// The columns the user explicitly listed in
/// `insert into (col1, col2, …) values (…)` (Form A),
/// in declaration order.
@@ -232,6 +243,7 @@ impl<'a> WalkContext<'a> {
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
+ surviving_intro_hint: None,
user_listed_columns: None,
subgrammar_depth: 0,
from_scope_stack: vec![ScopeFrame::default()],
@@ -254,6 +266,7 @@ impl<'a> WalkContext<'a> {
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
+ surviving_intro_hint: None,
user_listed_columns: None,
subgrammar_depth: 0,
from_scope_stack: vec![ScopeFrame::default()],
diff --git a/src/dsl/walker/driver.rs b/src/dsl/walker/driver.rs
index e14f04f..231bc36 100644
--- a/src/dsl/walker/driver.rs
+++ b/src/dsl/walker/driver.rs
@@ -990,6 +990,21 @@ fn walk_seq(
}
}
+/// Issue #26: when an `Optional` is skipped (its inner didn't engage),
+/// stash any `IntroProse` hint the inner left in `pending_hint_mode`
+/// into the surviving slot before it is cleared by this empty match.
+/// `position` is where the optional was skipped — the resolver compares
+/// it to the cursor so the hint only shows while the cursor sits at that
+/// optional, not after a later clause consumes input past it. Only
+/// `IntroProse` is carried (it is the "introduce an optional position"
+/// mode); `ProseOnly` / `ForceProse` mark active slots and reach the
+/// resolver through the normal `pending_hint_mode` path.
+const fn capture_skipped_intro_hint(ctx: &mut WalkContext, position: usize) {
+ if let Some(crate::dsl::grammar::HintMode::IntroProse(key)) = ctx.pending_hint_mode {
+ ctx.surviving_intro_hint = Some((key, position));
+ }
+}
+
fn walk_optional(
source: &str,
position: usize,
@@ -1008,6 +1023,7 @@ fn walk_optional(
// Inner didn't engage at all — skip the Optional
// but carry the inner's expectations so the caller's
// expected-set sees them.
+ capture_skipped_intro_hint(ctx, position);
path.items.truncate(saved_path_len);
per_byte.truncate(saved_byte_len);
NodeWalkResult::Matched {
@@ -1019,6 +1035,7 @@ fn walk_optional(
// Inner reported Incomplete without consuming
// anything — same as NoMatch from the user's
// perspective. Roll back and skip.
+ capture_skipped_intro_hint(ctx, position);
path.items.truncate(saved_path_len);
per_byte.truncate(saved_byte_len);
let _ = p;
diff --git a/src/dsl/walker/mod.rs b/src/dsl/walker/mod.rs
index 3723f20..575ec48 100644
--- a/src/dsl/walker/mod.rs
+++ b/src/dsl/walker/mod.rs
@@ -116,6 +116,19 @@ pub fn hint_resolution_at_input_in_mode(
use crate::dsl::grammar::HintMode;
let snap = expected_for_hint_snapshot(source, schema, mode);
+ // Issue #26: an optional positional slot with no candidate text
+ // (the `seed ` row count) left an `IntroProse` hint that
+ // survived the trailing optionals. It is shown even for an
+ // otherwise-complete command (empty expected set) — that is exactly
+ // the `seed users ▮` case where the count is invisible. Checked
+ // first, before the complete-command short-circuit below.
+ if let Some(key) = snap.surviving_intro_hint {
+ return Some(HintResolution {
+ mode: HintMode::IntroProse(key),
+ column: None,
+ form_b_autogen_skipped: Vec::new(),
+ });
+ }
// Empty expected set means the command is already complete
// (`WalkOutcome::Match`) — no slot to hint at.
if snap.expected.is_empty() {
@@ -2599,6 +2612,11 @@ struct HintWalkSnapshot {
/// The grammar-declared `HintMode` at the cursor's slot
/// (`Node::Hinted` annotation, ADR-0024 §HintMode-per-node).
pending_hint_mode: Option,
+ /// An `IntroProse` catalog key for an *optional* positional slot at
+ /// the cursor that produced no candidate (issue #26 — `seed `
+ /// row count). Survives the trailing optional siblings that clear
+ /// `pending_hint_mode`; already filtered to the cursor position.
+ surviving_intro_hint: Option<&'static str>,
current_table_columns: Option>,
/// `Some` when the input used Form A's explicit column list.
/// `None` for Form B (`insert into T values …`) and for
@@ -2625,6 +2643,7 @@ fn expected_for_hint_snapshot(
pending_value_type: None,
pending_value_column: None,
pending_hint_mode: None,
+ surviving_intro_hint: None,
current_table_columns: None,
user_listed_columns: None,
};
@@ -2652,6 +2671,14 @@ fn expected_for_hint_snapshot(
pending_value_type: ctx.pending_value_type,
pending_value_column: ctx.pending_value_column,
pending_hint_mode: ctx.pending_hint_mode,
+ // Issue #26: only surface the skipped-optional hint when the
+ // optional was skipped *at the cursor* (the end of the walked
+ // slice). Captured earlier (before a later clause consumed past
+ // it) → stale, so drop it.
+ surviving_intro_hint: ctx
+ .surviving_intro_hint
+ .filter(|(_, pos)| *pos == source.len())
+ .map(|(key, _)| key),
current_table_columns: ctx.current_table_columns,
user_listed_columns: ctx.user_listed_columns,
}
diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs
index 57e2ed3..f5244b6 100644
--- a/src/friendly/keys.rs
+++ b/src/friendly/keys.rs
@@ -231,6 +231,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// slot (`create table T (`) so the otherwise-invisible
// column-name role reads as the dominant first move.
("hint.create_table_element", &[]),
+ ("hint.seed_count", &[]),
("hint.value_literal_slot", &[]),
(
"hint.ambient_typing_name_then",
diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml
index 5999ff8..40930df 100644
--- a/src/friendly/strings/en-US.yaml
+++ b/src/friendly/strings/en-US.yaml
@@ -400,6 +400,12 @@ hint:
# at `create table T (` so the column-name role is visible
# alongside the table-level constraint keywords.
create_table_element: "Type a column name, or a table-level constraint: `primary`, `unique`, `check`, `constraint`, `foreign`"
+ # Issue #26: the `seed ▮` position. The optional row count is
+ # a bare number with no Tab candidate, so it (and the `.column`
+ # column-fill form) would be invisible next to the `set` / `--seed`
+ # chips. Names every option so the most common next move (a count) is
+ # discoverable.
+ seed_count: "Optionally a row count, e.g. `50` (default 20); `.column` to fill one column on existing rows; `set` to pin a column; `--seed` to fix the RNG"
# Value-literal slot — `insert ... values (`, `update ... set
# col=`, `where col=`. Replaces the misleading "null true
# false" keyword candidate list with format guidance for all
diff --git a/src/input_render.rs b/src/input_render.rs
index cf11299..01af66b 100644
--- a/src/input_render.rs
+++ b/src/input_render.rs
@@ -1356,6 +1356,93 @@ mod tests {
}
}
+ fn seed_cache() -> crate::completion::SchemaCache {
+ use crate::completion::TableColumn;
+ use crate::dsl::types::Type;
+ let mut cache = crate::completion::SchemaCache::default();
+ cache.tables.push("users".to_string());
+ cache.columns.push("email".to_string());
+ cache
+ .table_columns
+ .insert("users".to_string(), vec![TableColumn::new("email", Type::Text)]);
+ cache
+ }
+
+ #[test]
+ fn seed_count_is_advertised_at_the_optional_position() {
+ // Issue #26: `seed users ▮` is a complete command, so the hint
+ // ladder shows only the `set` / `--seed` continuation chips —
+ // the optional row count (a bare number with no candidate) was
+ // invisible. An IntroProse hint that survives the trailing
+ // optionals now advertises it; Tab still cycles the keywords.
+ let cache = seed_cache();
+ let input = "seed users ";
+ match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Simple) {
+ Some(AmbientHint::Prose(p)) => {
+ assert!(
+ p.contains("row count") && p.contains("20"),
+ "prose must mention the row count and the default; got: {p:?}",
+ );
+ assert!(
+ p.contains("set") && p.contains("--seed") && p.contains(".column"),
+ "prose should fold in the keyword + column-fill options; got: {p:?}",
+ );
+ }
+ other => panic!("expected a Prose count hint; got: {other:?}"),
+ }
+ // Tab candidates remain available (completion is independent).
+ let comp = crate::completion::candidates_at_cursor_in_mode(
+ input, input.len(), &cache, Mode::Simple,
+ )
+ .expect("completion remains available");
+ let texts: Vec<&str> = comp.candidates.iter().map(|c| c.text.as_str()).collect();
+ assert!(
+ texts.contains(&"set") && texts.contains(&"--seed"),
+ "Tab must still cycle `set` / `--seed`; got {texts:?}",
+ );
+
+ // `seed` runs in both modes (ADR-0048), so the hint must fire in
+ // advanced mode too — not only simple.
+ match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced) {
+ Some(AmbientHint::Prose(p)) => assert!(
+ p.contains("row count"),
+ "count hint must also fire in advanced mode; got: {p:?}",
+ ),
+ other => panic!("expected the count hint in advanced mode; got: {other:?}"),
+ }
+ }
+
+ #[test]
+ fn seed_count_hint_does_not_leak_once_the_count_or_a_clause_is_given() {
+ // Position guard: the hint shows only while the cursor sits at
+ // the count slot. Once the count is supplied — or a later clause
+ // consumes input past it — it must not reappear.
+ let cache = seed_cache();
+ for input in ["seed users 50 ", "seed users set email = 'x' "] {
+ let hint = ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Simple);
+ let is_count_prose = matches!(
+ &hint,
+ Some(AmbientHint::Prose(p)) if p.contains("row count")
+ );
+ assert!(!is_count_prose, "count hint must not show for {input:?}; got {hint:?}");
+ }
+ }
+
+ #[test]
+ fn seed_count_hint_also_fires_after_a_column_fill_target() {
+ // The count is valid after `seed users.email` too, so the hint
+ // fires there — `.email` is a real column (no diagnostic).
+ let cache = seed_cache();
+ let input = "seed users.email ";
+ match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Simple) {
+ Some(AmbientHint::Prose(p)) => assert!(
+ p.contains("row count"),
+ "count hint expected after a column-fill target; got: {p:?}",
+ ),
+ other => panic!("expected a Prose count hint; got: {other:?}"),
+ }
+ }
+
#[test]
fn genuine_column_typo_in_complete_select_still_hints_via_diagnostic() {
// Issue #6 trade-off lockdown: dropping the typing-time