Files
storybook/src/lsp/hover.rs
Sienna Meridian Satterwhite 8e4bdd3942 refactor(ast): rename value types to Number/Decimal/Text/Boolean
Renamed AST value types for v0.3 naming convention:
- Value::Int -> Value::Number
- Value::Float -> Value::Decimal
- Value::String -> Value::Text
- Value::Bool -> Value::Boolean
- Expr::IntLit -> Expr::NumberLit
- Expr::FloatLit -> Expr::DecimalLit
- Expr::StringLit -> Expr::TextLit
- Expr::BoolLit -> Expr::BooleanLit

Updated all references across parser, resolver, validator, LSP,
query engine, tests, and editor.
2026-02-14 14:03:21 +00:00

623 lines
20 KiB
Rust

//! 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<Hover> {
// 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<Hover> {
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<String> {
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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])
}
}