qlue-ls 3.0.1

A language server for SPARQL
Documentation
//! Server state management and document storage.
//!
//! This module manages all mutable state for the language server, including open
//! documents, registered backends, and cached parse trees.
//!
//! # Key Types
//!
//! - [`ServerState`]: Central state container accessed via `Server.state`
//! - [`ServerStatus`]: Lifecycle state (Initializing, Running, ShuttingDown)
//!
//! # State Components
//!
//! - **Documents**: Open text documents keyed by URI, with incremental sync support
//! - **Backends**: SPARQL endpoints with associated prefix maps and request methods
//! - **Parse tree cache**: Single-entry cache to avoid re-parsing unchanged documents
//! - **URI converters**: CURIE/prefix converters for URI compression per backend
//!
//! # Parse Tree Caching
//!
//! [`ServerState::get_cached_parse_tree`] returns cached parse results when the
//! document URI and version match, avoiding expensive re-parsing for repeated
//! operations on the same document state.
//!
//! # Related Modules
//!
//! - [`super::Server`]: Owns the `ServerState` instance
//! - [`super::lsp::textdocument`]: `TextDocumentItem` stored in documents map

use crate::server::configuration::{BackendConfiguration, RequestMethod};

use super::lsp::{
    TextDocumentContentChangeEvent,
    errors::{ErrorCode, LSPError},
    textdocument::TextDocumentItem,
};
use curies::{Converter, Record};
use ll_sparql_parser::{SyntaxNode, parse};
use std::cell::RefCell;
use std::collections::HashMap;

#[derive(Debug, PartialEq)]
pub enum ServerStatus {
    Initializing,
    Running,
    ShuttingDown,
}

#[derive(Debug)]
pub enum ClientType {
    Monaco,
    Neovim,
}

#[derive(Debug)]
pub struct CachedParseTree {
    document_uri: String,
    version: u32,
    tree: SyntaxNode,
    parse_time_ms: f64,
}

pub struct ServerState {
    pub status: ServerStatus,
    pub client_type: Option<ClientType>,
    documents: HashMap<String, TextDocumentItem>,
    backends: HashMap<String, BackendConfiguration>,
    uri_converter: HashMap<String, Converter>,
    default_backend: Option<String>,
    parse_tree_cache: RefCell<Option<CachedParseTree>>,
    request_id_counter: u32,
    running_sparql_requests: HashMap<String, Box<dyn Fn()>>,
    pub label_memory: HashMap<String, String>,
}

impl ServerState {
    pub fn new() -> Self {
        ServerState {
            status: ServerStatus::Initializing,
            client_type: None,
            documents: HashMap::new(),
            backends: HashMap::new(),
            uri_converter: HashMap::new(),
            default_backend: None,
            parse_tree_cache: RefCell::new(None),
            request_id_counter: 0,
            running_sparql_requests: HashMap::new(),
            label_memory: HashMap::new(),
        }
    }

    pub fn bump_request_id(&mut self) -> u32 {
        let current_id = self.request_id_counter;
        self.request_id_counter += 1;
        current_id
    }

    pub fn get_backend_name_by_url(&self, url: &str) -> Option<String> {
        self.backends
            .iter()
            .find_map(|(key, backend)| (backend.url == url).then(|| key.clone()))
    }

    /// Sets the default backend by name.
    ///
    /// # Panics
    ///
    /// Panics if no backend with the given name has been registered.
    pub fn set_default_backend(&mut self, name: String) {
        assert!(self.backends.contains_key(&name));
        self.default_backend = Some(name)
    }

    pub(super) fn get_default_backend(&self) -> Option<&BackendConfiguration> {
        self.backends.get(self.default_backend.as_ref()?)
    }

    pub fn add_backend(&mut self, backend: BackendConfiguration) {
        self.backends.insert(backend.name.clone(), backend);
    }

    /// Return the configured request method for given backend.
    /// Defaults to `GET`.
    pub fn get_backend_request_method(&self, backend_name: &str) -> RequestMethod {
        self.backends
            .get(backend_name)
            .and_then(|backend| backend.request_method.clone())
            .unwrap_or(RequestMethod::GET)
    }

