use std::collections::{BTreeMap, HashSet};
use fatou_parser::ast::{DocText, StaticDocText};
use fatou_parser::documentation::ast::{DocumenterLinkKind, FootnoteDefinition, Heading, Link};
use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::semantic::{SemanticDoc, SemanticModel};
pub enum MarkdownReference {
Anchor(String),
Footnote(String),
}
#[derive(Debug, PartialEq, Eq)]
struct Definition {
payload: usize,
range: TextRange,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct DocumentationIndex {
names: Vec<String>,
anchors: BTreeMap<String, Vec<Definition>>,
footnotes: BTreeMap<String, Vec<Definition>>,
}
impl DocumentationIndex {
pub fn build<'a>(payloads: impl IntoIterator<Item = &'a str>) -> Self {
let mut index = Self::default();
let mut seen = HashSet::new();
for (payload, text) in payloads.into_iter().enumerate() {
let markdown = fatou_parser::documentation::parse(text);
for node in markdown.cst.descendants() {
if let Some(link) = Link::cast(node.clone())
&& let Some(documenter) = link.documenter_link()
&& documenter.kind() == DocumenterLinkKind::Id
&& let Some(target) = documenter.target()
{
if seen.insert(target.to_string()) {
index.names.push(target.to_string());
}
if let Some(range) = documenter.target_range() {
insert_definition(&mut index.anchors, target.to_string(), payload, range);
}
}
if let Some(heading) = Heading::cast(node.clone()) {
for name in [heading.content(), heading.slug()] {
if !name.is_empty() && seen.insert(name.clone()) {
index.names.push(name.clone());
}
insert_definition(&mut index.anchors, name, payload, node.text_range());
}
}
if let Some(footnote) = FootnoteDefinition::cast(node) {
insert_definition(
&mut index.footnotes,
footnote.id(),
payload,
footnote.syntax().text_range(),
);
}
}
}
index
}
pub fn anchor_names(&self) -> &[String] {
&self.names
}
pub fn definition(
&self,
model: &SemanticModel,
reference: &MarkdownReference,
) -> Option<TextRange> {
let definitions = match reference {
MarkdownReference::Anchor(id) => self.anchors.get(id),
MarkdownReference::Footnote(id) => self.footnotes.get(id),
}?;
let mut documents = static_documentation(model).enumerate();
for definition in definitions {
let (_, (_, decoded)) =
documents.find(|(ordinal, _)| *ordinal == definition.payload)?;
if let Some(source) = decoded.source_map().source_range(definition.range) {
return Some(source);
}
}
None
}
}
fn insert_definition(
definitions: &mut BTreeMap<String, Vec<Definition>>,
name: String,
payload: usize,
range: TextRange,
) {
let entries = definitions.entry(name).or_default();
if entries.last().is_none_or(|entry| entry.payload != payload) {
entries.push(Definition { payload, range });
}
}
pub(crate) fn static_documentation(
model: &SemanticModel,
) -> impl Iterator<Item = (&SemanticDoc, &StaticDocText)> {
model
.documentation()
.iter()
.filter_map(|doc| match &doc.text {
DocText::Static(text) => Some((doc, text)),
DocText::Opaque(_) | DocText::Invalid(_) => None,
})
}