qlue-ls 2.7.0

A language server for SPARQL
Documentation
use std::rc::Rc;

use futures::lock::Mutex;
use ll_sparql_parser::{
    SyntaxNode,
    ast::{AstNode, GroupGraphPattern, QueryUnit},
};
use text_size::TextSize;

use crate::server::{
    Server,
    lsp::{
        JumpRequest, JumpResponse, JumpResult,
        errors::{ErrorCode, LSPError},
        textdocument::{Position, TextDocumentItem},
    },
};

#[tracing::instrument(skip_all, fields(id = %request.get_id(), uri = %request.params.base.text_document.uri))]
pub(super) async fn handle_jump_request(
    server_rc: Rc<Mutex<Server>>,
    request: JumpRequest,
) -> Result<(), LSPError> {
    let server = server_rc.lock().await;
    let document_uri = &request.params.base.text_document.uri;
    let document = server.state.get_document(document_uri)?;
    let root = server.state.get_cached_parse_tree(document_uri)?.tree;
    let cursor_offset = request
        .params
        .base
        .position
        .byte_index(&document.text)
        .ok_or(LSPError::new(
            ErrorCode::InvalidRequest,
            "given position is not inside document",
        ))?;
    let results = relevant_positions(
        document,
        root,
        request.params.previous.is_some_and(|prev| prev),
    );
    let jump_position = if request.params.previous.is_some_and(|prev| prev) {
        // NOTE: Jump to previous position
        let last = results.last().cloned();
        results
            .into_iter()
            .rev()
            .find(|(offset, _, _)| offset < &cursor_offset)
            .or(last)
    } else {
        // NOTE: Jump to next position
        let first = results.first().cloned();
        results
            .into_iter()
            .find(|(offset, _, _)| offset > &cursor_offset)
            .or(first)
    }
    .map(|(offset, before, after)| {
        JumpResult::new(
            Position::from_byte_index(offset, &document.text).unwrap(),
            before,
            after,
        )
    });
    server.send_message(JumpResponse::new(request.get_id(), jump_position))?;
    Ok(())
}

fn relevant_positions(
    _document: &TextDocumentItem,
    root: SyntaxNode,
    jump_to_previous: bool,
) -> Vec<(TextSize, Option<&str>, Option<&str>)> {
    let mut res = Vec::new();
    if let Some(query_unit) = QueryUnit::cast(root) {
        // NOTE: End of select clause
        if let Some(offset) = query_unit.select_query().and_then(|sq| {
            if jump_to_previous {
                sq.where_clause().map(|wc| wc.syntax().text_range().start())
            } else {
                sq.select_clause().map(|sc| sc.syntax().text_range().end())
            }
        }) {
            res.push((
                offset,
                (!jump_to_previous).then_some(" "),
                jump_to_previous.then_some(" "),
            ));
        }

        for (offset, has_children) in query_unit
            .syntax()
            .descendants()
            .filter_map(GroupGraphPattern::cast)
            .filter_map(|ggp| {
                ggp.syntax().last_child_or_token().map(|token| {
                    (
                        token.text_range().start(),
                        ggp.syntax().first_child().is_some(),
                    )
                })
            })
        {
            res.push((
                offset,
                has_children.then_some("  ").or(Some("\n  ")),
                Some("\n"),
            ));
        }
        // NOTE: End of soulution modifier
        if jump_to_previous {
            if let Some(offset) = query_unit
                .syntax()
                .last_token()
                .map(|token| token.text_range().end())
            {
                res.push((offset, None, None));
            }
        } else {
            if let Some(offset) = query_unit.select_query().and_then(|sq| {
                sq.soulution_modifier()
                    .map(|sm| sm.syntax().clone())
                    .or(sq.where_clause().map(|wc| wc.syntax().clone()))
                    .map(|node| node.text_range().end())
            }) {
                res.push((offset, Some("\n"), None));
            }
        }
    }
    res.sort_by(|a, b| a.0.cmp(&b.0));
    res
}