use fatou_parser::ast::{DocText, StaticDocText};
use fatou_parser::documentation::ParseOutput;
use fatou_parser::documentation::ast::{
CodeBlock, DocumenterLinkKind, FenceKind, FootnoteDefinition, FootnoteReference, Heading, Link,
};
use rowan::ast::AstNode as _;
use rowan::{TextRange, TextSize};
use crate::semantic::{SemanticDoc, SemanticModel};
use crate::text::{LineIndex, PositionEncoding};
pub(crate) struct DocumentationContext<'a> {
pub(crate) attachment: &'a SemanticDoc,
pub(crate) decoded: &'a StaticDocText,
pub(crate) markdown: ParseOutput,
pub(crate) offset: TextSize,
}
impl<'a> DocumentationContext<'a> {
pub(crate) fn at(model: &'a SemanticModel, source_offset: TextSize) -> Option<Self> {
model.documentation().iter().find_map(|attachment| {
let DocText::Static(decoded) = &attachment.text else {
return None;
};
let offset = decoded.source_map().decoded_offset(source_offset)?;
Some(Self {
attachment,
decoded,
markdown: fatou_parser::documentation::parse(decoded.as_str()),
offset,
})
})
}
pub(crate) fn explicit_ref(&self) -> Option<ExplicitRef> {
if let Some(reference) = self.markdown.cst.descendants().find_map(|node| {
let link = Link::cast(node)?;
let documenter = link.documenter_link()?;
if documenter.kind() != DocumenterLinkKind::Ref {
return None;
}
let range = documenter.target_range()?;
range.contains_inclusive(self.offset).then(|| ExplicitRef {
target: documenter.target().unwrap_or_default().to_string(),
range,
offset: TextSize::new(u32::from(self.offset) - u32::from(range.start())),
})
}) {
return Some(reference);
}
let cursor = usize::from(self.offset);
let prefix = self.decoded.as_str().get(..cursor)?;
let line_start = prefix.rfind(['\r', '\n']).map_or(0, |index| index + 1);
let marker = prefix[line_start..].rfind("(@ref")? + line_start;
let after_marker = marker + "(@ref".len();
let suffix = &prefix[after_marker..];
if suffix.is_empty() || !suffix.starts_with(char::is_whitespace) {
return None;
}
let mut nested_parentheses = 0_u32;
for character in suffix.chars() {
match character {
'(' => nested_parentheses += 1,
')' if nested_parentheses == 0 => return None,
')' => nested_parentheses -= 1,
_ => {}
}
}
let target_start = after_marker + suffix.len() - suffix.trim_start().len();
let range = TextRange::new(TextSize::new(target_start as u32), self.offset);
Some(ExplicitRef {
target: prefix[target_start..].to_string(),
range,
offset: TextSize::new((cursor - target_start) as u32),
})
}
pub(crate) fn markdown_reference(&self) -> Option<MarkdownReference> {
if let Some(reference) = self.markdown.cst.descendants().find_map(|node| {
let link = Link::cast(node)?;
let destination = link.destination();
let id = destination.strip_prefix('#')?;
link.destination_range()?
.contains_inclusive(self.offset)
.then(|| MarkdownReference::Anchor(id.to_string()))
}) {
return Some(reference);
}
self.markdown.cst.descendants().find_map(|node| {
let reference = FootnoteReference::cast(node)?;
reference
.syntax()
.text_range()
.contains_inclusive(self.offset)
.then(|| MarkdownReference::Footnote(reference.id()))
})
}
pub(crate) fn embedded_julia(&self) -> Option<EmbeddedJulia<'a>> {
self.markdown.cst.descendants().find_map(|node| {
let fence = CodeBlock::cast(node)?;
if !fence.fence_kind().contains_julia() {
return None;
}
let range = fence.content_range()?;
if !range.contains_inclusive(self.offset) {
return None;
}
let start = usize::from(range.start());
let end = usize::from(range.end());
let text = self.decoded.as_str().get(start..end)?;
Some(EmbeddedJulia {
decoded: self.decoded,
text,
range,
offset: TextSize::new(u32::from(self.offset) - u32::from(range.start())),
kind: fence.fence_kind(),
})
})
}
pub(crate) fn source_range(&self, range: TextRange) -> Option<TextRange> {
self.decoded.source_map().source_range(range)
}
}
pub(crate) struct ExplicitRef {
pub(crate) target: String,
pub(crate) range: TextRange,
pub(crate) offset: TextSize,
}
pub(crate) enum MarkdownReference {
Anchor(String),
Footnote(String),
}
pub(crate) struct EmbeddedJulia<'a> {
decoded: &'a StaticDocText,
pub(crate) text: &'a str,
pub(crate) range: TextRange,
pub(crate) offset: TextSize,
#[allow(dead_code)]
pub(crate) kind: FenceKind,
}
impl EmbeddedJulia<'_> {
pub(crate) fn source_range(&self, range: TextRange) -> Option<TextRange> {
let base = u32::from(self.range.start());
let decoded = TextRange::new(
TextSize::new(base.checked_add(u32::from(range.start()))?),
TextSize::new(base.checked_add(u32::from(range.end()))?),
);
self.decoded.source_map().source_range(decoded)
}
pub(crate) fn source_range_from_lsp(
&self,
range: lsp_types::Range,
index: &LineIndex<'_>,
encoding: PositionEncoding,
) -> Option<TextRange> {
let start = index.position_to_byte(range.start, encoding);
let end = index.position_to_byte(range.end, encoding);
self.source_range(TextRange::new(
TextSize::new(start as u32),
TextSize::new(end as u32),
))
}
}
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,
})
}
pub(crate) fn markdown_definition(
model: &SemanticModel,
reference: &MarkdownReference,
) -> Option<TextRange> {
for (_, decoded) in static_documentation(model) {
let markdown = fatou_parser::documentation::parse(decoded.as_str());
let decoded_range = match reference {
MarkdownReference::Anchor(id) => markdown.cst.descendants().find_map(|node| {
if let Some(link) = Link::cast(node.clone())
&& let Some(documenter) = link.documenter_link()
&& documenter.kind() == DocumenterLinkKind::Id
&& documenter.target() == Some(id.as_str())
{
return documenter.target_range();
}
let heading = Heading::cast(node)?;
let content = heading.content();
(content == *id || heading.slug() == *id).then(|| heading.syntax().text_range())
}),
MarkdownReference::Footnote(id) => markdown
.cst
.descendants()
.filter_map(FootnoteDefinition::cast)
.find(|definition| definition.id() == *id)
.map(|definition| definition.syntax().text_range()),
};
if let Some(range) = decoded_range
&& let Some(source) = decoded.source_map().source_range(range)
{
return Some(source);
}
}
None
}
pub(crate) fn markdown_anchor_names(model: &SemanticModel) -> Vec<String> {
let mut names = Vec::new();
for (_, decoded) in static_documentation(model) {
let markdown = fatou_parser::documentation::parse(decoded.as_str());
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()
&& !names.iter().any(|name| name == target)
{
names.push(target.to_string());
}
if let Some(heading) = Heading::cast(node) {
let content = heading.content();
if !content.is_empty() && !names.contains(&content) {
names.push(content.clone());
}
let slug = heading.slug();
if !slug.is_empty() && !names.contains(&slug) {
names.push(slug);
}
}
}
}
names
}
pub(crate) fn render(text: &str) -> Option<String> {
let text = text.trim();
(!text.is_empty()).then(|| text.to_string())
}
pub(crate) fn lsp_range(
range: TextRange,
index: &LineIndex<'_>,
encoding: PositionEncoding,
) -> lsp_types::Range {
lsp_types::Range::new(
index.byte_to_position(range.start().into(), encoding),
index.byte_to_position(range.end().into(), encoding),
)
}