qlue-ls 2.8.0

A language server for SPARQL
Documentation
//! Core language server implementation.
//!
//! This module contains the [`Server`] struct and the main message handling loop.
//! The server is platform-agnostic and works with both native stdio and WASM streams.
//!
//! # Key Types
//!
//! - [`Server`]: Holds server state, settings, capabilities, and the message send function
//! - [`handle_message`]: Entry point for processing incoming LSP messages
//!
//! # Architecture
//!
//! The server uses a closure-based approach for output: callers provide a `Fn(String)`
//! that handles sending responses. This allows the same server code to work with
//! stdio (native) or Web Streams (WASM).
//!
//! Message dispatch happens in [`message_handler`], which routes method names to
//! specific handlers. The server is wrapped in `Rc<Mutex<>>` for async access.
//!
//! # Submodules
//!
//! - [`state`]: Document storage, parse tree cache, backend registry
//! - [`capabilities`]: LSP capability negotiation
//! - [`configuration`]: Settings from `qlue-ls.toml`/`qlue-ls.yml`
//! - [`message_handler`]: Request/notification dispatch
//! - [`lsp`]: Protocol types and JSON-RPC serialization
//! - [`analysis`]: Semantic analysis (completions, hover, etc.)

mod analysis;
mod capabilities;
mod common;
pub(crate) mod configuration;
mod lsp;
mod sparql_operations;
mod state;
mod tools;

pub(crate) mod message_handler;

use capabilities::create_capabilities;
use configuration::Settings;
use futures::lock::Mutex;
use lsp::{
    ServerInfo,
    errors::{ErrorCode, LSPError},
    rpc::{RecoverId, ResponseMessage},
};
use message_handler::dispatch;
use serde::Serialize;
use state::ServerState;
use std::{any::type_name, collections::HashMap, fmt::Debug, rc::Rc};
use tools::Tools;
use tracing::{error, info};
use wasm_bindgen::prelude::wasm_bindgen;

use crate::server::{configuration::CompletionTemplate, lsp::LspMessage};

#[wasm_bindgen]
pub struct Server {
    pub(crate) state: ServerState,
    pub(crate) settings: Settings,
    pub(crate) capabilities: lsp::capabilities::server::ServerCapabilities,
    pub(crate) client_capabilities: Option<lsp::capabilities::client::ClientCapabilities>,
    pub(crate) server_info: ServerInfo,
    tools: Tools,
    send_message_closure: Box<dyn Fn(String)>,
}

impl Server {
    pub fn new(write_function: impl Fn(String) + 'static) -> Server {
        let version = env!("CARGO_PKG_VERSION");
        info!("Started Language Server: Qlue-ls - version: {}", version);
        Self {
            state: ServerState::new(),
            settings: Settings::new(),
            capabilities: create_capabilities(),
            client_capabilities: None,
            server_info: ServerInfo {
                name: "Qlue-ls".to_string(),
                version: Some(version.to_string()),
            },
            tools: Tools::init(),
            send_message_closure: Box::new(write_function),
        }
    }

    pub(crate) fn bump_request_id(&mut self) -> u32 {
        self.state.bump_request_id()
    }

    pub fn get_version(&self) -> String {
        self.server_info
            .version
            .clone()
            .unwrap_or("not-specified".to_string())
    }

    fn send_message<T>(&self, message: T) -> Result<(), LSPError>
    where
        T: Serialize + LspMessage + Debug,
    {
        let message_string = serde_json::to_string(&message).map_err(|error| {
            LSPError::new(
                ErrorCode::ParseError,
                &format!(
                    "Could not deserialize RPC-message \"{}\"\n\n{}",
                    type_name::<T>(),
                    error
                ),
            )
        })?;
        (self.send_message_closure)(message_string);
        Ok(())
    }

    /// Shortens a raw URI into its CURIE (Compact URI) form and retrieves related metadata.
    ///
    /// This method takes a raw URI as input, looks up a URI converter from the given backend
    /// (or falls back to the default converter), and attempts to compress the URI into its
    /// CURIE form.
    ///
    /// # Parameters
    /// - `uri`: A string slice representing the raw URI to be shortened.
    /// - `backend_name`: An optional backend name whose converter should be used.
    ///   If `None` or if the backend is not found, the default converter is used.
    ///
    /// # Returns
    /// - `Some((prefix, uri_prefix, curie))` if the URI can be successfully compacted:
    ///   - `prefix`: A `String` representing the prefix associated with the URI.
    ///   - `uri_prefix`: A `String` representing the URI namespace prefix.
    ///   - `curie`: A `String` representing the compact CURIE form of the URI.
    /// - `None` if no converter is available, or the URI cannot be found or shortened.
    pub(crate) fn shorten_uri(
        &self,
        uri: &str,
        backend_name: Option<&str>,
    ) -> Option<(String, String, String)> {
        let converter = backend_name
            .and_then(|name| self.state.get_converter(name))
            .or(self.state.get_default_converter())?;
        let record = converter.find_by_uri(uri).ok()?;
        let curie = converter.compress(uri).ok()?;
        Some((record.prefix.clone(), record.uri_prefix.clone(), curie))
    }

    pub(crate) fn load_templates(
        &mut self,
        backend_name: &str,
        templates: HashMap<CompletionTemplate, String>,
    ) -> Result<(), LSPError> {
        for (key, value) in templates {
            self.tools
                .tera
                .add_raw_template(&format!("{}-{}", &backend_name, &key), &value)
                .map_err(|err| {
                    tracing::error!("{}", err);
                    LSPError::new(
                        ErrorCode::InvalidParams,
                        &format!(
                            "Could not load template: {} of backend {}",
                            &key, &backend_name
                        ),
                    )
                })?;
        }
        Ok(())
    }
}

async fn handle_error(server_rc: Rc<Mutex<Server>>, message: &str, error: LSPError) {
    tracing::error!(
        "Error occurred while handling message:\n\"{}\"\n\n{:?}\n{}",
        message,
        error.code,
        error.message
    );
    if let Ok(id) = serde_json::from_str::<RecoverId>(message).map(|msg| msg.id)
        && let Err(error) = server_rc
            .lock()
            .await
            .send_message(ResponseMessage::error(&id, error))
    {
        error!(
            "CRITICAL: could not serialize error message (this very bad):\n{:?}",
            error
        )
    }
}

pub async fn handle_message(server_rc: Rc<Mutex<Server>>, message: String) {
    if let Err(err) = dispatch(server_rc.clone(), &message).await {
        handle_error(server_rc.clone(), &message, err).await;
    }
}