panache 2.43.0

An LSP, formatter, and linter for Markdown, Quarto, and R Markdown
use crate::syntax::SyntaxNode;

use super::helpers;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SymbolTarget {
    Citation(String),
    Crossref(String),
    ChunkLabel(String),
    ExampleLabel(String),
    HeadingLink(String),
    HeadingId(String),
    Reference { label: String, is_footnote: bool },
}

pub(crate) fn resolve_symbol_target_at_offset(
    root: &SyntaxNode,
    offset: usize,
) -> Option<SymbolTarget> {
    if let Some((label, is_footnote)) = helpers::extract_definition_target_at_offset(root, offset) {
        return Some(SymbolTarget::Reference { label, is_footnote });
    }

    if let Some(key) = helpers::extract_example_label_target_at_offset(root, offset) {
        return Some(SymbolTarget::ExampleLabel(key));
    }

    let mut node = helpers::find_node_at_offset(root, offset)?;

    loop {
        if let Some(key) = helpers::extract_citation_key(&node) {
            return Some(SymbolTarget::Citation(key));
        }

        if let Some(key) = helpers::extract_crossref_key(&node) {
            return Some(SymbolTarget::Crossref(key));
        }

        if let Some(key) = helpers::extract_chunk_label_key(&node) {
            return Some(SymbolTarget::ChunkLabel(key));
        }

        if let Some(key) = helpers::extract_heading_id_key(&node) {
            return Some(SymbolTarget::HeadingId(key));
        }

        if let Some(key) = helpers::extract_attribute_id_key(&node) {
            return Some(SymbolTarget::Crossref(key));
        }

        if let Some(key) = helpers::extract_heading_link_target(&node) {
            return Some(SymbolTarget::HeadingLink(key));
        }

        if let Some((label, is_footnote)) = helpers::extract_reference_target(&node) {
            return Some(SymbolTarget::Reference { label, is_footnote });
        }

        node = node.parent()?;
    }
}

#[cfg(test)]
mod tests {
    use super::{SymbolTarget, resolve_symbol_target_at_offset};

    #[test]
    fn resolves_citation_target() {
        let input = "See @doe2020.";
        let root = crate::parse(input, None);
        let offset = input.find("doe2020").unwrap();
        let target = resolve_symbol_target_at_offset(&root, offset);
        assert_eq!(target, Some(SymbolTarget::Citation("doe2020".to_string())));
    }

    #[test]
    fn resolves_bookdown_crossref_with_hyphen() {
        let input = "# Heading 2\n\nSee \\@ref(heading-2).\n";
        let mut config = crate::config::Config {
            flavor: crate::config::Flavor::RMarkdown,
            extensions: crate::config::Extensions::for_flavor(crate::config::Flavor::RMarkdown),
            ..Default::default()
        };
        config.extensions.bookdown_references = true;
        let root = crate::parse(input, Some(config));
        let offset = input.find("heading-2").unwrap();
        let target = resolve_symbol_target_at_offset(&root, offset);
        assert_eq!(
            target,
            Some(SymbolTarget::Crossref("heading-2".to_string()))
        );
    }

    #[test]
    fn resolves_heading_link_target() {
        let input = "# Heading {#heading}\n\nSee [text](#heading).\n";
        let root = crate::parse(input, None);
        let offset = input.rfind("#heading").unwrap() + 1;
        let target = resolve_symbol_target_at_offset(&root, offset);
        assert_eq!(
            target,
            Some(SymbolTarget::HeadingLink("heading".to_string()))
        );
    }

    #[test]
    fn resolves_chunk_label_target_from_hashpipe_label_value() {
        let input = "```{r}\n#| label: fig-plot\nplot(1:10)\n```\n";
        let config = crate::config::Config {
            flavor: crate::config::Flavor::Quarto,
            extensions: crate::config::Extensions::for_flavor(crate::config::Flavor::Quarto),
            ..Default::default()
        };
        let root = crate::parse(input, Some(config));
        let offset = input.find("fig-plot").unwrap();
        let target = resolve_symbol_target_at_offset(&root, offset);
        assert_eq!(
            target,
            Some(SymbolTarget::ChunkLabel("fig-plot".to_string()))
        );
    }

    #[test]
    fn resolves_example_label_target() {
        let config = crate::config::Config {
            flavor: crate::config::Flavor::Pandoc,
            extensions: crate::config::Extensions::for_flavor(crate::config::Flavor::Pandoc),
            ..Default::default()
        };
        let input = "(@good) First example.\n\nAs (@good) shows.\n";
        let root = crate::parse(input, Some(config));
        let offset = input.rfind("good").unwrap();
        let target = resolve_symbol_target_at_offset(&root, offset);
        assert_eq!(target, Some(SymbolTarget::ExampleLabel("good".to_string())));
    }
}