//! Hover information provider use tower_lsp::lsp_types::{ Hover, HoverContents, MarkupContent, MarkupKind, }; use crate::{ lsp::document::Document, resolve::names::DeclKind, syntax::{ ast::{ Declaration, Value, }, lexer::{ Lexer, Token, }, }, }; /// Get hover information at a position pub fn get_hover_info(text: &str, line: usize, character: usize) -> Option { // Calculate absolute byte offset from line/character position let mut byte_offset = 0; let mut found_line = false; for (current_line, line_text) in text.lines().enumerate() { if current_line == line { found_line = true; // Check if character position is beyond the line let line_char_count = line_text.chars().count(); if character >= line_char_count { return None; } // Add the character offset (assuming UTF-8) for (char_count, (byte_pos, _)) in line_text.char_indices().enumerate() { if char_count == character { byte_offset += byte_pos; break; } } break; } byte_offset += line_text.len() + 1; // +1 for newline } // If line was not found, return None if !found_line { return None; } // Tokenize and find the token at the cursor position let lexer = Lexer::new(text); let mut target_token = None; for (offset, token, end) in lexer { if offset <= byte_offset && byte_offset < end { target_token = Some(token); break; } } let token = target_token?; // Generate hover info based on the token let content = get_token_documentation(&token)?; Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: content.to_string(), }), range: None, }) } /// Get documentation for a token fn get_token_documentation(token: &Token) -> Option<&'static str> { match token { Token::Character => Some("**character** - Defines a character entity\n\nSyntax: `character Name { ... }`"), Token::Template => Some("**template** - Defines a reusable field template\n\nSyntax: `template Name { ... }`"), Token::LifeArc => Some("**life_arc** - Defines a state machine for character development\n\nSyntax: `life_arc Name { ... }`"), Token::Schedule => Some("**schedule** - Defines a daily schedule or routine\n\nSyntax: `schedule Name { ... }`"), Token::Behavior => Some("**behavior** - Defines a behavior tree for AI\n\nSyntax: `behavior Name { ... }`"), Token::Institution => Some("**institution** - Defines an organization or group\n\nSyntax: `institution Name { ... }`"), Token::Relationship => Some("**relationship** - Defines a multi-party relationship\n\nSyntax: `relationship Name { ... }`"), Token::Location => Some("**location** - Defines a place or setting\n\nSyntax: `location Name { ... }`"), Token::Species => Some("**species** - Defines a species with templates\n\nSyntax: `species Name { ... }`"), Token::Use => Some("**use** - Imports declarations from other files\n\nSyntax: `use path::to::item;`"), Token::From => Some("**from** - Applies templates to a character\n\nSyntax: `character Name from Template { ... }`"), Token::Include => Some("**include** - Includes another template\n\nSyntax: `include TemplateName`"), Token::State => Some("**state** - Defines a state in a life arc\n\nSyntax: `state name { ... }`"), Token::On => Some("**on** - Defines a transition or enter handler\n\nSyntax: `on condition -> target` or `on enter { ... }`"), Token::Strict => Some("**strict** - Enforces that a template only accepts defined fields"), _ => None, } } /// Get semantic hover information for symbols pub fn get_semantic_hover_info(doc: &Document, line: usize, character: usize) -> Option { let ast = doc.ast.as_ref()?; // Calculate absolute byte offset from line/character position let mut byte_offset = 0; let mut found_line = false; for (current_line, line_text) in doc.text.lines().enumerate() { if current_line == line { found_line = true; // Check if character position is beyond the line let line_char_count = line_text.chars().count(); if character >= line_char_count { return None; } for (char_count, (byte_pos, _)) in line_text.char_indices().enumerate() { if char_count == character { byte_offset += byte_pos; break; } } break; } byte_offset += line_text.len() + 1; // +1 for newline } if !found_line { return None; } // Tokenize and find the identifier at the cursor position let lexer = Lexer::new(&doc.text); let mut target_ident = None; for (offset, token, end) in lexer { if offset <= byte_offset && byte_offset < end { if let Token::Ident(name) = token { target_ident = Some(name); } break; } } let word = target_ident?; // Look up the symbol in the name table let symbol_info = doc.name_table.lookup(std::slice::from_ref(&word))?; // Find the declaration in the AST for decl in &ast.declarations { let decl_name = get_declaration_name(decl); if decl_name.as_deref() == Some(word.as_str()) { return Some(format_declaration_hover(decl, &symbol_info.kind)); } } None } /// Extract the name from a declaration fn get_declaration_name(decl: &Declaration) -> Option { match decl { | Declaration::Character(c) => Some(c.name.clone()), | Declaration::Template(t) => Some(t.name.clone()), | Declaration::Species(s) => Some(s.name.clone()), | Declaration::Location(l) => Some(l.name.clone()), | Declaration::Institution(i) => Some(i.name.clone()), | Declaration::Relationship(r) => Some(r.name.clone()), | Declaration::LifeArc(la) => Some(la.name.clone()), | Declaration::Schedule(s) => Some(s.name.clone()), | Declaration::Behavior(b) => Some(b.name.clone()), | Declaration::Use(_) => None, | Declaration::Concept(_) | Declaration::SubConcept(_) | Declaration::ConceptComparison(_) => None, // TODO: Implement hover for type system } } /// Format hover information for a declaration fn format_declaration_hover(decl: &Declaration, _kind: &DeclKind) -> Hover { let content = match decl { | Declaration::Character(c) => format_character_hover(c), | Declaration::Template(t) => format_template_hover(t), | Declaration::Species(s) => format_species_hover(s), | Declaration::Location(l) => format_location_hover(l), | Declaration::Institution(i) => format_institution_hover(i), | Declaration::Relationship(r) => format_relationship_hover(r), | Declaration::LifeArc(la) => format_life_arc_hover(la), | Declaration::Schedule(s) => format_schedule_hover(s), | Declaration::Behavior(b) => format_behavior_hover(b), | Declaration::Use(_) => "**use** declaration".to_string(), | Declaration::Concept(_) => "**concept** declaration".to_string(), | Declaration::SubConcept(_) => "**sub_concept** declaration".to_string(), | Declaration::ConceptComparison(_) => "**concept_comparison** declaration".to_string(), }; Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: content, }), range: None, } } /// Format character hover information fn format_character_hover(c: &crate::syntax::ast::Character) -> String { let mut content = format!("**character** `{}`\n\n", c.name); // Species if let Some(ref species) = c.species { content.push_str(&format!("**Species:** `{}`\n\n", species)); } // Templates if let Some(ref templates) = c.template { content.push_str(&format!( "**Templates:** {}\n\n", templates .iter() .map(|t| format!("`{}`", t)) .collect::>() .join(", ") )); } // Fields if !c.fields.is_empty() { content.push_str("**Fields:**\n"); for field in &c.fields { let value_preview = format_value_preview(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, value_preview)); } content.push('\n'); } // Prose blocks count let prose_count = c .fields .iter() .filter(|f| matches!(f.value, Value::ProseBlock(_))) .count(); if prose_count > 0 { content.push_str(&format!("*{} prose block(s)*\n", prose_count)); } content } /// Format template hover information fn format_template_hover(t: &crate::syntax::ast::Template) -> String { let mut content = format!("**template** `{}`\n\n", t.name); if t.strict { content.push_str("*strict mode*\n\n"); } // Includes if !t.includes.is_empty() { content.push_str(&format!( "**Includes:** {}\n\n", t.includes .iter() .map(|i| format!("`{}`", i)) .collect::>() .join(", ") )); } // Fields with types if !t.fields.is_empty() { content.push_str("**Fields:**\n"); for field in &t.fields { let type_name = format_value_as_type(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, type_name)); } content.push('\n'); } content } /// Format species hover information fn format_species_hover(s: &crate::syntax::ast::Species) -> String { let mut content = format!("**species** `{}`\n\n", s.name); // Includes if !s.includes.is_empty() { content.push_str(&format!( "**Includes:** {}\n\n", s.includes .iter() .map(|i| format!("`{}`", i)) .collect::>() .join(", ") )); } // Fields with types if !s.fields.is_empty() { content.push_str("**Fields:**\n"); for field in &s.fields { let type_name = format_value_as_type(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, type_name)); } content.push('\n'); } content } /// Format location hover information fn format_location_hover(l: &crate::syntax::ast::Location) -> String { let mut content = format!("**location** `{}`\n\n", l.name); if !l.fields.is_empty() { content.push_str("**Properties:**\n"); for field in &l.fields { let value_preview = format_value_preview(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, value_preview)); } content.push('\n'); } content } /// Format institution hover information fn format_institution_hover(i: &crate::syntax::ast::Institution) -> String { let mut content = format!("**institution** `{}`\n\n", i.name); if !i.fields.is_empty() { content.push_str("**Properties:**\n"); for field in &i.fields { let value_preview = format_value_preview(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, value_preview)); } content.push('\n'); } content } /// Format relationship hover information fn format_relationship_hover(r: &crate::syntax::ast::Relationship) -> String { let mut content = format!("**relationship** `{}`\n\n", r.name); // Participants if !r.participants.is_empty() { content.push_str(&format!( "**Participants:** {}\n\n", r.participants .iter() .map(|p| { let name = p.name.join("."); if let Some(ref role) = p.role { format!("`{}` as {}", name, role) } else { format!("`{}`", name) } }) .collect::>() .join(", ") )); } // Fields if !r.fields.is_empty() { content.push_str("**Fields:**\n"); for field in &r.fields { let value_preview = format_value_preview(&field.value); content.push_str(&format!("- `{}`: {}\n", field.name, value_preview)); } content.push('\n'); } content } /// Format life arc hover information fn format_life_arc_hover(la: &crate::syntax::ast::LifeArc) -> String { let mut content = format!("**life_arc** `{}`\n\n", la.name); if !la.states.is_empty() { content.push_str(&format!("**States:** {} states\n\n", la.states.len())); // Show first few states let preview_count = 5; for state in la.states.iter().take(preview_count) { content.push_str(&format!( "- `{}` ({} transitions)\n", state.name, state.transitions.len() )); } if la.states.len() > preview_count { content.push_str(&format!( "- *... and {} more*\n", la.states.len() - preview_count )); } content.push('\n'); } content } /// Format schedule hover information fn format_schedule_hover(s: &crate::syntax::ast::Schedule) -> String { let mut content = format!("**schedule** `{}`\n\n", s.name); if !s.blocks.is_empty() { content.push_str(&format!("**Time Blocks:** {} entries\n\n", s.blocks.len())); // Show first few blocks let preview_count = 5; for block in s.blocks.iter().take(preview_count) { let start_str = format_time(&block.start); let end_str = format_time(&block.end); content.push_str(&format!( "- {} - {}: {}\n", start_str, end_str, block.activity )); } if s.blocks.len() > preview_count { content.push_str(&format!( "- *... and {} more*\n", s.blocks.len() - preview_count )); } content.push('\n'); } content } /// Format behavior hover information fn format_behavior_hover(b: &crate::syntax::ast::Behavior) -> String { let mut content = format!("**behavior** `{}`\n\n", b.name); content.push_str("**Behavior Tree:**\n"); content.push_str(&format_behavior_node_preview(&b.root, 0)); content.push('\n'); content } /// Format a behavior tree node preview (recursively, up to depth 2) fn format_behavior_node_preview(node: &crate::syntax::ast::BehaviorNode, depth: usize) -> String { if depth > 2 { return format!("{} *...*\n", " ".repeat(depth)); } let indent = " ".repeat(depth); let mut content = String::new(); match node { | crate::syntax::ast::BehaviorNode::Action(name, params) => { content.push_str(&format!("{}- Action: `{}`", indent, name)); if !params.is_empty() { content.push_str(&format!(" ({} params)", params.len())); } content.push('\n'); }, | crate::syntax::ast::BehaviorNode::Sequence { children, .. } => { content.push_str(&format!( "{}- Sequence ({} children)\n", indent, children.len() )); for child in children.iter().take(3) { content.push_str(&format_behavior_node_preview(child, depth + 1)); } if children.len() > 3 { content.push_str(&format!( "{} *... and {} more*\n", indent, children.len() - 3 )); } }, | crate::syntax::ast::BehaviorNode::Selector { children, .. } => { content.push_str(&format!( "{}- Selector ({} children)\n", indent, children.len() )); for child in children.iter().take(3) { content.push_str(&format_behavior_node_preview(child, depth + 1)); } if children.len() > 3 { content.push_str(&format!( "{} *... and {} more*\n", indent, children.len() - 3 )); } }, | crate::syntax::ast::BehaviorNode::Condition(_) => { content.push_str(&format!("{}- Condition\n", indent)); }, | crate::syntax::ast::BehaviorNode::Decorator { decorator_type, child, .. } => { content.push_str(&format!("{}- Decorator: `{:?}`\n", indent, decorator_type)); content.push_str(&format_behavior_node_preview(child, depth + 1)); }, | crate::syntax::ast::BehaviorNode::SubTree(name) => { content.push_str(&format!("{}- SubTree: `{}`\n", indent, name.join("."))); }, } content } /// Format a value as a type name (for template/species fields) fn format_value_as_type(value: &Value) -> String { match value { | Value::Identifier(path) => path.join("."), | Value::Text(_) => "Text".to_string(), | Value::Number(_) => "Number".to_string(), | Value::Decimal(_) => "Decimal".to_string(), | Value::Boolean(_) => "Boolean".to_string(), | Value::List(items) => { if items.is_empty() { "List".to_string() } else { format!("[{}]", format_value_as_type(&items[0])) } }, | Value::Object(_) => "Object".to_string(), | Value::Range(start, end) => { format!( "{}..{}", format_value_as_type(start), format_value_as_type(end) ) }, | Value::Time(_) => "Time".to_string(), | Value::Duration(_) => "Duration".to_string(), | Value::ProseBlock(_) => "ProseBlock".to_string(), | Value::Override(_) => "Override".to_string(), | Value::Any => "Any".to_string(), } } /// Format a value preview (for character/location fields) fn format_value_preview(value: &Value) -> String { match value { | Value::Identifier(path) => format!("`{}`", path.join(".")), | Value::Text(s) => format!("\"{}\"", truncate(s, 50)), | Value::Number(n) => n.to_string(), | Value::Decimal(f) => f.to_string(), | Value::Boolean(b) => b.to_string(), | Value::List(items) => { if items.is_empty() { "[]".to_string() } else { format!("[{} items]", items.len()) } }, | Value::Object(fields) => format!("{{{} fields}}", fields.len()), | Value::Range(start, end) => { format!( "{}..{}", format_value_preview(start), format_value_preview(end) ) }, | Value::Time(time) => format_time(time), | Value::Duration(duration) => format_duration(duration), | Value::ProseBlock(prose) => format!("*prose ({} chars)*", prose.content.len()), | Value::Override(override_val) => format!("*{} overrides*", override_val.overrides.len()), | Value::Any => "any".to_string(), } } /// Format a time value fn format_time(time: &crate::syntax::ast::Time) -> String { if time.second == 0 { format!("{:02}:{:02}", time.hour, time.minute) } else { format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second) } } /// Format a duration value fn format_duration(duration: &crate::syntax::ast::Duration) -> String { let mut parts = Vec::new(); if duration.hours > 0 { parts.push(format!("{}h", duration.hours)); } if duration.minutes > 0 { parts.push(format!("{}m", duration.minutes)); } if duration.seconds > 0 { parts.push(format!("{}s", duration.seconds)); } if parts.is_empty() { "0s".to_string() } else { parts.join(" ") } } /// Truncate a string to a maximum length fn truncate(s: &str, max_len: usize) -> String { if s.len() <= max_len { s.to_string() } else { format!("{}...", &s[..max_len - 3]) } }