panache 2.34.0

An LSP, formatter, and linter for Pandoc markdown, Quarto, and RMarkdown
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::sync::Mutex;
use tower_lsp_server::ls_types::{Location, Range, Uri};

use crate::salsa::Db;

use super::conversions;

#[derive(Clone)]
pub(crate) struct IndexedDocument {
    pub(crate) uri: Uri,
    pub(crate) text: String,
    pub(crate) symbol_index: crate::salsa::SymbolUsageIndex,
}

#[derive(Clone)]
pub(crate) struct ProjectDocumentBundle {
    pub(crate) inputs: Vec<(PathBuf, String)>,
    pub(crate) parse_config: crate::config::Config,
}

pub(crate) async fn project_document_inputs(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    salsa_file: crate::salsa::FileText,
    salsa_config: crate::salsa::FileConfig,
    doc_path: &Path,
    current_content: &str,
) -> Vec<(PathBuf, String)> {
    let doc_paths = project_document_paths(salsa_db, salsa_file, salsa_config, doc_path).await;
    document_inputs_for_paths(salsa_db, doc_path, current_content, doc_paths).await
}

pub(crate) async fn project_document_bundle(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    salsa_file: crate::salsa::FileText,
    salsa_config: crate::salsa::FileConfig,
    doc_path: &Path,
    current_content: &str,
) -> ProjectDocumentBundle {
    let inputs = project_document_inputs(
        salsa_db,
        salsa_file,
        salsa_config,
        doc_path,
        current_content,
    )
    .await;
    let parse_config = {
        let db = salsa_db.lock().await;
        salsa_config.config(&*db).clone()
    };

    ProjectDocumentBundle {
        inputs,
        parse_config,
    }
}

pub(crate) fn parse_with_config(
    input: &str,
    parse_config: &crate::config::Config,
) -> crate::syntax::SyntaxNode {
    crate::parse(input, Some(parse_config.clone()))
}

pub(crate) async fn project_document_paths(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    salsa_file: crate::salsa::FileText,
    salsa_config: crate::salsa::FileConfig,
    doc_path: &Path,
) -> Vec<PathBuf> {
    let db = salsa_db.lock().await;
    let mut doc_paths =
        crate::salsa::project_graph(&*db, salsa_file, salsa_config, doc_path.to_path_buf())
            .documents()
            .iter()
            .cloned()
            .collect::<Vec<_>>();
    if !doc_paths.contains(&doc_path.to_path_buf()) {
        doc_paths.push(doc_path.to_path_buf());
    }
    doc_paths.sort();
    doc_paths.dedup();

    doc_paths
}

pub(crate) async fn document_inputs_for_paths(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    doc_path: &Path,
    current_content: &str,
    mut doc_paths: Vec<PathBuf>,
) -> Vec<(PathBuf, String)> {
    if !doc_paths.contains(&doc_path.to_path_buf()) {
        doc_paths.push(doc_path.to_path_buf());
    }
    doc_paths.sort();
    doc_paths.dedup();

    let db = salsa_db.lock().await;
    let mut inputs = Vec::new();
    for path in doc_paths {
        let text = if path == doc_path {
            current_content.to_string()
        } else if let Some(file) = db.file_text(path.clone()) {
            file.text(&*db).clone()
        } else {
            continue;
        };
        inputs.push((path, text));
    }

    inputs
}

pub(crate) async fn project_symbol_documents(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    salsa_file: crate::salsa::FileText,
    salsa_config: crate::salsa::FileConfig,
    doc_path: &Path,
    current_uri: &Uri,
    current_content: &str,
) -> Vec<IndexedDocument> {
    let inputs = project_document_inputs(
        salsa_db,
        salsa_file,
        salsa_config,
        doc_path,
        current_content,
    )
    .await;

    indexed_documents_from_inputs(
        salsa_db,
        salsa_file,
        salsa_config,
        doc_path,
        current_uri,
        inputs,
    )
    .await
}

pub(crate) async fn indexed_documents_from_inputs(
    salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
    salsa_file: crate::salsa::FileText,
    salsa_config: crate::salsa::FileConfig,
    doc_path: &Path,
    current_uri: &Uri,
    inputs: Vec<(PathBuf, String)>,
) -> Vec<IndexedDocument> {
    let db = salsa_db.lock().await;
    let mut docs = Vec::new();
    for (path, text) in inputs {
        let file = if path == doc_path {
            salsa_file
        } else if let Some(file) = crate::salsa::Db::file_text(&*db, path.clone()) {
            file
        } else {
            continue;
        };

        let symbol_index =
            crate::salsa::symbol_usage_index(&*db, file, salsa_config, path.clone()).clone();
        let uri = Uri::from_file_path(&path).unwrap_or_else(|| current_uri.clone());
        docs.push(IndexedDocument {
            uri,
            text,
            symbol_index,
        });
    }

    docs
}

pub(crate) fn location_from_range(uri: &Uri, text: &str, range: rowan::TextRange) -> Location {
    Location {
        uri: uri.clone(),
        range: Range {
            start: conversions::offset_to_position(text, range.start().into()),
            end: conversions::offset_to_position(text, range.end().into()),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::parse_with_config;

    #[test]
    fn parse_with_config_uses_provided_configuration() {
        let input = "A ref to \\@ref(heading-2).";

        let mut disabled = crate::Config {
            flavor: crate::config::Flavor::RMarkdown,
            ..Default::default()
        };
        disabled.extensions.bookdown_references = false;

        let mut enabled = crate::Config {
            flavor: crate::config::Flavor::RMarkdown,
            ..Default::default()
        };
        enabled.extensions.bookdown_references = true;

        let disabled_tree = parse_with_config(input, &disabled);
        let enabled_tree = parse_with_config(input, &enabled);

        let disabled_has_crossref = disabled_tree
            .descendants()
            .any(|node| node.kind() == crate::syntax::SyntaxKind::CROSSREF);
        let enabled_has_crossref = enabled_tree
            .descendants()
            .any(|node| node.kind() == crate::syntax::SyntaxKind::CROSSREF);

        assert!(!disabled_has_crossref);
        assert!(enabled_has_crossref);
    }
}