qlue-ls 2.8.0

A language server for SPARQL
Documentation
use std::{collections::HashMap, sync::LazyLock};

use crate::server::{
    Server,
    lsp::{
        base_types::LSPAny,
        diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
        textdocument::{Range, TextDocumentItem},
    },
};
use ll_sparql_parser::ast::{AstNode, GroupGraphPattern, QueryUnit, Triple};

pub static CODE: LazyLock<DiagnosticCode> =
    LazyLock::new(|| DiagnosticCode::String("same-subject".to_string()));

pub(crate) fn diagnostics(
    document: &TextDocumentItem,
    query_unit: &QueryUnit,
    _server: &Server,
) -> Option<Vec<Diagnostic>> {
    let groups = find_all_triple_groups(query_unit);
    Some(
        groups
            .into_iter()
            .flat_map(|(subject, triples)| {
                let ranges = LSPAny::LSPArray(
                    triples
                        .iter()
                        .map(|triple| {
                            let range = triple.syntax().text_range();
                            LSPAny::LSPObject(HashMap::from_iter([
                                ("start".to_string(), LSPAny::Uinteger(range.start().into())),
                                ("end".to_string(), LSPAny::Uinteger(range.end().into())),
                            ]))
                        })
                        .collect(),
                );
                triples.into_iter().map(move |triple| Diagnostic {
                    range: Range::from_byte_offset_range(
                        triple.syntax().text_range(),
                        &document.text,
                    )
                    .expect("triple text range should be in text"),
                    severity: DiagnosticSeverity::Information,
                    code: Some((*CODE).clone()),
                    source: None,
                    message: format!(
                        "Triple with same subject \"{}\" can be contracted",
                        subject
                    ),
                    data: Some(ranges.clone()),
                })
            })
            .collect(),
    )
}

pub(crate) fn find_all_triple_groups(query_unit: &QueryUnit) -> Vec<(String, Vec<Triple>)> {
    let mut result = Vec::new();
    for ggp in query_unit
        .syntax()
        .descendants()
        .filter_map(GroupGraphPattern::cast)
    {
        let triples: Vec<_> = ggp
            .triple_blocks()
            .into_iter()
            .flat_map(|tb| tb.triples())
            .collect();
        let mut groups: HashMap<String, Vec<Triple>> = HashMap::new();
        for triple in triples.into_iter().filter(|triple| !triple.has_error()) {
            if let Some(subject) = triple.subject() {
                groups
                    .entry(subject.text())
                    .and_modify(|group| group.push(triple.clone()))
                    .or_insert(vec![triple]);
            }
        }
        for (subject, triples) in groups.into_iter() {
            if triples.len() >= 2 {
                result.push((subject, triples));
            }
        }
    }
    result
}