lmntalc-ide 0.13.1

IDE-oriented document/session APIs for lmntalc
Documentation
use std::collections::HashMap;

use lmntalc_core::syntax::{
    ast::{Atom, AtomName, FunctorName, Membrane, Process, ProcessList, Rule},
    token::KEYWORD,
};

use crate::{OutlineKind, OutlineSymbol, ReferenceIndex, SemanticKind, SemanticSpan, Span};

#[derive(Debug)]
pub(crate) struct LanguageInfo {
    outline: Vec<OutlineSymbol>,
    semantic_spans: Vec<SemanticSpan>,
    reference_index: ReferenceIndex,
}

impl LanguageInfo {
    pub(crate) fn outline(&self) -> &[OutlineSymbol] {
        &self.outline
    }

    pub(crate) fn semantic_spans(&self) -> &[SemanticSpan] {
        &self.semantic_spans
    }

    pub(crate) fn reference_index(&self) -> &ReferenceIndex {
        &self.reference_index
    }
}

#[derive(Debug, Default)]
struct ScopeResult {
    outline: Vec<OutlineSymbol>,
    link_occurrences: HashMap<String, Vec<Span>>,
    hyperlink_occurrences: HashMap<String, Vec<Span>>,
}

#[derive(Debug, Default)]
struct LanguageAnalyzer {
    semantic_spans: Vec<SemanticSpan>,
    queryable_spans: Vec<Span>,
    reference_groups: Vec<Vec<Span>>,
}

pub(crate) fn build_language_info(ast: &Membrane) -> LanguageInfo {
    let mut analyzer = LanguageAnalyzer::default();

    let mut init_result = ScopeResult::default();
    for process_list in &ast.process_lists {
        init_result.extend(analyzer.analyze_process_list(process_list, true));
    }
    analyzer.filter_links_top(init_result.link_occurrences);

    let mut rule_result = ScopeResult::default();
    for rule in &ast.rules {
        rule_result.extend(analyzer.analyze_rule(rule));
    }

    init_result
        .hyperlink_occurrences
        .extend(rule_result.hyperlink_occurrences);
    analyzer
        .reference_groups
        .extend(init_result.hyperlink_occurrences.into_values());

    let mut outline = Vec::new();
    if !ast.process_lists.is_empty() {
        let span = span_for_process_lists(&ast.process_lists).unwrap_or(ast.span);
        outline.push(OutlineSymbol {
            name: "init".to_string(),
            kind: OutlineKind::InitialProcess,
            span,
            selection_span: span,
            children: init_result.outline,
        });
    }
    outline.extend(rule_result.outline);

    LanguageInfo {
        outline,
        semantic_spans: analyzer.semantic_spans,
        reference_index: ReferenceIndex::new(analyzer.reference_groups, analyzer.queryable_spans),
    }
}

impl LanguageAnalyzer {
    fn analyze_process_list(&mut self, ast: &ProcessList, top_level: bool) -> ScopeResult {
        let mut result = ScopeResult::default();
        for process in &ast.processes {
            result.extend(self.analyze_process(process, top_level));
        }
        result
    }

    fn analyze_process(&mut self, process: &Process, top_level: bool) -> ScopeResult {
        let mut result = ScopeResult::default();
        match process {
            Process::Membrane(membrane) => result.extend(self.analyze_membrane(membrane)),
            Process::Atom(atom) => {
                for arg in &atom.args {
                    result.extend(self.analyze_process(arg, false));
                }
                self.add_semantic_span(atom.name.1, semantic_kind_for_atom(atom), true);
            }
            Process::Link(link) => {
                if !top_level {
                    self.add_semantic_span(link.span, SemanticKind::Link, true);
                    result
                        .link_occurrences
                        .entry(link.name.clone())
                        .or_default()
                        .push(link.span);
                }
            }
            Process::Hyperlink(hyperlink) => {
                self.add_semantic_span(hyperlink.span, SemanticKind::Hyperlink, true);
                result
                    .hyperlink_occurrences
                    .entry(hyperlink.name.0.clone())
                    .or_default()
                    .push(hyperlink.span);
            }
            Process::ProcessContext(context) => {
                self.add_semantic_span(context.span, SemanticKind::Context, true)
            }
            Process::RuleContext(context) => {
                self.add_semantic_span(context.span, SemanticKind::Context, true)
            }
            Process::LinkBundle(bundle) => {
                self.add_semantic_span(bundle.span, SemanticKind::Context, true)
            }
            Process::Rule(rule) => result.extend(self.analyze_rule(rule)),
        }
        result
    }

