feat(hint): H2 Phase A — hint command + F1 keybinding skeleton (ADR-0053)
The mechanism for the contextual hint, with tier-2 fallback; the tier-3 corpus lands in later phases. - new CommandNode `hint_id` field (all None for now) - AppCommand::Hint + HINT grammar node + REGISTRY + dispatch - F1 read-only overlay in handle_key (buffer/cursor/memo untouched) - note_hint* renderers; hint_id_for_input_in_mode (shared selection helper refactored out of usage_keys_for_input_in_mode) - last_error_hint_key + friendly::error_hint_class classifier - catalogue: help.app.hint / parse.usage.hint / hint.getting_started - +12 tests; 2483 pass / 1 ignored, clippy clean
This commit is contained in:
@@ -177,6 +177,9 @@ const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result<Command, Va
|
||||
const fn build_undo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
Ok(Command::App(AppCommand::Undo))
|
||||
}
|
||||
const fn build_hint(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
Ok(Command::App(AppCommand::Hint))
|
||||
}
|
||||
|
||||
const fn build_redo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
Ok(Command::App(AppCommand::Redo))
|
||||
@@ -263,6 +266,7 @@ pub static QUIT: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_quit,
|
||||
help_id: Some("app.quit"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.quit"],};
|
||||
|
||||
pub static HELP: CommandNode = CommandNode {
|
||||
@@ -270,13 +274,24 @@ pub static HELP: CommandNode = CommandNode {
|
||||
shape: HELP_TOPIC_OPT,
|
||||
ast_builder: build_help,
|
||||
help_id: Some("app.help"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.help"],};
|
||||
|
||||
pub static HINT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("hint"),
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_hint,
|
||||
help_id: Some("app.hint"),
|
||||
// hint_id assigned in Phase C with the tier-3 corpus (ADR-0053).
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.hint"],};
|
||||
|
||||
pub static REBUILD: CommandNode = CommandNode {
|
||||
entry: Word::keyword("rebuild"),
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_rebuild,
|
||||
help_id: Some("app.rebuild"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.rebuild"],};
|
||||
|
||||
pub static SAVE: CommandNode = CommandNode {
|
||||
@@ -284,6 +299,7 @@ pub static SAVE: CommandNode = CommandNode {
|
||||
shape: SAVE_AS_OPT,
|
||||
ast_builder: build_save,
|
||||
help_id: Some("app.save"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.save"],};
|
||||
|
||||
pub static NEW: CommandNode = CommandNode {
|
||||
@@ -291,6 +307,7 @@ pub static NEW: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_new,
|
||||
help_id: Some("app.new"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.new"],};
|
||||
|
||||
pub static LOAD: CommandNode = CommandNode {
|
||||
@@ -298,6 +315,7 @@ pub static LOAD: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_load,
|
||||
help_id: Some("app.load"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.load"],};
|
||||
|
||||
pub static EXPORT: CommandNode = CommandNode {
|
||||
@@ -305,6 +323,7 @@ pub static EXPORT: CommandNode = CommandNode {
|
||||
shape: EXPORT_PATH_OPT,
|
||||
ast_builder: build_export,
|
||||
help_id: Some("app.export"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.export"],};
|
||||
|
||||
pub static IMPORT: CommandNode = CommandNode {
|
||||
@@ -312,6 +331,7 @@ pub static IMPORT: CommandNode = CommandNode {
|
||||
shape: IMPORT_BODY_OPT,
|
||||
ast_builder: build_import,
|
||||
help_id: Some("app.import"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.import"],};
|
||||
|
||||
pub static MODE: CommandNode = CommandNode {
|
||||
@@ -319,6 +339,7 @@ pub static MODE: CommandNode = CommandNode {
|
||||
shape: MODE_VALUE,
|
||||
ast_builder: build_mode,
|
||||
help_id: Some("app.mode"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.mode"],};
|
||||
|
||||
pub static MESSAGES: CommandNode = CommandNode {
|
||||
@@ -326,6 +347,7 @@ pub static MESSAGES: CommandNode = CommandNode {
|
||||
shape: MESSAGES_VALUE_OPT,
|
||||
ast_builder: build_messages,
|
||||
help_id: Some("app.messages"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.messages"],};
|
||||
|
||||
pub static UNDO: CommandNode = CommandNode {
|
||||
@@ -333,6 +355,7 @@ pub static UNDO: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_undo,
|
||||
help_id: Some("app.undo"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.undo"],};
|
||||
|
||||
pub static REDO: CommandNode = CommandNode {
|
||||
@@ -340,6 +363,7 @@ pub static REDO: CommandNode = CommandNode {
|
||||
shape: EMPTY_SEQ,
|
||||
ast_builder: build_redo,
|
||||
help_id: Some("app.redo"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.redo"],};
|
||||
|
||||
pub static COPY: CommandNode = CommandNode {
|
||||
@@ -347,4 +371,5 @@ pub static COPY: CommandNode = CommandNode {
|
||||
shape: COPY_VALUE_OPT,
|
||||
ast_builder: build_copy,
|
||||
help_id: Some("app.copy"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.copy"],};
|
||||
|
||||
@@ -1790,6 +1790,7 @@ pub static SHOW: CommandNode = CommandNode {
|
||||
shape: SHOW_SHAPE,
|
||||
ast_builder: build_show,
|
||||
help_id: Some("data.show"),
|
||||
hint_id: None,
|
||||
usage_ids: &[
|
||||
"parse.usage.show_data",
|
||||
"parse.usage.show_table",
|
||||
@@ -1805,6 +1806,7 @@ pub static SEED: CommandNode = CommandNode {
|
||||
shape: SEED_SHAPE,
|
||||
ast_builder: build_seed,
|
||||
help_id: Some("data.seed"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.seed"],
|
||||
};
|
||||
|
||||
@@ -1813,6 +1815,7 @@ pub static INSERT: CommandNode = CommandNode {
|
||||
shape: INSERT_SHAPE,
|
||||
ast_builder: build_insert,
|
||||
help_id: Some("data.insert"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.insert"],};
|
||||
|
||||
pub static UPDATE: CommandNode = CommandNode {
|
||||
@@ -1820,6 +1823,7 @@ pub static UPDATE: CommandNode = CommandNode {
|
||||
shape: UPDATE_SHAPE,
|
||||
ast_builder: build_update,
|
||||
help_id: Some("data.update"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.update"],};
|
||||
|
||||
pub static DELETE: CommandNode = CommandNode {
|
||||
@@ -1827,6 +1831,7 @@ pub static DELETE: CommandNode = CommandNode {
|
||||
shape: DELETE_SHAPE,
|
||||
ast_builder: build_delete,
|
||||
help_id: Some("data.delete"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.delete"],};
|
||||
|
||||
pub static REPLAY: CommandNode = CommandNode {
|
||||
@@ -1834,6 +1839,7 @@ pub static REPLAY: CommandNode = CommandNode {
|
||||
shape: REPLAY_PATH,
|
||||
ast_builder: build_replay,
|
||||
help_id: Some("data.replay"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.replay"],};
|
||||
|
||||
pub static EXPLAIN: CommandNode = CommandNode {
|
||||
@@ -1841,6 +1847,7 @@ pub static EXPLAIN: CommandNode = CommandNode {
|
||||
shape: EXPLAIN_SHAPE,
|
||||
ast_builder: build_explain,
|
||||
help_id: Some("data.explain"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.explain"],};
|
||||
|
||||
/// `explain` over advanced-mode SQL (ADR-0039).
|
||||
@@ -1860,6 +1867,7 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
||||
// too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`
|
||||
// precedent; otherwise `note_help` would print `explain` twice.
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &[],};
|
||||
|
||||
/// SQL `SELECT` (ADR-0030 §6, ADR-0031, ADR-0032).
|
||||
@@ -1875,6 +1883,7 @@ pub static SELECT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
|
||||
/// `WITH …` top-level statement (ADR-0032 §4 / sub-phase 2c).
|
||||
@@ -1889,6 +1898,7 @@ pub static WITH: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
|
||||
ast_builder: build_select,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.with"],};
|
||||
|
||||
/// SQL `INSERT` — the `Advanced`-category node of the shared
|
||||
@@ -1906,6 +1916,7 @@ pub static SQL_INSERT: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||
ast_builder: build_sql_insert,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1919,6 +1930,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
||||
ast_builder: build_sql_update,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
@@ -1934,6 +1946,7 @@ pub static SQL_DELETE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
||||
ast_builder: build_sql_delete,
|
||||
help_id: None,
|
||||
hint_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
|
||||
@@ -968,6 +968,7 @@ pub static DROP: CommandNode = CommandNode {
|
||||
shape: DROP_SHAPE,
|
||||
ast_builder: build_drop,
|
||||
help_id: Some("ddl.drop"),
|
||||
hint_id: None,
|
||||
usage_ids: &[
|
||||
"parse.usage.drop_table",
|
||||
"parse.usage.drop_column",
|
||||
@@ -981,6 +982,7 @@ pub static ADD: CommandNode = CommandNode {
|
||||
shape: ADD_SHAPE,
|
||||
ast_builder: build_add,
|
||||
help_id: Some("ddl.add"),
|
||||
hint_id: None,
|
||||
usage_ids: &[
|
||||
"parse.usage.add_column",
|
||||
"parse.usage.add_relationship",
|
||||
@@ -993,6 +995,7 @@ pub static RENAME: CommandNode = CommandNode {
|
||||
shape: RENAME_COLUMN,
|
||||
ast_builder: build_rename_column,
|
||||
help_id: Some("ddl.rename"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.rename_column"],};
|
||||
|
||||
pub static CHANGE: CommandNode = CommandNode {
|
||||
@@ -1000,6 +1003,7 @@ pub static CHANGE: CommandNode = CommandNode {
|
||||
shape: CHANGE_COLUMN,
|
||||
ast_builder: build_change_column,
|
||||
help_id: Some("ddl.change"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.change_column"],};
|
||||
|
||||
// =================================================================
|
||||
@@ -1360,6 +1364,7 @@ pub static CREATE: CommandNode = CommandNode {
|
||||
shape: CREATE_TABLE,
|
||||
ast_builder: build_create_table,
|
||||
help_id: Some("ddl.create"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.create_table"],};
|
||||
|
||||
// =================================================================
|
||||
@@ -1428,6 +1433,7 @@ pub static CREATE_M2N: CommandNode = CommandNode {
|
||||
shape: CREATE_M2N_SHAPE,
|
||||
ast_builder: build_create_m2n,
|
||||
help_id: Some("ddl.create_m2n"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.create_m2n"],
|
||||
};
|
||||
|
||||
@@ -1858,6 +1864,7 @@ pub static SQL_CREATE_TABLE: CommandNode = CommandNode {
|
||||
shape: Node::Subgrammar(&super::sql_create_table::SQL_CREATE_TABLE_SHAPE),
|
||||
ast_builder: build_sql_create_table,
|
||||
help_id: Some("ddl.sql_create_table"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.sql_create_table"],
|
||||
};
|
||||
|
||||
@@ -1877,6 +1884,7 @@ pub static SQL_DROP_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_TABLE_SHAPE,
|
||||
ast_builder: build_sql_drop_table,
|
||||
help_id: Some("ddl.sql_drop_table"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.sql_drop_table"],
|
||||
};
|
||||
|
||||
@@ -1896,6 +1904,7 @@ pub static SQL_DROP_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_DROP_INDEX_SHAPE,
|
||||
ast_builder: build_sql_drop_index,
|
||||
help_id: Some("ddl.sql_drop_index"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.sql_drop_index"],
|
||||
};
|
||||
|
||||
@@ -1977,6 +1986,7 @@ pub static SQL_CREATE_INDEX: CommandNode = CommandNode {
|
||||
shape: SQL_CREATE_INDEX_SHAPE,
|
||||
ast_builder: build_sql_create_index,
|
||||
help_id: Some("ddl.sql_create_index"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.sql_create_index"],
|
||||
};
|
||||
|
||||
@@ -2535,6 +2545,7 @@ pub static SQL_ALTER_TABLE: CommandNode = CommandNode {
|
||||
shape: SQL_ALTER_TABLE_SHAPE,
|
||||
ast_builder: build_sql_alter_table,
|
||||
help_id: Some("ddl.sql_alter_table"),
|
||||
hint_id: None,
|
||||
usage_ids: &["parse.usage.sql_alter_table"],
|
||||
};
|
||||
|
||||
|
||||
+68
-31
@@ -530,6 +530,15 @@ pub struct CommandNode {
|
||||
/// so a newly-registered command appears in `help`
|
||||
/// automatically (ADR-0024 §help_id).
|
||||
pub help_id: Option<&'static str>,
|
||||
/// Catalog key stem (`hint.cmd.<id>`) for this command form's
|
||||
/// **tier-3** contextual hint (ADR-0053 / H2). Unlike `help_id`
|
||||
/// — which is `None` on advanced-SQL forms purely to dedup the
|
||||
/// `help` list — `hint_id` is 1:1 with command *forms*, so each
|
||||
/// advanced-SQL form carries its own id and renders SQL-syntax
|
||||
/// content distinct from its simple-DSL sibling. `None` until a
|
||||
/// form's tier-3 block is authored (the surface falls back to
|
||||
/// tier-2 ambient/error text).
|
||||
pub hint_id: Option<&'static str>,
|
||||
/// Catalog keys under `parse.usage.*` to render in the
|
||||
/// "usage:" block when a parse error fires for this command
|
||||
/// (ADR-0021 §1, ADR-0024 §architecture). Multi-form families
|
||||
@@ -574,32 +583,69 @@ pub fn usage_keys_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Option<(&'static str, Vec<&'static str>)> {
|
||||
let pick = selected_nodes_for_input_in_mode(source, mode);
|
||||
if pick.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut keys: Vec<&'static str> = Vec::new();
|
||||
for (_, node, _) in &pick {
|
||||
for k in node.usage_ids {
|
||||
if !keys.contains(k) {
|
||||
keys.push(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entry = pick[0].1.entry.primary;
|
||||
Some((entry, keys))
|
||||
}
|
||||
|
||||
/// The tier-3 `hint_id` of the command form `source` is currently
|
||||
/// typing, in `mode` (H2 / ADR-0053).
|
||||
///
|
||||
/// Reuses the same mode-aware
|
||||
/// selection as [`usage_keys_for_input_in_mode`] and returns the
|
||||
/// **mode-primary** node's `hint_id` — so an advanced-SQL form
|
||||
/// resolves to its *own* id, not its simple-DSL sibling's. `None` if
|
||||
/// no entry word matches, or the chosen form has no tier-3 block yet
|
||||
/// (the caller then falls back to tier-2 ambient text).
|
||||
#[must_use]
|
||||
pub fn hint_id_for_input_in_mode(source: &str, mode: crate::mode::Mode) -> Option<&'static str> {
|
||||
selected_nodes_for_input_in_mode(source, mode)
|
||||
.first()
|
||||
.and_then(|(_, node, _)| node.hint_id)
|
||||
}
|
||||
|
||||
/// Shared mode-aware command-form selection for the entry word at the
|
||||
/// start of `source`.
|
||||
///
|
||||
/// Extracted so the usage-key and hint-id lookups agree on which form
|
||||
/// the user is typing.
|
||||
///
|
||||
/// Advanced mode: every candidate form is reachable — the SQL nodes
|
||||
/// are primary, and the DSL nodes remain valid via fallback (verified:
|
||||
/// `create table … with pk` and `drop column …` both run in advanced
|
||||
/// mode). Mode-primary (Advanced) first, so a hint never hides input
|
||||
/// that works. Simple mode: only the DSL forms — the SQL-only forms
|
||||
/// hit the "this is SQL" rail and are not reachable. (ADR-0042 G3.)
|
||||
/// Degenerate guard: an advanced-only word in simple mode leaves the
|
||||
/// selection empty; fall back to all candidates.
|
||||
fn selected_nodes_for_input_in_mode(
|
||||
source: &str,
|
||||
mode: crate::mode::Mode,
|
||||
) -> Vec<(usize, &'static CommandNode, CommandCategory)> {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let start = skip_whitespace(source, 0);
|
||||
let (kw_start, kw_end) = consume_ident(source, start)?;
|
||||
let Some((kw_start, kw_end)) = consume_ident(source, start) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let word = &source[kw_start..kw_end];
|
||||
let candidates = commands_for_entry_word(word);
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
return Vec::new();
|
||||
}
|
||||
let union = |nodes: &[(usize, &'static CommandNode, CommandCategory)]| -> Vec<&'static str> {
|
||||
let mut keys: Vec<&'static str> = Vec::new();
|
||||
for (_, node, _) in nodes {
|
||||
for k in node.usage_ids {
|
||||
if !keys.contains(k) {
|
||||
keys.push(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
keys
|
||||
};
|
||||
// Advanced mode: every candidate form is reachable — the SQL
|
||||
// nodes are primary, and the DSL nodes remain valid via fallback
|
||||
// (verified: `create table … with pk` and `drop column …` both
|
||||
// run in advanced mode). Show them all, mode-primary (Advanced)
|
||||
// first, so the usage hint never hides input that works. Simple
|
||||
// mode: only the DSL forms — the SQL-only forms hit the "this is
|
||||
// SQL" rail and are not reachable. (ADR-0042 G3.)
|
||||
let selected: Vec<(usize, &'static CommandNode, CommandCategory)> =
|
||||
if mode == crate::mode::Mode::Advanced {
|
||||
let mut v: Vec<_> = candidates
|
||||
@@ -621,17 +667,7 @@ pub fn usage_keys_for_input_in_mode(
|
||||
.filter(|(_, _, c)| *c == CommandCategory::Simple)
|
||||
.collect()
|
||||
};
|
||||
// Degenerate guard: an advanced-only word in simple mode (not
|
||||
// normally reachable — it hits the SQL rail first) leaves
|
||||
// `selected` empty; fall back to all candidates so a usage block
|
||||
// still renders rather than the available-commands fallback.
|
||||
let pick = if selected.is_empty() { candidates } else { selected };
|
||||
let keys = union(&pick);
|
||||
if keys.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entry = pick[0].1.entry.primary;
|
||||
Some((entry, keys))
|
||||
if selected.is_empty() { candidates } else { selected }
|
||||
}
|
||||
|
||||
/// The single usage template most relevant to `source`, when
|
||||
@@ -712,6 +748,7 @@ pub fn entry_words_alphabetised() -> Vec<&'static str> {
|
||||
pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
|
||||
(&app::QUIT, CommandCategory::Simple),
|
||||
(&app::HELP, CommandCategory::Simple),
|
||||
(&app::HINT, CommandCategory::Simple),
|
||||
(&app::REBUILD, CommandCategory::Simple),
|
||||
(&app::SAVE, CommandCategory::Simple),
|
||||
(&app::NEW, CommandCategory::Simple),
|
||||
|
||||
Reference in New Issue
Block a user