Walker: node-attached HintMode via Node::Hinted (ADR-0024 §HintMode-per-node)
Replaces the hint resolver's signature-matching (does the expected set
contain all five literal forms? an Ident{NewName}?) with a grammar-
declared annotation. New Node::Hinted { mode, inner } wrapper; the
walker records the mode in WalkContext::pending_hint_mode on entry and
clears it on any successful match (cursor moved past the slot — this
also undoes the leak where a failed Hinted branch of a Choice would
otherwise strand a stale mode). The resolver reads pending_hint_mode
directly.
Value-literal fallback slots carry ProseOnly; NewName ident slots carry
ForceProse. hint_mode_at_input_inner now delegates to
hint_resolution_at_input — one resolution path, no duplicated logic.
No behaviour change; the typing-surface matrix guards it.
This commit is contained in:
+17
-10
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
use crate::dsl::command::{AppCommand, Command, MessagesValue, ModeValue};
|
use crate::dsl::command::{AppCommand, Command, MessagesValue, ModeValue};
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, IdentSource, IdentValidator, Node, ValidationError, Word,
|
CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError,
|
||||||
|
Word,
|
||||||
};
|
};
|
||||||
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
||||||
|
|
||||||
@@ -43,17 +44,23 @@ const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages;
|
|||||||
|
|
||||||
const SAVE_AS_WORD: Node = Node::Word(Word::keyword("as"));
|
const SAVE_AS_WORD: Node = Node::Word(Word::keyword("as"));
|
||||||
|
|
||||||
|
const IMPORT_TARGET_IDENT: Node = Node::Ident {
|
||||||
|
source: IdentSource::NewName,
|
||||||
|
role: "target",
|
||||||
|
validator: None,
|
||||||
|
highlight_override: None,
|
||||||
|
writes_table: false,
|
||||||
|
writes_column: false,
|
||||||
|
writes_user_listed_column: false,
|
||||||
|
};
|
||||||
|
const IMPORT_TARGET: Node = Node::Hinted {
|
||||||
|
mode: HintMode::ForceProse("hint.ambient_typing_name"),
|
||||||
|
inner: &IMPORT_TARGET_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
const IMPORT_AS_TARGET: Node = Node::Seq(&[
|
const IMPORT_AS_TARGET: Node = Node::Seq(&[
|
||||||
Node::Word(Word::keyword("as")),
|
Node::Word(Word::keyword("as")),
|
||||||
Node::Ident {
|
IMPORT_TARGET,
|
||||||
source: IdentSource::NewName,
|
|
||||||
role: "target",
|
|
||||||
validator: None,
|
|
||||||
highlight_override: None,
|
|
||||||
writes_table: false,
|
|
||||||
writes_column: false,
|
|
||||||
writes_user_listed_column: false,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
|
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use crate::dsl::command::{Command, RowFilter};
|
use crate::dsl::command::{Command, RowFilter};
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, IdentSource, Node, ValidationError, Word,
|
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
||||||
shared::{column_value_list, current_column_value},
|
shared::{column_value_list, current_column_value},
|
||||||
};
|
};
|
||||||
use crate::dsl::value::Value;
|
use crate::dsl::value::Value;
|
||||||
@@ -61,7 +61,15 @@ const VALUE_LITERAL_CHOICES: &[Node] = &[
|
|||||||
Node::NumberLit { validator: None },
|
Node::NumberLit { validator: None },
|
||||||
Node::StringLit,
|
Node::StringLit,
|
||||||
];
|
];
|
||||||
const VALUE_LITERAL: Node = Node::Choice(VALUE_LITERAL_CHOICES);
|
const VALUE_LITERAL_INNER: Node = Node::Choice(VALUE_LITERAL_CHOICES);
|
||||||
|
/// Value-literal slot with the `ProseOnly` HintMode
|
||||||
|
/// (ADR-0024 §HintMode-per-node) — the hint resolver surfaces
|
||||||
|
/// the generic "Type a value: …" prose rather than the
|
||||||
|
/// misleading `null`/`true`/`false` candidate trio.
|
||||||
|
const VALUE_LITERAL: Node = Node::Hinted {
|
||||||
|
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
||||||
|
inner: &VALUE_LITERAL_INNER,
|
||||||
|
};
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// show — `show (data|table) <T>`
|
// show — `show (data|table) <T>`
|
||||||
|
|||||||
+52
-22
@@ -14,9 +14,15 @@
|
|||||||
use crate::dsl::action::ReferentialAction;
|
use crate::dsl::action::ReferentialAction;
|
||||||
use crate::dsl::command::{ChangeColumnMode, ColumnSpec, Command, RelationshipSelector};
|
use crate::dsl::command::{ChangeColumnMode, ColumnSpec, Command, RelationshipSelector};
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
CommandNode, IdentSource, Node, ValidationError, Word,
|
CommandNode, HintMode, IdentSource, Node, ValidationError, Word,
|
||||||
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
shared::{REFERENTIAL_CLAUSES, TYPE_SLOT, TYPE_VALIDATOR},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// `HintMode` annotation shared by every `NewName` ident slot:
|
||||||
|
/// the user is inventing a name, so the hint panel forces the
|
||||||
|
/// "Type a name [then …]" prose rather than offering schema
|
||||||
|
/// candidates (ADR-0024 §HintMode-per-node).
|
||||||
|
const NEW_NAME_HINT: HintMode = HintMode::ForceProse("hint.ambient_typing_name");
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
||||||
|
|
||||||
@@ -24,7 +30,7 @@ use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};
|
|||||||
// Building blocks
|
// Building blocks
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
const TABLE_NAME_NEW: Node = Node::Ident {
|
const TABLE_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
source: IdentSource::NewName,
|
source: IdentSource::NewName,
|
||||||
role: "table_name",
|
role: "table_name",
|
||||||
validator: None,
|
validator: None,
|
||||||
@@ -33,6 +39,10 @@ const TABLE_NAME_NEW: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
const TABLE_NAME_NEW: Node = Node::Hinted {
|
||||||
|
mode: NEW_NAME_HINT,
|
||||||
|
inner: &TABLE_NAME_NEW_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
// `writes_table: true` so that the column-name slots that
|
// `writes_table: true` so that the column-name slots that
|
||||||
// follow the table name in `drop column` / `rename column` /
|
// follow the table name in `drop column` / `rename column` /
|
||||||
@@ -61,7 +71,7 @@ const COLUMN_NAME: Node = Node::Ident {
|
|||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_NAME_NEW: Node = Node::Ident {
|
const COLUMN_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
source: IdentSource::NewName,
|
source: IdentSource::NewName,
|
||||||
role: "column_name",
|
role: "column_name",
|
||||||
validator: None,
|
validator: None,
|
||||||
@@ -70,6 +80,10 @@ const COLUMN_NAME_NEW: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
const COLUMN_NAME_NEW: Node = Node::Hinted {
|
||||||
|
mode: NEW_NAME_HINT,
|
||||||
|
inner: &COLUMN_NAME_NEW_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_NAME: Node = Node::Ident {
|
const RELATIONSHIP_NAME: Node = Node::Ident {
|
||||||
source: IdentSource::Relationships,
|
source: IdentSource::Relationships,
|
||||||
@@ -81,7 +95,7 @@ const RELATIONSHIP_NAME: Node = Node::Ident {
|
|||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
|
const RELATIONSHIP_NAME_NEW_IDENT: Node = Node::Ident {
|
||||||
source: IdentSource::NewName,
|
source: IdentSource::NewName,
|
||||||
role: "relationship_name",
|
role: "relationship_name",
|
||||||
validator: None,
|
validator: None,
|
||||||
@@ -90,6 +104,10 @@ const RELATIONSHIP_NAME_NEW: Node = Node::Ident {
|
|||||||
writes_column: false,
|
writes_column: false,
|
||||||
writes_user_listed_column: false,
|
writes_user_listed_column: false,
|
||||||
};
|
};
|
||||||
|
const RELATIONSHIP_NAME_NEW: Node = Node::Hinted {
|
||||||
|
mode: NEW_NAME_HINT,
|
||||||
|
inner: &RELATIONSHIP_NAME_NEW_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
// `[to]` and `[table]` connectives.
|
// `[to]` and `[table]` connectives.
|
||||||
const TO_OPT: Node = Node::Optional(&Node::Word(Word::keyword("to")));
|
const TO_OPT: Node = Node::Optional(&Node::Word(Word::keyword("to")));
|
||||||
@@ -308,6 +326,20 @@ const ADD_SHAPE: Node = Node::Choice(ADD_CHOICES);
|
|||||||
// rename_column — `rename column [in] [table] <T> : <col> to <new>`
|
// rename_column — `rename column [in] [table] <T> : <col> to <new>`
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
|
const NEW_COLUMN_NAME_IDENT: Node = Node::Ident {
|
||||||
|
source: IdentSource::NewName,
|
||||||
|
role: "new_column_name",
|
||||||
|
validator: None,
|
||||||
|
highlight_override: None,
|
||||||
|
writes_table: false,
|
||||||
|
writes_column: false,
|
||||||
|
writes_user_listed_column: false,
|
||||||
|
};
|
||||||
|
const NEW_COLUMN_NAME: Node = Node::Hinted {
|
||||||
|
mode: NEW_NAME_HINT,
|
||||||
|
inner: &NEW_COLUMN_NAME_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
const RENAME_COLUMN_NODES: &[Node] = &[
|
const RENAME_COLUMN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("column")),
|
Node::Word(Word::keyword("column")),
|
||||||
IN_OPT,
|
IN_OPT,
|
||||||
@@ -316,15 +348,7 @@ const RENAME_COLUMN_NODES: &[Node] = &[
|
|||||||
Node::Punct(':'),
|
Node::Punct(':'),
|
||||||
COLUMN_NAME,
|
COLUMN_NAME,
|
||||||
Node::Word(Word::keyword("to")),
|
Node::Word(Word::keyword("to")),
|
||||||
Node::Ident {
|
NEW_COLUMN_NAME,
|
||||||
source: IdentSource::NewName,
|
|
||||||
role: "new_column_name",
|
|
||||||
validator: None,
|
|
||||||
highlight_override: None,
|
|
||||||
writes_table: false,
|
|
||||||
writes_column: false,
|
|
||||||
writes_user_listed_column: false,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES);
|
const RENAME_COLUMN: Node = Node::Seq(RENAME_COLUMN_NODES);
|
||||||
|
|
||||||
@@ -650,16 +674,22 @@ pub static CHANGE: CommandNode = CommandNode {
|
|||||||
// (Phase C)
|
// (Phase C)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
|
const COL_NAME_IDENT: Node = Node::Ident {
|
||||||
|
source: IdentSource::NewName,
|
||||||
|
role: "col_name",
|
||||||
|
validator: None,
|
||||||
|
highlight_override: None,
|
||||||
|
writes_table: false,
|
||||||
|
writes_column: false,
|
||||||
|
writes_user_listed_column: false,
|
||||||
|
};
|
||||||
|
const COL_NAME: Node = Node::Hinted {
|
||||||
|
mode: NEW_NAME_HINT,
|
||||||
|
inner: &COL_NAME_IDENT,
|
||||||
|
};
|
||||||
|
|
||||||
const COL_SPEC_NODES: &[Node] = &[
|
const COL_SPEC_NODES: &[Node] = &[
|
||||||
Node::Ident {
|
COL_NAME,
|
||||||
source: IdentSource::NewName,
|
|
||||||
role: "col_name",
|
|
||||||
validator: None,
|
|
||||||
highlight_override: None,
|
|
||||||
writes_table: false,
|
|
||||||
writes_column: false,
|
|
||||||
writes_user_listed_column: false,
|
|
||||||
},
|
|
||||||
Node::Punct(':'),
|
Node::Punct(':'),
|
||||||
Node::Ident {
|
Node::Ident {
|
||||||
source: IdentSource::Types,
|
source: IdentSource::Types,
|
||||||
|
|||||||
@@ -308,6 +308,24 @@ pub enum Node {
|
|||||||
column_name: Option<&'static str>,
|
column_name: Option<&'static str>,
|
||||||
inner: &'static Self,
|
inner: &'static Self,
|
||||||
},
|
},
|
||||||
|
/// Annotates `inner` with a hint-panel `HintMode` (ADR-0024
|
||||||
|
/// §HintMode-per-node). On entry the walker records `mode`
|
||||||
|
/// in `WalkContext::pending_hint_mode`; on a successful
|
||||||
|
/// inner match the record clears (so positions past the
|
||||||
|
/// slot don't carry stale hint state). Transparent to
|
||||||
|
/// matching, highlighting and the expected-set otherwise —
|
||||||
|
/// it walks `inner` and returns its result verbatim.
|
||||||
|
///
|
||||||
|
/// This is the node-attached replacement for the hint
|
||||||
|
/// resolver's earlier signature-matching: the grammar tree
|
||||||
|
/// declares the hint mode at the slot, the walker
|
||||||
|
/// propagates it, the resolver reads it. Used by the
|
||||||
|
/// value-literal fallback slot (`ProseOnly`) and `NewName`
|
||||||
|
/// ident slots (`ForceProse`).
|
||||||
|
Hinted {
|
||||||
|
mode: HintMode,
|
||||||
|
inner: &'static Self,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level entry record. One per command. The `entry` keyword
|
/// Top-level entry record. One per command. The `entry` keyword
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
use crate::completion::TableColumn;
|
use crate::completion::TableColumn;
|
||||||
use crate::dsl::grammar::{
|
use crate::dsl::grammar::{
|
||||||
IdentSource, IdentValidator, Node, NumberValidator, ValidationError, Word,
|
HintMode, IdentSource, IdentValidator, Node, NumberValidator,
|
||||||
|
ValidationError, Word,
|
||||||
};
|
};
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
use crate::dsl::walker::context::WalkContext;
|
use crate::dsl::walker::context::WalkContext;
|
||||||
@@ -347,7 +348,16 @@ const FALLBACK_VALUE_LITERAL_CHOICES: &[Node] = &[
|
|||||||
Node::NumberLit { validator: None },
|
Node::NumberLit { validator: None },
|
||||||
Node::StringLit,
|
Node::StringLit,
|
||||||
];
|
];
|
||||||
const FALLBACK_VALUE_LITERAL: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
|
const FALLBACK_VALUE_LITERAL_INNER: Node = Node::Choice(FALLBACK_VALUE_LITERAL_CHOICES);
|
||||||
|
/// The schemaless value-literal slot. The `ProseOnly` HintMode
|
||||||
|
/// (ADR-0024 §HintMode-per-node) tells the hint resolver to
|
||||||
|
/// surface the generic "Type a value: number, 'text', …" prose
|
||||||
|
/// here rather than the misleading `null`/`true`/`false`
|
||||||
|
/// candidate trio.
|
||||||
|
const FALLBACK_VALUE_LITERAL: Node = Node::Hinted {
|
||||||
|
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
||||||
|
inner: &FALLBACK_VALUE_LITERAL_INNER,
|
||||||
|
};
|
||||||
|
|
||||||
const FALLBACK_VALUE_LIST: Node = Node::Repeated {
|
const FALLBACK_VALUE_LIST: Node = Node::Repeated {
|
||||||
inner: &FALLBACK_VALUE_LITERAL,
|
inner: &FALLBACK_VALUE_LITERAL,
|
||||||
@@ -434,10 +444,3 @@ pub fn column_value_list(ctx: &WalkContext) -> Node {
|
|||||||
}
|
}
|
||||||
Node::Seq(Box::leak(children.into_boxed_slice()))
|
Node::Seq(Box::leak(children.into_boxed_slice()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The HintMode / NumberValidator imports are part of the Phase D
|
|
||||||
// typed-slot toolkit even though only NumberValidator is used by
|
|
||||||
// the explicit validators above; surface HintMode so future
|
|
||||||
// per-type prose annotations can attach without re-importing.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const _USES_HINT_MODE: Option<crate::dsl::grammar::HintMode> = None;
|
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ pub struct WalkContext<'a> {
|
|||||||
/// Cleared on successful inner match alongside
|
/// Cleared on successful inner match alongside
|
||||||
/// `pending_value_type`.
|
/// `pending_value_type`.
|
||||||
pub pending_value_column: Option<String>,
|
pub pending_value_column: Option<String>,
|
||||||
|
/// The hint-panel `HintMode` declared by the grammar node
|
||||||
|
/// the walker is currently inside (ADR-0024
|
||||||
|
/// §HintMode-per-node). Set on entry to a `Node::Hinted`
|
||||||
|
/// wrapper, cleared on successful inner match. The hint
|
||||||
|
/// resolver reads this directly instead of inferring the
|
||||||
|
/// slot kind from the shape of the expected set.
|
||||||
|
pub pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
|
||||||
/// The columns the user explicitly listed in
|
/// The columns the user explicitly listed in
|
||||||
/// `insert into <T> (col1, col2, …) values (…)` (Form A),
|
/// `insert into <T> (col1, col2, …) values (…)` (Form A),
|
||||||
/// in declaration order.
|
/// in declaration order.
|
||||||
@@ -91,6 +98,7 @@ impl<'a> WalkContext<'a> {
|
|||||||
current_column: None,
|
current_column: None,
|
||||||
pending_value_type: None,
|
pending_value_type: None,
|
||||||
pending_value_column: None,
|
pending_value_column: None,
|
||||||
|
pending_hint_mode: None,
|
||||||
user_listed_columns: None,
|
user_listed_columns: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,28 @@ pub fn walk_node(
|
|||||||
per_byte: &mut Vec<ByteClass>,
|
per_byte: &mut Vec<ByteClass>,
|
||||||
) -> NodeWalkResult {
|
) -> NodeWalkResult {
|
||||||
let pos = skip_whitespace(source, position);
|
let pos = skip_whitespace(source, position);
|
||||||
|
let result = walk_node_inner(source, pos, node, ctx, path, per_byte);
|
||||||
|
// ADR-0024 §HintMode-per-node: `pending_hint_mode` records
|
||||||
|
// the Hinted slot the cursor is currently inside. Any
|
||||||
|
// successful match means the cursor advanced past whatever
|
||||||
|
// slot was pending — clear it. This also undoes the leak
|
||||||
|
// where a failed `Hinted` branch of a `Choice` sets the
|
||||||
|
// mode and the `Choice` then matches via a different
|
||||||
|
// branch: that branch's match clears the stale mode.
|
||||||
|
if matches!(result, NodeWalkResult::Matched { .. }) {
|
||||||
|
ctx.pending_hint_mode = None;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_node_inner(
|
||||||
|
source: &str,
|
||||||
|
pos: usize,
|
||||||
|
node: &Node,
|
||||||
|
ctx: &mut WalkContext,
|
||||||
|
path: &mut MatchedPath,
|
||||||
|
per_byte: &mut Vec<ByteClass>,
|
||||||
|
) -> NodeWalkResult {
|
||||||
match node {
|
match node {
|
||||||
Node::Word(word) => walk_word(source, pos, word, path, per_byte),
|
Node::Word(word) => walk_word(source, pos, word, path, per_byte),
|
||||||
Node::Punct(ch) => walk_punct(source, pos, *ch, path, per_byte),
|
Node::Punct(ch) => walk_punct(source, pos, *ch, path, per_byte),
|
||||||
@@ -156,6 +178,16 @@ pub fn walk_node(
|
|||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
Node::Hinted { mode, inner } => {
|
||||||
|
// ADR-0024 §HintMode-per-node. Record the grammar's
|
||||||
|
// declared hint mode so the hint resolver can read
|
||||||
|
// it directly. The `walk_node` wrapper clears it on
|
||||||
|
// any successful match (the cursor moved past the
|
||||||
|
// slot), so a Hinted slot whose inner fails at EOF
|
||||||
|
// leaves the mode set for the resolver to read.
|
||||||
|
ctx.pending_hint_mode = Some(*mode);
|
||||||
|
walk_node(source, pos, inner, ctx, path, per_byte)
|
||||||
|
}
|
||||||
Node::Flag(name) => walk_flag(source, pos, name, path, per_byte),
|
Node::Flag(name) => walk_flag(source, pos, name, path, per_byte),
|
||||||
Node::Repeated {
|
Node::Repeated {
|
||||||
inner,
|
inner,
|
||||||
|
|||||||
+44
-122
@@ -95,15 +95,19 @@ pub fn hint_resolution_at_input(
|
|||||||
source: &str,
|
source: &str,
|
||||||
schema: Option<&crate::completion::SchemaCache>,
|
schema: Option<&crate::completion::SchemaCache>,
|
||||||
) -> Option<HintResolution> {
|
) -> Option<HintResolution> {
|
||||||
use crate::dsl::grammar::{HintMode, IdentSource};
|
use crate::dsl::grammar::HintMode;
|
||||||
use crate::dsl::walker::outcome::Expectation;
|
|
||||||
|
|
||||||
let snap = expected_for_hint_snapshot(source, schema);
|
let snap = expected_for_hint_snapshot(source, schema);
|
||||||
|
// Empty expected set means the command is already complete
|
||||||
|
// (`WalkOutcome::Match`) — no slot to hint at.
|
||||||
if snap.expected.is_empty() {
|
if snap.expected.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let expected = snap.expected;
|
|
||||||
|
|
||||||
|
// Typed value slot: the walker tagged `pending_value_type`
|
||||||
|
// on entry to a `Node::TypedValueSlot`. Per-column-type
|
||||||
|
// prose, narrowed by the column's user-facing type, plus
|
||||||
|
// the Form B auto-gen pedagogical note.
|
||||||
if let Some(ty) = snap.pending_value_type {
|
if let Some(ty) = snap.pending_value_type {
|
||||||
return Some(HintResolution {
|
return Some(HintResolution {
|
||||||
mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)),
|
mode: HintMode::ProseOnly(catalog_key_for_value_type(ty)),
|
||||||
@@ -117,42 +121,23 @@ pub fn hint_resolution_at_input(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_word = |w: &str| {
|
// Node-attached HintMode (ADR-0024 §HintMode-per-node): the
|
||||||
expected
|
// grammar declares the mode at the slot via `Node::Hinted`;
|
||||||
.iter()
|
// the walker recorded it in `pending_hint_mode`. The hint
|
||||||
.any(|e| matches!(e, Expectation::Word(x) if *x == w))
|
// resolver reads it directly — no signature-matching on the
|
||||||
};
|
// shape of the expected set. `ProseOnly` covers the
|
||||||
let value_literal_slot = has_word("null")
|
// value-literal fallback slot; `ForceProse` covers `NewName`
|
||||||
&& has_word("true")
|
// ident slots ("Type a name").
|
||||||
&& has_word("false")
|
match snap.pending_hint_mode {
|
||||||
&& expected.iter().any(|e| matches!(e, Expectation::NumberLit))
|
Some(mode @ (HintMode::ProseOnly(_) | HintMode::ForceProse(_))) => {
|
||||||
&& expected.iter().any(|e| matches!(e, Expectation::StringLit));
|
Some(HintResolution {
|
||||||
if value_literal_slot {
|
mode,
|
||||||
return Some(HintResolution {
|
column: None,
|
||||||
mode: HintMode::ProseOnly("hint.value_literal_slot"),
|
form_b_autogen_skipped: Vec::new(),
|
||||||
column: None,
|
})
|
||||||
form_b_autogen_skipped: Vec::new(),
|
}
|
||||||
});
|
Some(HintMode::SuppressProse | HintMode::Default) | None => 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,
|
|
||||||
form_b_autogen_skipped: Vec::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-generated columns a Form B insert skips from its value
|
/// Auto-generated columns a Form B insert skips from its value
|
||||||
@@ -206,68 +191,12 @@ fn hint_mode_at_input_inner(
|
|||||||
source: &str,
|
source: &str,
|
||||||
schema: Option<&crate::completion::SchemaCache>,
|
schema: Option<&crate::completion::SchemaCache>,
|
||||||
) -> Option<crate::dsl::grammar::HintMode> {
|
) -> Option<crate::dsl::grammar::HintMode> {
|
||||||
use crate::dsl::grammar::{HintMode, IdentSource};
|
// Single source of truth: `hint_resolution_at_input` already
|
||||||
use crate::dsl::walker::outcome::Expectation;
|
// resolves the slot's HintMode (typed-value-slot per-type
|
||||||
|
// prose, or the node-attached `Node::Hinted` annotation).
|
||||||
// Hint mode is only meaningful at *required* slot positions
|
// This thin wrapper just drops the resolution's column /
|
||||||
// (Incomplete / Mismatch outcomes). For complete commands
|
// skip detail for callers that only need the mode.
|
||||||
// (Match), `tail_expected` may carry optional-suffix
|
hint_resolution_at_input(source, schema).map(|r| r.mode)
|
||||||
// expectations — completion surfaces those as Tab
|
|
||||||
// candidates, but the hint resolver should stay silent so
|
|
||||||
// we don't push prose like "Type a name" at the end of a
|
|
||||||
// valid command.
|
|
||||||
let (expected, pending_value_type) = expected_for_hint_with_ctx(source, schema);
|
|
||||||
if expected.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typed value slot at the cursor: the walker tagged
|
|
||||||
// ctx.pending_value_type on entry to the slot but did not
|
|
||||||
// clear it (no inner literal matched). Emit per-type prose.
|
|
||||||
if let Some(ty) = pending_value_type {
|
|
||||||
return Some(HintMode::ProseOnly(catalog_key_for_value_type(ty)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value-literal slot signature: all five forms present.
|
|
||||||
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 {
|
|
||||||
// Fallback prose: lists every literal form with format
|
|
||||||
// examples. Fires when the walker can't resolve a column
|
|
||||||
// type at the cursor (schemaless caller, missing table,
|
|
||||||
// unknown column).
|
|
||||||
return Some(HintMode::ProseOnly("hint.value_literal_slot"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewName ident slot: user invents a name.
|
|
||||||
let new_name_slot = expected.iter().any(|e| {
|
|
||||||
matches!(
|
|
||||||
e,
|
|
||||||
Expectation::Ident {
|
|
||||||
source: IdentSource::NewName,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if new_name_slot {
|
|
||||||
// The "Type a name" prose key is selected by the
|
|
||||||
// ambient_hint dispatch — the `ForceProse` key here is
|
|
||||||
// a stable identifier the resolver maps to one of the
|
|
||||||
// two variants (`hint.ambient_typing_name` /
|
|
||||||
// `hint.ambient_typing_name_then`) depending on whether
|
|
||||||
// a next-token probe yields content.
|
|
||||||
return Some(HintMode::ForceProse("hint.ambient_typing_name"));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str {
|
const fn catalog_key_for_value_type(ty: crate::dsl::types::Type) -> &'static str {
|
||||||
@@ -421,6 +350,9 @@ struct HintWalkSnapshot {
|
|||||||
expected: Vec<outcome::Expectation>,
|
expected: Vec<outcome::Expectation>,
|
||||||
pending_value_type: Option<crate::dsl::types::Type>,
|
pending_value_type: Option<crate::dsl::types::Type>,
|
||||||
pending_value_column: Option<String>,
|
pending_value_column: Option<String>,
|
||||||
|
/// The grammar-declared `HintMode` at the cursor's slot
|
||||||
|
/// (`Node::Hinted` annotation, ADR-0024 §HintMode-per-node).
|
||||||
|
pending_hint_mode: Option<crate::dsl::grammar::HintMode>,
|
||||||
current_table_columns: Option<Vec<crate::completion::TableColumn>>,
|
current_table_columns: Option<Vec<crate::completion::TableColumn>>,
|
||||||
/// `Some` when the input used Form A's explicit column list.
|
/// `Some` when the input used Form A's explicit column list.
|
||||||
/// `None` for Form B (`insert into T values …`) and for
|
/// `None` for Form B (`insert into T values …`) and for
|
||||||
@@ -428,14 +360,6 @@ struct HintWalkSnapshot {
|
|||||||
user_listed_columns: Option<Vec<String>>,
|
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 snap = expected_for_hint_snapshot(source, schema);
|
|
||||||
(snap.expected, snap.pending_value_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expected_for_hint_snapshot(
|
fn expected_for_hint_snapshot(
|
||||||
source: &str,
|
source: &str,
|
||||||
schema: Option<&crate::completion::SchemaCache>,
|
schema: Option<&crate::completion::SchemaCache>,
|
||||||
@@ -449,27 +373,24 @@ fn expected_for_hint_snapshot(
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let empty_snapshot = || HintWalkSnapshot {
|
||||||
|
expected: entry_words(),
|
||||||
|
pending_value_type: None,
|
||||||
|
pending_value_column: None,
|
||||||
|
pending_hint_mode: None,
|
||||||
|
current_table_columns: None,
|
||||||
|
user_listed_columns: None,
|
||||||
|
};
|
||||||
|
|
||||||
if source.trim().is_empty() {
|
if source.trim().is_empty() {
|
||||||
return HintWalkSnapshot {
|
return empty_snapshot();
|
||||||
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| {
|
let mut ctx = schema.map_or_else(context::WalkContext::new, |s| {
|
||||||
context::WalkContext::with_schema(s)
|
context::WalkContext::with_schema(s)
|
||||||
});
|
});
|
||||||
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
let (result, _cmd) = walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||||
let Some(result) = result else {
|
let Some(result) = result else {
|
||||||
return HintWalkSnapshot {
|
return empty_snapshot();
|
||||||
expected: entry_words(),
|
|
||||||
pending_value_type: None,
|
|
||||||
pending_value_column: None,
|
|
||||||
current_table_columns: None,
|
|
||||||
user_listed_columns: None,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
let expected = match result.outcome {
|
let expected = match result.outcome {
|
||||||
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
|
outcome::WalkOutcome::Match { .. } | outcome::WalkOutcome::ValidationFailed { .. } => {
|
||||||
@@ -482,6 +403,7 @@ fn expected_for_hint_snapshot(
|
|||||||
expected,
|
expected,
|
||||||
pending_value_type: ctx.pending_value_type,
|
pending_value_type: ctx.pending_value_type,
|
||||||
pending_value_column: ctx.pending_value_column,
|
pending_value_column: ctx.pending_value_column,
|
||||||
|
pending_hint_mode: ctx.pending_hint_mode,
|
||||||
current_table_columns: ctx.current_table_columns,
|
current_table_columns: ctx.current_table_columns,
|
||||||
user_listed_columns: ctx.user_listed_columns,
|
user_listed_columns: ctx.user_listed_columns,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user