use std::fmt::Write;
use lsp_types::Documentation;
use lsp_types::MarkupContent;
use rowan::TextSize;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Comment;
use wdl_ast::DOC_COMMENT_PREFIX;
use wdl_ast::Documented;
use wdl_ast::v1::Decl;
use wdl_ast::v1::EnumDefinition;
use wdl_ast::v1::InputSection;
use wdl_ast::v1::MetadataSection;
use wdl_ast::v1::MetadataValue;
use wdl_ast::v1::OutputSection;
use wdl_ast::v1::ParameterMetadataSection;
use wdl_ast::v1::StructDefinition;
use wdl_ast::v1::TaskDefinition;
use wdl_ast::v1::WorkflowDefinition;
use wdl_ast::v1::format_meta_value;
use wdl_ast::v1::get_param_meta;
use crate::document::Enum;
use crate::document::Struct;
use crate::document::Task;
use crate::document::Workflow;
pub fn make_md_docs(definition: String) -> Option<Documentation> {
Some(Documentation::MarkupContent(MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: definition,
}))
}
fn normalize_doc_comments(comments: &[Comment]) -> Vec<String> {
let raw: Vec<&str> = comments
.iter()
.filter_map(|c| c.inner().text().strip_prefix(DOC_COMMENT_PREFIX))
.collect();
let min_indent = raw
.iter()
.filter(|line| line.chars().any(|c| !c.is_whitespace()))
.map(|line| line.chars().take_while(|c| *c == ' ' || *c == '\t').count())
.min()
.unwrap_or(0);
raw.into_iter()
.map(|line| {
if line.chars().all(char::is_whitespace) {
String::new()
} else {
line[min_indent..].to_string()
}
})
.collect()
}
pub(crate) fn comments_to_string(comments: Vec<Comment>) -> Option<String> {
if comments.is_empty() {
return None;
}
let lines = normalize_doc_comments(&comments);
Some(lines.join("\n")).filter(|s| !s.trim().is_empty())
}
fn first_paragraph_doc(comments: Vec<Comment>) -> Option<String> {
if comments.is_empty() {
return None;
}
let lines = normalize_doc_comments(&comments);
let para: Vec<&str> = lines
.iter()
.map(String::as_str)
.take_while(|line| !line.is_empty())
.collect();
if para.is_empty() {
None
} else {
Some(para.join("\n"))
}
}
fn write_documented_decls(
f: &mut impl Write,
header: &str,
decls: impl Iterator<Item = Decl>,
param_meta: Option<&ParameterMetadataSection>,
show_default: bool,
) -> std::fmt::Result {
let decls: Vec<Decl> = decls.collect();
if decls.is_empty() {
return Ok(());
}
writeln!(f, "\n{header}")?;
for decl in decls {
let name_text = decl.name().text().to_string();
let ty_text = decl.ty().inner().text().to_string();
let doc = match &decl {
Decl::Unbound(u) => first_paragraph_doc(u.doc_comments().unwrap_or_default()),
Decl::Bound(b) => first_paragraph_doc(b.doc_comments().unwrap_or_default()),
};
write!(f, "- **{}**: `{}`", name_text, ty_text)?;
if show_default && let Some(val) = decl.expr().map(|e| e.text().to_string()) {
write!(f, " = *`{}`*", val.trim_start_matches(" = "))?;
}
if let Some(doc_str) = doc {
writeln!(f, "\n{doc_str}")?;
} else if let Some(meta_val) = get_param_meta(&name_text, param_meta) {
writeln!(f)?;
format_meta_value(f, &meta_val, 2)?;
writeln!(f)?;
} else {
writeln!(f)?;
}
}
Ok(())
}
fn write_documented_inputs(
f: &mut impl Write,
input: Option<&InputSection>,
param_meta: Option<&ParameterMetadataSection>,
) -> std::fmt::Result {
let Some(input) = input else {
return Ok(());
};
write_documented_decls(f, "**Inputs**", input.declarations(), param_meta, true)
}
fn write_documented_outputs(
f: &mut impl Write,
output: Option<&OutputSection>,
param_meta: Option<&ParameterMetadataSection>,
) -> std::fmt::Result {
let Some(output) = output else {
return Ok(());
};
write_documented_decls(
f,
"**Outputs**",
output.declarations().map(Decl::Bound),
param_meta,
false,
)
}
fn render_runnable_doc(
keyword: &str,
name: &str,
doc: Option<String>,
input: Option<&InputSection>,
output: Option<&OutputSection>,
param_meta: Option<&ParameterMetadataSection>,
) -> String {
let mut s = String::new();
let _ = writeln!(s, "```wdl\n{keyword} {name}\n```\n---");
if let Some(desc) = doc {
let _ = writeln!(s, "{desc}\n");
}
let _ = write_documented_inputs(&mut s, input, param_meta);
let _ = write_documented_outputs(&mut s, output, param_meta);
s
}
fn read_meta_description(meta: Option<MetadataSection>) -> Option<String> {
let meta = meta?;
let desc = meta.items().find(|i| i.name().text() == "description")?;
if let MetadataValue::String(s) = desc.value() {
s.text().map(|t| t.text().to_string())
} else {
None
}
}
fn render_task_doc(n: &TaskDefinition) -> String {
let doc = comments_to_string(n.doc_comments().unwrap_or_default())
.or_else(|| read_meta_description(n.metadata()));
render_runnable_doc(
"task",
n.name().text(),
doc,
n.input().as_ref(),
n.output().as_ref(),
n.parameter_metadata().as_ref(),
)
}
fn render_workflow_doc(n: &WorkflowDefinition) -> String {
let doc = comments_to_string(n.doc_comments().unwrap_or_default())
.or_else(|| read_meta_description(n.metadata()));
render_runnable_doc(
"workflow",
n.name().text(),
doc,
n.input().as_ref(),
n.output().as_ref(),
n.parameter_metadata().as_ref(),
)
}
fn render_struct_doc(n: &StructDefinition) -> String {
let mut s = String::new();
let _ = writeln!(s, "```wdl");
let _ = writeln!(s, "{n}");
let _ = writeln!(s, "```\n---");
let description = comments_to_string(n.doc_comments().unwrap_or_default())
.or_else(|| read_meta_description(n.metadata().next()));
if let Some(desc) = description {
let _ = writeln!(s, "{desc}\n");
}
let members: Vec<_> = n.members().collect();
if !members.is_empty() {
let _ = writeln!(s, "\n**Members**");
for member in members {
let name = member.name();
let _ = write!(s, "- **{}**: `{}`", name.text(), member.ty().inner().text());
let doc =
first_paragraph_doc(member.doc_comments().unwrap_or_default()).or_else(|| {
get_param_meta(name.text(), n.parameter_metadata().next().as_ref()).map(
|meta_val| {
let mut buf = String::new();
let _ = format_meta_value(&mut buf, &meta_val, 2);
buf
},
)
});
if let Some(d) = doc {
let _ = writeln!(s, "\n{d}");
} else {
let _ = writeln!(s);
}
}
}
s
}
fn render_enum_doc(n: &EnumDefinition, computed_type: Option<&str>) -> String {
let mut s = String::new();
let _ = writeln!(s, "```wdl");
let _ = write!(s, "{}", n.display(computed_type));
let _ = write!(s, "```");
if let Some(desc) = comments_to_string(n.doc_comments().unwrap_or_default()) {
let _ = write!(s, "\n\n---\n{desc}\n");
}
s
}
pub fn provide_task_documentation(task: &Task, root: &wdl_ast::Document) -> Option<String> {
match TextSize::try_from(task.name_span().start()) {
Ok(offset) => root
.inner()
.token_at_offset(offset)
.left_biased()
.and_then(|t| t.parent_ancestors().find_map(TaskDefinition::cast))
.map(|n| render_task_doc(&n)),
Err(_) => None,
}
}
pub fn provide_workflow_documentation(
workflow: &Workflow,
root: &wdl_ast::Document,
) -> Option<String> {
match TextSize::try_from(workflow.name_span().start()) {
Ok(offset) => root
.inner()
.token_at_offset(offset)
.left_biased()
.and_then(|t| t.parent_ancestors().find_map(WorkflowDefinition::cast))
.map(|n| render_workflow_doc(&n)),
Err(_) => None,
}
}
pub fn provide_struct_documentation(
struct_info: &Struct,
root: &wdl_ast::Document,
) -> Option<String> {
match TextSize::try_from(struct_info.name_span().start()) {
Ok(offset) => root
.inner()
.token_at_offset(offset)
.left_biased()
.and_then(|t| t.parent_ancestors().find_map(StructDefinition::cast))
.map(|n| render_struct_doc(&n)),
Err(_) => None,
}
}
pub fn provide_enum_documentation(enum_info: &Enum, root: &wdl_ast::Document) -> Option<String> {
match TextSize::try_from(enum_info.name_span().start()) {
Ok(offset) => root
.inner()
.token_at_offset(offset)
.left_biased()
.and_then(|t| t.parent_ancestors().find_map(EnumDefinition::cast))
.map(|n| {
let computed_type = enum_info
.ty()
.and_then(|ty| ty.as_enum())
.map(|enum_ty| enum_ty.inner_value_type().to_string());
render_enum_doc(&n, computed_type.as_deref())
}),
Err(_) => None,
}
}