    fn analyze_membrane(&mut self, ast: &Membrane) -> ScopeResult {
        let mut result = ScopeResult::default();

        for process_list in &ast.process_lists {
            result.extend(self.analyze_process_list(process_list, false));
        }
        self.filter_links_inner(&mut result.link_occurrences);

        for rule in &ast.rules {
            result.extend(self.analyze_rule(rule));
        }

        let selection_span = selection_span(ast.name.1, ast.span);
        self.add_semantic_span(selection_span, SemanticKind::Membrane, true);

        let children = std::mem::take(&mut result.outline);
        result.outline.push(OutlineSymbol {
            name: if ast.name.0.is_empty() {
                "Anonymous membrane".to_string()
            } else {
                ast.name.0.clone()
            },
            kind: OutlineKind::Membrane,
            span: ast.span,
            selection_span,
            children,
        });
        result
    }

    fn analyze_rule(&mut self, ast: &Rule) -> ScopeResult {
        let selection_span = selection_span(ast.name.1, ast.span);
        self.add_semantic_span(selection_span, SemanticKind::Rule, false);

        let mut result = self.analyze_process_list(&ast.head, true);
        if let Some(propagation) = &ast.propagation {
            result.extend(self.analyze_process_list(propagation, true));
        }
        self.filter_links_inner(&mut result.link_occurrences);

        if let Some(body) = &ast.body {
            result.extend(self.analyze_process_list(body, true));
        }
        self.filter_links_top(result.link_occurrences.clone());
        result.link_occurrences.clear();

        result.outline.push(OutlineSymbol {
            name: ast.name.0.clone(),
            kind: OutlineKind::Rule,
            span: ast.span,
            selection_span,
            children: Vec::new(),
        });
        result
    }

    fn add_semantic_span(&mut self, span: Span, kind: SemanticKind, queryable: bool) {
        self.semantic_spans.push(SemanticSpan { span, kind });
        if queryable {
            self.queryable_spans.push(span);
        }
    }

    fn filter_links_top(&mut self, links: HashMap<String, Vec<Span>>) {
        for occurrences in links.into_values() {
            if occurrences.len() == 2 {
                self.reference_groups.push(occurrences);
            }
        }
    }

    fn filter_links_inner(&mut self, links: &mut HashMap<String, Vec<Span>>) {
        links.retain(|_, occurrences| match occurrences.len() {
            0 | 1 => true,
            2 => {
                self.reference_groups.push(occurrences.clone());
                false
            }
            _ => false,
        });
    }
}

impl ScopeResult {
    fn extend(&mut self, other: ScopeResult) {
        self.outline.extend(other.outline);
        for (link, occurrences) in other.link_occurrences {
            self.link_occurrences
                .entry(link)
                .or_default()
                .extend(occurrences);
        }
        for (hyperlink, occurrences) in other.hyperlink_occurrences {
            self.hyperlink_occurrences
                .entry(hyperlink)
                .or_default()
                .extend(occurrences);
        }
    }
}

fn semantic_kind_for_atom(atom: &Atom) -> SemanticKind {
    match &atom.name.0 {
        AtomName::Operator(_) => SemanticKind::OperatorAtom,
        AtomName::Int(_) | AtomName::Float(_) => SemanticKind::NumberAtom,
        AtomName::Char(_) => SemanticKind::StringAtom,
        AtomName::Functor(FunctorName::String(_) | FunctorName::QuotedString(_)) => {
            SemanticKind::StringAtom
        }
        AtomName::Functor(FunctorName::AtomName(name)) if KEYWORD.contains(&name.as_str()) => {
            SemanticKind::KeywordAtom
        }
        AtomName::Functor(_) => SemanticKind::Atom,
    }
}

fn span_for_process_lists(process_lists: &[ProcessList]) -> Option<Span> {
    process_lists
        .iter()
        .map(|process_list| process_list.span)
        .reduce(|lhs, rhs| lhs.merge(rhs))
}

fn selection_span(candidate: Span, fallback: Span) -> Span {
    if candidate.is_empty() {
        fallback
    } else {
        candidate
    }
}