use std::collections::HashMap;
use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind, Position, Uri};
use crate::fmt::{format_equation, format_expression};
use crate::ir::ast::{
Causality, ClassDefinition, ClassType, Component, Expression, Import, StoredDefinition,
Variability,
};
use crate::ir::transform::constants::get_builtin_functions;
use crate::ir::transform::scope_resolver::{ResolvedSymbol, ScopeResolver, find_class_in_ast};
use crate::lsp::data::keywords::get_keyword_hover;
use crate::lsp::utils::{get_qualified_name_at_position, get_word_at_position, parse_document};
use crate::lsp::workspace::WorkspaceState;
pub fn handle_hover(documents: &HashMap<Uri, String>, params: HoverParams) -> Option<Hover> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let text = documents.get(uri)?;
let path = uri.path().as_str();
let word = get_word_at_position(text, position)?;
if let Some(ast) = parse_document(text, path)
&& let Some(hover_text) = get_ast_hover_info(&ast, &word, position)
{
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
});
}
for func in get_builtin_functions() {
if func.name == word {
let params_doc: String = func
.parameters
.iter()
.map(|(name, doc)| format!("- `{}`: {}", name, doc))
.collect::<Vec<_>>()
.join("\n");
let hover_text = format!(
"```modelica\n{}\n```\n\n{}\n\n**Parameters:**\n{}",
func.signature, func.documentation, params_doc
);
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
});
}
}
let hover_text = get_keyword_hover(&word)?;
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
})
}
pub fn handle_hover_workspace(
workspace: &mut WorkspaceState,
params: HoverParams,
) -> Option<Hover> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let text = workspace.get_document(uri)?;
let path = uri.path().as_str();
let word = get_word_at_position(text, position)?;
let qualified_name = get_qualified_name_at_position(text, position);
if let Some(ast) = parse_document(text, path) {
for class in ast.class_list.values() {
if let Some(import_path) = find_import_path_for_name(class, &word) {
workspace.ensure_package_indexed(&import_path);
}
}
let resolver = ScopeResolver::with_lookup(&ast, workspace as &WorkspaceState);
let mut resolved = None;
if let Some(ref qn) = qualified_name {
resolved = resolver.resolve_qualified(qn, position.line + 1, position.character + 1);
}
if resolved.is_none() {
resolved = resolver.resolve(&word, position.line + 1, position.character + 1);
}
if let Some(ref resolved) = resolved
&& let Some(hover_text) = format_resolved_symbol_unified(resolved, workspace, &ast)
{
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
});
}
}
for func in get_builtin_functions() {
if func.name == word {
let params_doc: String = func
.parameters
.iter()
.map(|(name, doc)| format!("- `{}`: {}", name, doc))
.collect::<Vec<_>>()
.join("\n");
let hover_text = format!(
"```modelica\n{}\n```\n\n{}\n\n**Parameters:**\n{}",
func.signature, func.documentation, params_doc
);
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
});
}
}
let hover_text = get_keyword_hover(&word)?;
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: None,
})
}
fn format_resolved_symbol_unified(
resolved: &ResolvedSymbol,
workspace: &WorkspaceState,
ast: &StoredDefinition,
) -> Option<String> {
match resolved {
ResolvedSymbol::Component {
component,
defined_in,
inherited_via,
} => {
let type_name = component.type_name.to_string();
let type_class = workspace
.get_parsed_ast_by_name(&type_name)
.and_then(|ast| ast.class_list.values().next());
let mut info = format_component_hover_with_class(component, type_class);
if let Some(base_class_name) = inherited_via {
info += &format!("\n\n*Inherited from `{}`*", base_class_name);
} else {
info += &format!("\n\n*Defined in `{}`*", defined_in.name.text);
}
Some(info)
}
ResolvedSymbol::Class(class_def) => {
let class_name = &class_def.name.text;
if let Ok(flattened) = crate::ir::transform::flatten::flatten(ast, Some(class_name)) {
Some(format_class_hover(&flattened, class_name))
} else {
Some(format_class_hover(class_def, class_name))
}
}
ResolvedSymbol::External(sym_info) => Some(format_symbol_info_hover(sym_info, workspace)),
}
}
fn format_symbol_info_hover(
sym_info: &crate::ir::transform::scope_resolver::ExternalSymbol,
workspace: &WorkspaceState,
) -> String {
let parts: Vec<&str> = sym_info.qualified_name.split('.').collect();
if !parts.is_empty() {
let lib_name = parts[0];
if let Some(lib_ast) = workspace.get_library(lib_name) {
if let Some(class) = navigate_class_path(&lib_ast, lib_name, &parts[1..]) {
return format_class_hover(class, &sym_info.qualified_name);
}
}
}
if let Some(ast) = workspace.get_parsed_ast_by_name(&sym_info.qualified_name) {
let simple_name = sym_info
.qualified_name
.rsplit('.')
.next()
.unwrap_or(&sym_info.qualified_name);
if let Some(class) = ast.class_list.get(simple_name) {
return format_class_hover(class, &sym_info.qualified_name);
}
}
let kind = format!("{:?}", sym_info.kind);
let mut info = format!("**{}** ({})\n\n", sym_info.qualified_name, kind);
if let Some(detail) = &sym_info.detail {
info += detail;
}
info
}
fn navigate_class_path<'a>(
ast: &'a StoredDefinition,
lib_name: &str,
path: &[&str],
) -> Option<&'a ClassDefinition> {
let mut current = ast.class_list.get(lib_name)?;
if path.is_empty() {
return Some(current);
}
for &part in path {
current = current.classes.get(part)?;
}
Some(current)
}
fn get_ast_hover_info(ast: &StoredDefinition, word: &str, position: Position) -> Option<String> {
let resolver = ScopeResolver::new(ast);
if let Some(symbol) = resolver.resolve_0indexed(word, position.line, position.character) {
match symbol {
ResolvedSymbol::Component {
component,
defined_in,
inherited_via,
} => {
let type_class = find_class_in_ast(ast, &component.type_name.to_string());
let mut info = format_component_hover_with_class(component, type_class);
if let Some(base_class_name) = inherited_via {
info += &format!("\n\n*Inherited from `{}`*", base_class_name);
} else {
info += &format!("\n\n*Defined in `{}`*", defined_in.name.text);
}
return Some(info);
}
ResolvedSymbol::Class(class_def) => {
let class_name = &class_def.name.text;
if let Ok(flattened) = crate::ir::transform::flatten::flatten(ast, Some(class_name))
{
return Some(format_class_hover(&flattened, class_name));
}
return Some(format_class_hover(class_def, word));
}
ResolvedSymbol::External(sym_info) => {
let kind = format!("{:?}", sym_info.kind);
let mut info = format!("**{}** ({})\n\n", sym_info.qualified_name, kind);
if let Some(detail) = &sym_info.detail {
info += detail;
}
return Some(info);
}
}
}
if let Some(class_def) = ast.class_list.get(word) {
if let Ok(flattened) = crate::ir::transform::flatten::flatten(ast, Some(word)) {
return Some(format_class_hover(&flattened, word));
}
return Some(format_class_hover(class_def, word));
}
for class in ast.class_list.values() {
if let Some(nested) = class.classes.get(word) {
let qualified_name = format!("{}.{}", class.name.text, word);
if let Ok(flattened) =
crate::ir::transform::flatten::flatten(ast, Some(&qualified_name))
{
return Some(format_class_hover(&flattened, word));
}
return Some(format_class_hover(nested, word));
}
}
None
}
fn format_expr(expr: &Expression) -> String {
format_expression(expr)
}
fn format_component_hover_with_class(
comp: &Component,
type_class: Option<&crate::ir::ast::ClassDefinition>,
) -> String {
let mut type_sig = comp.type_name.to_string();
if !comp.shape.is_empty() {
type_sig += &format!(
"[{}]",
comp.shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
let mut qualifiers = Vec::new();
match &comp.variability {
Variability::Parameter(_) => qualifiers.push("parameter"),
Variability::Constant(_) => qualifiers.push("constant"),
Variability::Discrete(_) => qualifiers.push("discrete"),
_ => {}
}
match &comp.causality {
Causality::Input(_) => qualifiers.push("input"),
Causality::Output(_) => qualifiers.push("output"),
_ => {}
}
let qualifier_str = if qualifiers.is_empty() {
String::new()
} else {
format!("{} ", qualifiers.join(" "))
};
let mut info = format!(
"```modelica\n{}{} {}\n```",
qualifier_str, type_sig, comp.name
);
if !comp.description.is_empty() {
let desc = comp
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ");
info += &format!("\n\n*{}*", desc);
}
if let Some(class_def) = type_class {
let class_type_str = format!("{:?}", class_def.class_type).to_lowercase();
info += &format!("\n\n**Type:** `{}`", class_type_str);
if !class_def.description.is_empty() {
let class_desc = class_def
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ");
info += &format!(" - {}", class_desc);
}
if !class_def.components.is_empty() {
info += "\n\n**Class Attributes:**\n| Name | Type | Description |\n|------|------|-------------|\n";
for (attr_name, attr_comp) in &class_def.components {
let mut attr_type = attr_comp.type_name.to_string();
if !attr_comp.shape.is_empty() {
attr_type += &format!(
"[{}]",
attr_comp
.shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
let mut attr_qualifiers = Vec::new();
match &attr_comp.variability {
Variability::Parameter(_) => attr_qualifiers.push("parameter"),
Variability::Constant(_) => attr_qualifiers.push("constant"),
_ => {}
}
match &attr_comp.causality {
Causality::Input(_) => attr_qualifiers.push("input"),
Causality::Output(_) => attr_qualifiers.push("output"),
_ => {}
}
if !attr_qualifiers.is_empty() {
attr_type = format!("{} {}", attr_qualifiers.join(" "), attr_type);
}
let attr_desc = if !attr_comp.description.is_empty() {
attr_comp
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ")
} else {
String::new()
};
info += &format!("| {} | `{}` | {} |\n", attr_name, attr_type, attr_desc);
}
}
let functions: Vec<_> = class_def
.classes
.iter()
.filter(|(_, c)| c.class_type == ClassType::Function)
.collect();
if !functions.is_empty() {
info += "\n**Class Functions:**\n";
for (func_name, func_def) in functions {
let inputs: Vec<_> = func_def
.components
.iter()
.filter(|(_, c)| matches!(c.causality, Causality::Input(_)))
.map(|(n, c)| format!("{}: {}", n, c.type_name))
.collect();
let outputs: Vec<_> = func_def
.components
.iter()
.filter(|(_, c)| matches!(c.causality, Causality::Output(_)))
.map(|(n, c)| format!("{}: {}", n, c.type_name))
.collect();
let sig = if outputs.is_empty() {
format!("{}({})", func_name, inputs.join(", "))
} else {
format!(
"{}({}) -> ({})",
func_name,
inputs.join(", "),
outputs.join(", ")
)
};
info += &format!("- `{}`\n", sig);
}
}
if !class_def.equations.is_empty() {
info += "\n**Equations:**\n```modelica\n";
for eq in &class_def.equations {
info += &format_equation(eq);
}
info += "```\n";
}
if !class_def.initial_equations.is_empty() {
info += "\n**Initial Equations:**\n```modelica\n";
for eq in &class_def.initial_equations {
info += &format_equation(eq);
}
info += "```\n";
}
}
let mut attrs = Vec::new();
if !comp.shape.is_empty() {
let shape_str = format!(
"[{}]",
comp.shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
);
attrs.push(("shape", shape_str));
}
if comp.start != Expression::Empty {
attrs.push(("start", format_expr(&comp.start)));
}
let important_mods = [
"unit",
"displayUnit",
"min",
"max",
"nominal",
"fixed",
"stateSelect",
];
for mod_name in important_mods {
if let Some(expr) = comp.modifications.get(mod_name) {
attrs.push((mod_name, format_expr(expr)));
}
}
for (mod_name, expr) in &comp.modifications {
if !important_mods.contains(&mod_name.as_str()) {
attrs.push((mod_name.as_str(), format_expr(expr)));
}
}
if !attrs.is_empty() {
info += "\n\n**Instance Modifications:**\n| Attribute | Value |\n|-----------|-------|\n";
for (name, value) in attrs {
info += &format!("| {} | `{}` |\n", name, value);
}
}
info
}
fn extract_documentation_info(annotation: &[Expression]) -> Option<String> {
for expr in annotation {
if let Expression::FunctionCall { comp, args } = expr {
if comp.to_string() == "Documentation" {
for arg in args {
if let Expression::Binary { op, lhs, rhs } = arg {
if matches!(op, crate::ir::ast::OpBinary::Eq(_))
&& let Expression::ComponentReference(comp_ref) = lhs.as_ref()
&& comp_ref.to_string() == "info"
&& let Expression::Terminal { token, .. } = rhs.as_ref()
{
let html = token.text.trim_matches('"');
return Some(html.to_string());
}
}
}
}
}
}
None
}
fn html_to_markdown(html: &str) -> String {
let mut result = html.to_string();
result = result.replace("<html>", "").replace("</html>", "");
let mut processed = String::new();
let mut remaining = result.as_str();
while let Some(pre_start) = remaining.find("<pre>") {
processed.push_str(&remaining[..pre_start]);
let after_pre_tag = &remaining[pre_start + 5..];
if let Some(pre_end) = after_pre_tag.find("</pre>") {
let pre_content = &after_pre_tag[..pre_end];
let pre_content = pre_content.trim_start_matches('\n').trim_end_matches('\n');
processed.push_str("\n```\n");
processed.push_str(pre_content);
processed.push_str("\n```\n");
remaining = &after_pre_tag[pre_end + 6..];
} else {
processed.push_str(remaining);
remaining = "";
}
}
processed.push_str(remaining);
result = processed;
result = result
.replace("<blockquote>", "")
.replace("</blockquote>", "");
result = result.replace("<p>", "\n\n").replace("</p>", "");
result = result
.replace("<br>", "\n")
.replace("<br/>", "\n")
.replace("<br />", "\n");
result = result.replace("<b>", "**").replace("</b>", "**");
result = result.replace("<strong>", "**").replace("</strong>", "**");
result = result.replace("<i>", "*").replace("</i>", "*");
result = result.replace("<em>", "*").replace("</em>", "*");
result = result.replace("<code>", "`").replace("</code>", "`");
result = result.replace("<h1>", "\n# ").replace("</h1>", "\n");
result = result.replace("<h2>", "\n## ").replace("</h2>", "\n");
result = result.replace("<h3>", "\n### ").replace("</h3>", "\n");
result = result.replace("<h4>", "\n#### ").replace("</h4>", "\n");
result = result.replace("<ul>", "\n").replace("</ul>", "\n");
result = result.replace("<ol>", "\n").replace("</ol>", "\n");
result = result.replace("<li>", "- ").replace("</li>", "\n");
result = result.trim().to_string();
let mut clean = String::new();
let mut in_tag = false;
for ch in result.chars() {
if ch == '<' {
in_tag = true;
} else if ch == '>' {
in_tag = false;
} else if !in_tag {
clean.push(ch);
}
}
let mut final_result = String::new();
let mut prev_newline_count = 0;
for ch in clean.chars() {
if ch == '\n' {
prev_newline_count += 1;
if prev_newline_count <= 2 {
final_result.push(ch);
}
} else {
prev_newline_count = 0;
final_result.push(ch);
}
}
final_result.trim().to_string()
}
fn format_class_hover(class_def: &crate::ir::ast::ClassDefinition, name: &str) -> String {
let class_type_str = format!("{:?}", class_def.class_type).to_lowercase();
let mut info = format!("```modelica\n{} {}\n```", class_type_str, name);
if !class_def.description.is_empty() {
let desc = class_def
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ");
info += &format!("\n\n*{}*", desc);
}
if let Some(doc_html) = extract_documentation_info(&class_def.annotation) {
let doc_md = html_to_markdown(&doc_html);
if !doc_md.is_empty() {
info += &format!("\n\n---\n\n{}", doc_md);
}
}
if class_def.class_type == ClassType::Function {
let mut inputs = Vec::new();
let mut outputs = Vec::new();
for (comp_name, comp) in &class_def.components {
match &comp.causality {
Causality::Input(_) => {
inputs.push(format!("{}: {}", comp_name, comp.type_name));
}
Causality::Output(_) => {
outputs.push(format!("{}: {}", comp_name, comp.type_name));
}
_ => {}
}
}
info += &format!(
"\n\n**Signature:**\n```modelica\n{}({}) -> ({})\n```",
name,
inputs.join(", "),
outputs.join(", ")
);
}
let functions: Vec<_> = class_def
.classes
.iter()
.filter(|(_, c)| c.class_type == ClassType::Function)
.collect();
if !functions.is_empty() {
info += "\n\n**Functions:**\n";
for (func_name, func_def) in functions {
let inputs: Vec<_> = func_def
.components
.iter()
.filter(|(_, c)| matches!(c.causality, Causality::Input(_)))
.map(|(n, c)| format!("{}: {}", n, c.type_name))
.collect();
let outputs: Vec<_> = func_def
.components
.iter()
.filter(|(_, c)| matches!(c.causality, Causality::Output(_)))
.map(|(n, c)| format!("{}: {}", n, c.type_name))
.collect();
let sig = if outputs.is_empty() {
format!("{}({})", func_name, inputs.join(", "))
} else {
format!(
"{}({}) -> ({})",
func_name,
inputs.join(", "),
outputs.join(", ")
)
};
let desc = if !func_def.description.is_empty() {
let d = func_def
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ");
format!(" - {}", d)
} else {
String::new()
};
info += &format!("- `{}`{}\n", sig, desc);
}
}
if class_def.class_type != ClassType::Function && !class_def.components.is_empty() {
info +=
"\n\n**Attributes:**\n| Name | Type | Description |\n|------|------|-------------|\n";
for (comp_name, comp) in &class_def.components {
let mut type_str = comp.type_name.to_string();
if !comp.shape.is_empty() {
type_str += &format!(
"[{}]",
comp.shape
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
let mut qualifiers = Vec::new();
match &comp.variability {
Variability::Parameter(_) => qualifiers.push("parameter"),
Variability::Constant(_) => qualifiers.push("constant"),
_ => {}
}
match &comp.causality {
Causality::Input(_) => qualifiers.push("input"),
Causality::Output(_) => qualifiers.push("output"),
_ => {}
}
if !qualifiers.is_empty() {
type_str = format!("{} {}", qualifiers.join(" "), type_str);
}
let desc = if !comp.description.is_empty() {
comp.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ")
} else {
String::new()
};
info += &format!("| {} | `{}` | {} |\n", comp_name, type_str, desc);
}
}
let nested_classes: Vec<_> = class_def
.classes
.iter()
.filter(|(_, c)| c.class_type != ClassType::Function)
.collect();
if !nested_classes.is_empty() {
info += "\n\n**Nested Types:**\n";
for (nested_name, nested_def) in nested_classes {
let nested_type = format!("{:?}", nested_def.class_type).to_lowercase();
let desc = if !nested_def.description.is_empty() {
let d = nested_def
.description
.iter()
.map(|t| t.text.trim_matches('"').to_string())
.collect::<Vec<_>>()
.join(" ");
format!(" - {}", d)
} else {
String::new()
};
info += &format!("- `{} {}`{}\n", nested_type, nested_name, desc);
}
}
if !class_def.equations.is_empty() {
info += "\n\n**Equations:**\n```modelica\n";
for eq in &class_def.equations {
info += &format_equation(eq);
}
info += "```";
}
if !class_def.initial_equations.is_empty() {
info += "\n\n**Initial Equations:**\n```modelica\n";
for eq in &class_def.initial_equations {
info += &format_equation(eq);
}
info += "```";
}
info
}
fn find_import_path_for_name(class: &ClassDefinition, name: &str) -> Option<String> {
for import in &class.imports {
match import {
Import::Renamed { alias, path, .. } => {
if alias.text == name {
return Some(path.to_string());
}
}
Import::Qualified { path, .. } => {
if let Some(last) = path.name.last()
&& last.text == name
{
return Some(path.to_string());
}
}
Import::Unqualified { path, .. } => {
return Some(format!("{}.{}", path, name));
}
Import::Selective { path, names, .. } => {
for n in names {
if n.text == name {
return Some(format!("{}.{}", path, name));
}
}
}
}
}
for nested in class.classes.values() {
if let Some(path) = find_import_path_for_name(nested, name) {
return Some(path);
}
}
None
}