//! Hover information — identify the token under the cursor and return type docs. use serde::{Deserialize, Serialize}; use el_types::{TypeDef, TypeEnv}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HoverInfo { pub type_name: String, pub documentation: String, pub engram_node_type: Option, } /// Return hover info for the token at `cursor_pos`. pub fn hover_at(env: &TypeEnv, source: &str, cursor_pos: usize) -> Option { let token = extract_token(source, cursor_pos); if token.is_empty() { return None; } // Check built-in primitives let primitive_doc = match token.as_str() { "Int" => Some(("Int", "64-bit signed integer")), "Float" => Some(("Float", "64-bit IEEE 754 double")), "String" => Some(("String", "UTF-8 string")), "Bool" => Some(("Bool", "Boolean — true or false")), "Uuid" => Some(("Uuid", "RFC 4122 UUID")), "Void" => Some(("Void", "Unit type — no value")), _ => None, }; if let Some((name, doc)) = primitive_doc { return Some(HoverInfo { type_name: name.to_string(), documentation: doc.to_string(), engram_node_type: None, }); } // Check user-defined types if let Some(def) = env.types.get(&token) { let engram_node_type = env.engram_mappings.get(&token).cloned(); let doc = format_typedef_doc(&token, def); return Some(HoverInfo { type_name: token.clone(), documentation: doc, engram_node_type, }); } // Check functions if let Some(fn_type) = env.functions.get(&token) { return Some(HoverInfo { type_name: token.clone(), documentation: format!("fn {token} :: {fn_type}"), engram_node_type: None, }); } // Check keywords let kw_doc = match token.as_str() { "activate" => Some("activate TypeName where \"query\"\nReturns [TypeName] via spreading activation over the Engram knowledge graph."), "sealed" => Some("sealed { ... }\nMarks the block as sensitive — values are redacted from debuggers in debug builds."), "let" => Some("let name: Type = expr\nDeclare an immutable binding."), "fn" => Some("fn name(params) -> ReturnType { body }\nDefine a function."), "type" => Some("type Name { field: Type }\nDefine a struct type."), "enum" => Some("enum Name { Variant1, Variant2(Type) }\nDefine an enum."), "match" => Some("match expr { Pattern => result }\nPattern match an expression."), "return" => Some("return expr\nReturn a value from the enclosing function."), _ => None, }; if let Some(doc) = kw_doc { return Some(HoverInfo { type_name: token.clone(), documentation: doc.to_string(), engram_node_type: None, }); } None } // ── Helpers ─────────────────────────────────────────────────────────────────── fn extract_token(source: &str, cursor_pos: usize) -> String { let len = source.len(); if cursor_pos > len { return String::new(); } let bytes = source.as_bytes(); // Expand left let mut start = cursor_pos; while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') { start -= 1; } // Expand right let mut end = cursor_pos; while end < len && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') { end += 1; } source[start..end].to_string() } fn format_typedef_doc(name: &str, def: &TypeDef) -> String { match def { TypeDef::Struct { fields, .. } => { let fields_str = fields .iter() .map(|(f, t)| format!(" {f}: {t}")) .collect::>() .join("\n"); format!("type {name} {{\n{fields_str}\n}}") } TypeDef::Enum { variants, .. } => { let variants_str = variants .iter() .map(|v| { if let Some(payload) = &v.payload { format!(" {}({})", v.name, payload) } else { format!(" {}", v.name) } }) .collect::>() .join("\n"); format!("enum {name} {{\n{variants_str}\n}}") } TypeDef::Primitive(t) => format!("primitive type {name} = {t}"), } }