    pub fn load_prefix_map(
        &mut self,
        backend: String,
        map: &HashMap<String, String>,
    ) -> Result<(), LSPError> {
        let mut converter = Converter::default();
        for (prefix, uri_prefix) in map {
            // NOTE: `add_prefix` errors on a duplicate prefix *or* a duplicate uri_prefix.
            //       Instead of failing, overwrite the conflicting record.
            if converter.add_prefix(prefix, uri_prefix).is_err() {
                // INFO: `update_record` matches by prefix, so it only handles the
                //       duplicate-prefix case. If that fails, the uri_prefix was the
                //       conflict: register `prefix` as a synonym on the existing record.
                let result = converter
                    .update_record(Record::new(prefix, uri_prefix))
                    .inspect(|()| {
                        tracing::warn!(
                            "Prefix \"{prefix}\" is already registered, overwriting it with \"{uri_prefix}\""
                        )
                    })
                    .or_else(|_| {
                        let mut record = converter.find_by_uri_prefix(uri_prefix)?.as_ref().clone();
                        tracing::warn!(
                            "URI prefix \"{uri_prefix}\" is already registered to prefix \"{}\", adding \"{prefix}\" as a synonym",
                            record.prefix
                        );
                        record.prefix_synonyms.insert(prefix.clone());
                        converter.update_record(record)
                    });
                if let Err(err) = result {
                    tracing::error!("Could not load prefix \"{prefix}\"\n{}", err);
                    return Err(LSPError::new(
                        ErrorCode::InvalidParams,
                        &format!("Could not load prefix map:\n\"{}\"", err),
                    ));
                }
            }
        }
        self.uri_converter.insert(backend, converter);
        Ok(())
    }

    pub fn get_backend(&self, backend_name: &str) -> Option<&BackendConfiguration> {
        self.backends.get(backend_name)
    }

    pub(super) fn add_document(&mut self, text_document: TextDocumentItem) {
        self.documents
            .insert(text_document.uri.clone(), text_document);
    }

    pub(super) fn change_document(
        &mut self,
        uri: &String,
        content_changes: Vec<TextDocumentContentChangeEvent>,
    ) -> Result<(), LSPError> {
        let document = self.documents.get_mut(uri).ok_or(LSPError::new(
            ErrorCode::InvalidParams,
            &format!("Could not change unknown document {}", uri),
        ))?;
        document.apply_content_changes(content_changes);
        document.increase_version();
        Ok(())
    }

    pub(super) fn get_document(&self, uri: &str) -> Result<&TextDocumentItem, LSPError> {
        self.documents.get(uri).ok_or(LSPError::new(
            ErrorCode::InvalidRequest,
            &format!("Requested document \"{}\"could not be found", uri),
        ))
    }

    #[tracing::instrument(skip(self), fields(cache_hit, parse_time_ms))]
    pub(super) fn get_cached_parse_tree(&self, uri: &str) -> Result<TimedParseResult, LSPError> {
        let document = self.documents.get(uri).ok_or(LSPError::new(
            ErrorCode::InvalidRequest,
            &format!("Requested document \"{}\"could not be found", uri),
        ))?;
        if let Some(cached_parse_tree) = self.parse_tree_cache.borrow().as_ref()
            && uri == cached_parse_tree.document_uri
            && cached_parse_tree.version == document.version()
        {
            tracing::Span::current().record("cache_hit", true);
            return Ok(TimedParseResult {
                tree: cached_parse_tree.tree.clone(),
                parse_time_ms: cached_parse_tree.parse_time_ms,
            });
        }

        tracing::Span::current().record("cache_hit", false);
        let start = get_timestamp_ms();
        let (root, _) = parse(&document.text);
        let parse_time_ms = get_timestamp_ms() - start;
        tracing::Span::current().record("parse_time_ms", parse_time_ms);
        *self.parse_tree_cache.borrow_mut() = Some(CachedParseTree {
            document_uri: uri.to_string(),
            version: document.version(),
            tree: root.clone(),
            parse_time_ms,
        });
        Ok(TimedParseResult {
            tree: root,
            parse_time_ms,
        })
    }

    pub(crate) fn get_default_converter(&self) -> Option<&Converter> {
        self.uri_converter.get(self.default_backend.as_ref()?)
    }

    pub(crate) fn get_converter(&self, backend_name: &str) -> Option<&Converter> {
        self.uri_converter.get(backend_name)
    }

    pub(crate) fn get_all_backends(&self) -> Vec<&BackendConfiguration> {
        self.backends.values().collect()
    }

    #[cfg(target_arch = "wasm32")]
    pub(crate) fn add_running_request(&mut self, id: String, cancel_fn: Box<dyn Fn()>) {
        self.running_sparql_requests.insert(id, cancel_fn);
    }

    #[allow(clippy::borrowed_box)]
    pub(crate) fn get_running_request(&mut self, id: &str) -> Option<&Box<dyn Fn()>> {
        self.running_sparql_requests.get(id)
    }
}

pub struct TimedParseResult {
    pub tree: SyntaxNode,
    pub parse_time_ms: f64,
}

#[cfg(not(target_arch = "wasm32"))]
fn get_timestamp_ms() -> f64 {
    use std::time::SystemTime;
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .expect("system time should be after epoch")
        .as_secs_f64()
        * 1000.0
}

#[cfg(target_arch = "wasm32")]
fn get_timestamp_ms() -> f64 {
    use wasm_bindgen::JsCast;
    use web_sys::WorkerGlobalScope;
    let worker_global: WorkerGlobalScope = js_sys::global().unchecked_into();
    worker_global
        .performance()
        .expect("performance should be available")
        .now()
}