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.
623 lines
20 KiB
Rust
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])
|
|
}
|
|
}
|