use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use lsp_types::Hover;
use lsp_types::HoverContents;
use lsp_types::MarkupContent;
use lsp_types::MarkupKind;
use rowan::Direction;
use tracing::debug;
use url::Url;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Comment;
use wdl_ast::CommentKind;
use wdl_ast::Documented;
use wdl_ast::Ident;
use wdl_ast::SyntaxKind;
use wdl_ast::SyntaxNode;
use wdl_ast::SyntaxToken;
use wdl_ast::TreeNode;
use wdl_ast::TreeToken;
use wdl_ast::v1::AccessExpr;
use wdl_ast::v1::CallExpr;
use wdl_ast::v1::CallTarget;
use wdl_ast::v1::Decl;
use wdl_ast::v1::EnumVariant;
use wdl_ast::v1::LiteralStruct;
use wdl_ast::v1::LiteralStructItem;
use wdl_ast::v1::MetadataObject;
use wdl_ast::v1::MetadataValue;
use wdl_ast::v1::ParameterMetadataSection;
use wdl_ast::v1::StructDefinition;
use wdl_grammar::SyntaxElement;
use crate::Document;
use crate::SourcePosition;
use crate::SourcePositionEncoding;
use crate::graph::DocumentGraph;
use crate::graph::ParseState;
use crate::handlers::TypeEvalContext;
use crate::handlers::common::comments_to_string;
use crate::handlers::common::location_from_span;
use crate::handlers::common::position_to_offset;
use crate::handlers::common::provide_enum_documentation;
use crate::handlers::common::provide_struct_documentation;
use crate::handlers::common::provide_task_documentation;
use crate::handlers::common::provide_workflow_documentation;
use crate::stdlib::Function;
use crate::stdlib::STDLIB;
use crate::stdlib::TypeParameters;
use crate::types::CompoundType;
use crate::types::CustomType;
use crate::types::Type;
use crate::types::v1::ExprTypeEvaluator;
pub fn hover(
graph: &DocumentGraph,
document_uri: &Url,
position: SourcePosition,
encoding: SourcePositionEncoding,
) -> Result<Option<Hover>> {
let Some(index) = graph.get_index(document_uri) else {
bail!("document `{document_uri}` not found in graph")
};
let node = graph.get(index);
let (root, lines) = match node.parse_state() {
ParseState::Parsed { lines, root, .. } => {
(SyntaxNode::new_root(root.clone()), lines.clone())
}
_ => bail!("document `{uri}` has not been parsed", uri = document_uri),
};
let Some(document) = node.document() else {
bail!("document analysis data not available for {}", document_uri);
};
let offset = position_to_offset(&lines, position, encoding)?;
let hovered_token = root
.token_at_offset(offset)
.find(|t| t.kind() == SyntaxKind::Ident || t.kind() == SyntaxKind::Comment)
.ok_or_else(|| anyhow!("no hoverable token found at offset"))?;
let parent_node = hovered_token.parent().expect("token has no parent");
if let Ok(Some(value)) = resolve_hover_content(&parent_node, &hovered_token, document, graph) {
let range = location_from_span(document_uri, hovered_token.span(), &lines)?.range;
Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value,
}),
range: Some(range),
}))
} else {
Ok(None)
}
}
fn resolve_hover_content(
parent_node: &SyntaxNode,
token: &SyntaxToken,
document: &Document,
graph: &DocumentGraph,
) -> Result<Option<String>> {
if let Some(content) = resolve_hover_by_context(parent_node, token, document, graph)? {
return Ok(Some(content));
}
if let Some(scope) = document.find_scope_by_position(token.span().start())
&& let Some(name) = scope.lookup(token.text())
{
let (kind, documentation) = match name.ty() {
Type::Call(_) => ("call", None),
_ => {
let doc = find_parameter_meta_documentation(token);
("variable", doc)
}
};
let mut content = format!("```wdl\n({kind}) {}: {}\n```", token.text(), name.ty());
if let Some(doc) = documentation {
content.push_str("\n---\n");
content.push_str(&doc);
}
return Ok(Some(content));
}
if let Some(content) = find_global_hover_in_doc(document, token)? {
return Ok(Some(content));
}
for (_, ns) in document.namespaces() {
let node = graph.get(graph.get_index(ns.source()).unwrap());
let Some(imported_doc) = node.document() else {
continue;
};
if let Some(content) = find_global_hover_in_doc(imported_doc, token)? {
return Ok(Some(content));
}
}
Ok(None)
}
fn resolve_hover_by_context(
parent_node: &SyntaxNode,
token: &SyntaxToken,
document: &Document,
graph: &DocumentGraph,
) -> Result<Option<String>> {
if token.kind() == SyntaxKind::Comment {
let comment = Comment::cast(token.clone()).expect("should cast");
if comment.kind() != CommentKind::Documentation {
return Ok(None);
}
for sibling in token.siblings_with_tokens(Direction::Next) {
let target = match sibling {
SyntaxElement::Node(target) => target,
SyntaxElement::Token(token)
if token.kind() == SyntaxKind::Comment
|| token.kind() == SyntaxKind::Whitespace =>
{
continue;
}
_ => break,
};
if target.kind() == SyntaxKind::VersionStatementNode {
return match document.root().doc_comments() {
Some(comments) if !comments.is_empty() => Ok(comments_to_string(comments)),
_ => Ok(None),
};
}
let Some(item_name) = target.descendants_with_tokens().find_map(|element| {
if let SyntaxElement::Token(token) = element
&& let Some(ident) = Ident::cast(token)
{
return Some(ident);
}
None
}) else {
break;
};
return resolve_hover_content(&target, item_name.inner(), document, graph);
}
}
match parent_node.kind() {
SyntaxKind::TypeRefNode | SyntaxKind::LiteralStructNode => {
if let Some(s) = document.struct_by_name(token.text()) {
let root = if let Some(ns_name) = s.namespace() {
let ns = document.namespace(ns_name).unwrap();
let node = graph.get(graph.get_index(ns.source()).unwrap());
node.document().unwrap().root()
} else {
document.root()
};
return Ok(provide_struct_documentation(s, &root));
}
if let Some(e) = document.enum_by_name(token.text()) {
let root = if let Some(ns_name) = e.namespace() {
let ns = document.namespace(ns_name).unwrap();
let node = graph.get(graph.get_index(ns.source()).unwrap());
node.document().unwrap().root()
} else {
document.root()
};
return Ok(provide_enum_documentation(e, &root));
}
}
SyntaxKind::EnumVariantNode => {
let variant = EnumVariant::cast(parent_node.clone()).unwrap();
let variant_name = variant.name().text().to_string();
if let Some(value_expr) = variant.value() {
let content = format!(
"```wdl\n{} = {}\n```",
variant_name,
value_expr.inner().text()
);
return Ok(Some(content));
} else {
let content = format!("```wdl\n{} = \"{}\"\n```", variant_name, variant_name);
return Ok(Some(content));
}
}
SyntaxKind::CallTargetNode => {
let target = CallTarget::cast(parent_node.clone()).unwrap();
let mut target_names = target.names();
let (ns_name, callee_name) = match (target_names.next(), target_names.next()) {
(Some(ns), Some(name)) => {
if token.span() == name.span() {
(Some(ns), name)
} else if token.span() == ns.span() {
if let Some(ns) = document.namespace(token.text()) {
return Ok(Some(format!(
"```wdl\n(import) {}\n```\nImports from `{}`",
token.text(),
ns.source()
)));
}
return Ok(None);
} else {
return Ok(None);
}
}
(Some(name), None) if token.span() == name.span() => (None, name),
_ => return Ok(None),
};
let target_doc = if let Some(ns_name) = ns_name {
let ns = document.namespace(ns_name.text()).unwrap();
let node = graph.get(graph.get_index(ns.source()).unwrap());
node.document().unwrap()
} else {
document
};
if let Some(task) = target_doc.task_by_name(callee_name.text()) {
return Ok(provide_task_documentation(task, &target_doc.root()));
}
if let Some(workflow) = target_doc
.workflow()
.filter(|w| w.name() == callee_name.text())
{
return Ok(provide_workflow_documentation(workflow, &target_doc.root()));
}
}
SyntaxKind::AccessExprNode => {
let access_expr = AccessExpr::cast(parent_node.clone()).unwrap();
let (expr, member) = access_expr.operands();
if member.span() != token.span() {
return Ok(None);
}
let Some(scope) = document.find_scope_by_position(parent_node.span().start()) else {
return Ok(None);
};
let mut ctx = TypeEvalContext { scope, document };
let mut evaluator = ExprTypeEvaluator::new(&mut ctx);
let target_type = evaluator
.evaluate_expr(&expr)
.unwrap_or(crate::types::Type::Union);
let (member_ty, documentation) = match target_type {
Type::TypeNameRef(CustomType::Enum(e)) => {
if e.variants().iter().any(|text| text == member.text()) {
if let Some(enum_entry) = document.enum_by_name(e.name()) {
let definition = enum_entry.definition();
if let Some(variant) = definition
.variants()
.find(|v| v.name().text() == member.text())
{
let value_str = if let Some(value_expr) = variant.value() {
value_expr.inner().text().to_string()
} else {
format!("\"{}\"", member.text())
};
let content = format!(
"```wdl\n{}.{}[{}] = {}\n```",
e.name(),
member.text(),
e.inner_value_type(),
value_str
);
return Ok(Some(content));
}
}
let content = format!(
"```wdl\n{}.{}[{}]\n```",
e.name(),
member.text(),
e.inner_value_type()
);
return Ok(Some(content));
}
(None, None)
}
Type::TypeNameRef(CustomType::Struct(_)) => {
return Ok(None);
}
Type::Compound(CompoundType::Custom(CustomType::Struct(s)), _) => {
let target_doc = if let Some(s) = document.struct_by_name(s.name()) {
if let Some(ns_name) = s.namespace() {
let ns = document.namespace(ns_name).unwrap();
let node = graph.get(graph.get_index(ns.source()).unwrap());
node.document().unwrap()
} else {
document
}
} else {
bail!("struct not found in document");
};
let doc = target_doc.struct_by_name(s.name()).and_then(|s| {
let def = StructDefinition::cast(SyntaxNode::new_root(s.node().clone()))?;
def.members()
.find(|m| m.name().text() == member.text())
.and_then(|decl| find_parameter_meta_documentation(decl.name().inner()))
});
(s.members().get(member.text()).cloned(), doc)
}
Type::Call(c) => (c.outputs().get(member.text()).map(|o| o.ty().clone()), None),
Type::Compound(CompoundType::Pair(p), _) => match member.text() {
"left" => (Some(p.left_type().clone()), None),
"right" => (Some(p.right_type().clone()), None),
_ => (None, None),
},
Type::Compound(CompoundType::Custom(CustomType::Enum(e)), _) => {
if e.variants().iter().any(|text| text == member.text()) {
if let Some(enum_entry) = document.enum_by_name(e.name()) {
let definition = enum_entry.definition();
if let Some(variant) = definition
.variants()
.find(|v| v.name().text() == member.text())
{
let value_str = if let Some(value_expr) = variant.value() {
value_expr.inner().text().to_string()
} else {
format!("\"{}\"", member.text())
};
let content = format!(
"```wdl\n{}.{}[{}] = {}\n```",
e.name(),
member.text(),
e.inner_value_type(),
value_str
);
return Ok(Some(content));
}
}
let content = format!(
"```wdl\n{}.{}[{}]\n```",
e.name(),
member.text(),
e.inner_value_type()
);
return Ok(Some(content));
}
(None, None)
}
_ => (None, None),
};
if let Some(ty) = member_ty {
let mut content = format!("```wdl\n(property) {}: {}\n```", member.text(), ty);
if let Some(doc) = documentation {
content.push_str("\n---\n");
content.push_str(&doc);
}
return Ok(Some(content));
}
}
SyntaxKind::CallExprNode => {
let Some(call_expr) = CallExpr::cast(parent_node.clone()) else {
return Ok(None);
};
if call_expr.target().span() != token.span() {
return Ok(None);
}
if let Some(func) = STDLIB.function(call_expr.target().text()) {
let content = get_function_hover_content(call_expr.target().text(), func);
return Ok(Some(content));
}
}
SyntaxKind::LiteralStructItemNode => {
let Some(item) = LiteralStructItem::cast(parent_node.clone()) else {
return Ok(None);
};
let (name, _) = item.name_value();
if name.span() != token.span() {
return Ok(None);
}
let Some(struct_literal) = parent_node.parent().and_then(LiteralStruct::cast) else {
return Ok(None);
};
let struct_name = struct_literal.name();
if let Some(s) = document.struct_by_name(struct_name.text()) {
let def = StructDefinition::cast(SyntaxNode::new_root(s.node().clone()))
.expect("should cast to StructDefinition");
if let Some(member) = def.members().find(|m| m.name().text() == name.text()) {
let doc = find_parameter_meta_documentation(member.name().inner());
let mut content =
format!("```wdl\n(property) {}: {}\n```", name.text(), member.ty());
if let Some(doc) = doc {
content.push_str("\n---\n");
content.push_str(&doc);
}
return Ok(Some(content));
}
}
}
_ => debug!("hover is not implemented for {:?}", parent_node.kind()),
}
Ok(None)
}
fn find_global_hover_in_doc(document: &Document, token: &SyntaxToken) -> Result<Option<String>> {
if let Some(s) = document.struct_by_name(token.text()) {
return Ok(provide_struct_documentation(s, &document.root()));
}
if let Some(e) = document.enum_by_name(token.text()) {
return Ok(provide_enum_documentation(e, &document.root()));
}
if let Some(t) = document.task_by_name(token.text()) {
return Ok(provide_task_documentation(t, &document.root()));
}
if let Some(w) = document.workflow().filter(|w| w.name() == token.text()) {
return Ok(provide_workflow_documentation(w, &document.root()));
}
Ok(None)
}
fn get_function_hover_content(name: &str, func: &Function) -> String {
let (detail, docs) = match func {
Function::Monomorphic(m) => {
let sig = m.signature();
let params = TypeParameters::new(sig.type_parameters());
let detail = format!("```wdl\n{}{}\n```", name, sig.display(¶ms));
let docs = sig.definition().unwrap_or("");
(detail, docs)
}
Function::Polymorphic(p) => {
let detail = p
.signatures()
.iter()
.map(|s| {
let params = TypeParameters::new(s.type_parameters());
format!("```wdl\n{}{}\n```", name, s.display(¶ms))
})
.collect::<Vec<_>>()
.join("\n---\n");
let docs = p
.signatures()
.first()
.and_then(|s| s.definition())
.unwrap_or("");
(detail, docs)
}
};
format!("{detail}\n\n{docs}")
}
fn find_parameter_meta_documentation(token: &SyntaxToken) -> Option<String> {
use crate::handlers::common::docs::comments_to_string;
if let Some(doc) = token
.parent_ancestors()
.find_map(Decl::cast)
.and_then(|d| match &d {
Decl::Bound(b) => b.doc_comments(),
Decl::Unbound(u) => u.doc_comments(),
})
.and_then(comments_to_string)
{
return Some(doc);
}
let parent = token.parent_ancestors().find(|p| {
matches!(
p.kind(),
SyntaxKind::StructDefinitionNode
| SyntaxKind::TaskDefinitionNode
| SyntaxKind::WorkflowDefinitionNode
)
})?;
let param_meta = parent.children().find_map(ParameterMetadataSection::cast)?;
for item in param_meta.items() {
if item.name().text() == token.text() {
return Some(render_parameter_meta_value(&item.value()));
}
}
None
}
fn render_parameter_meta_value(value: &MetadataValue) -> String {
match value {
MetadataValue::String(s) => s
.text()
.map(|t| t.text().to_string())
.unwrap_or_else(|| s.inner().text().to_string().trim_matches('"').to_string()),
MetadataValue::Object(obj) => {
object_description_and_help(obj).unwrap_or_else(|| value.text().to_string())
}
_ => value.text().to_string().trim_matches('"').to_string(),
}
}
fn object_description_and_help(obj: &MetadataObject) -> Option<String> {
let mut description: Option<String> = None;
let mut help: Option<String> = None;
for item in obj.items() {
let key = item.name().text().to_string();
if key != "description" && key != "help" {
continue;
}
if let MetadataValue::String(s) = item.value()
&& let Some(t) = s.text()
{
let text = t.text().to_string();
if key == "description" {
description = Some(text);
} else {
help = Some(text);
}
}
}
match (description, help) {
(Some(mut d), Some(h)) => {
d.push_str("\n\n");
d.push_str(&h);
Some(d)
}
(Some(d), None) => Some(d),
(None, Some(h)) => Some(h),
(None, None) => None,
}
}