mortar_lsp 0.4.3

Mortar language LSP server
Documentation
//! # backend.rs
//!
//! # backend.rs 文件
//!
//! ## Module Overview
//!
//! ## 模块概述
//!
//! Defines the shared backend state for the Mortar language server. It owns the open
//! document cache, diagnostics, symbol tables, language selection, and the submodules that
//! implement LSP handlers, completion, diagnostics, and semantic-token support.
//!
//! 定义了 Mortar 语言服务器的共享 backend 状态。它负责维护打开文档缓存、诊断结果、
//! 符号表、语言选择,以及实现 LSP 处理器、补全、诊断和语义 token 支持的子模块。

use std::sync::Arc;

use dashmap::DashMap;
use mortar_compiler::Language;
use ropey::Rope;
use tokio::sync::RwLock;
use tower_lsp_server::Client;
use tower_lsp_server::lsp_types::*;
use tracing::info;

use crate::analysis::SymbolTable;
use crate::files::Files;

#[derive(Clone)]
pub struct Backend {
    pub client: Client,
    pub files: Arc<RwLock<Files>>,
    pub documents: Arc<DashMap<Uri, (Rope, Option<i32>)>>,
    pub diagnostics: Arc<DashMap<Uri, Vec<Diagnostic>>>,
    pub symbol_tables: Arc<DashMap<Uri, SymbolTable>>,
    pub language: Arc<RwLock<Language>>,
    pub debouncers: Arc<DashMap<Uri, tokio::task::JoinHandle<()>>>,
}

impl Backend {
    pub fn new(client: Client) -> Self {
        Self {
            client,
            files: Arc::new(RwLock::new(Files::new())),
            documents: Arc::new(DashMap::new()),
            diagnostics: Arc::new(DashMap::new()),
            symbol_tables: Arc::new(DashMap::new()),
            language: Arc::new(RwLock::new(Language::English)), // Default to English
            debouncers: Arc::new(DashMap::new()),
        }
    }

    /// Set the language for diagnostics
    pub async fn set_language(&self, language: Language) {
        let mut lang = self.language.write().await;
        *lang = language;
    }

    /// Get the current language
    pub async fn get_language(&self) -> Language {
        *self.language.read().await
    }

    /// Clear all cached data
    pub async fn cleanup(&self) {
        let language = self.get_language().await;
        info!("{}", i18n::get_lsp_text("cleanup_resources", language));

        // Abort all pending debounce tasks
        for entry in self.debouncers.iter() {
            entry.value().abort();
        }
        self.debouncers.clear();

        let documents_count = self.documents.len();
        self.documents.clear();
        info!(
            "{} {}",
            documents_count,
            i18n::get_lsp_text("documents_cleaned", language)
        );

        let diagnostics_count = self.diagnostics.len();
        self.diagnostics.clear();
        info!(
            "{} {}",
            diagnostics_count,
            i18n::get_lsp_text("diagnostics_cleaned", language)
        );

        let symbols_count = self.symbol_tables.len();
        self.symbol_tables.clear();
        info!(
            "{} {}",
            symbols_count,
            i18n::get_lsp_text("symbols_cleaned", language)
        );

        {
            let mut files = self.files.write().await;
            *files = Files::new();
        }

        info!("{}", i18n::get_lsp_text("cleanup_completed", language));
    }

    /// Simplified cleanup operations to avoid asynchronous blocking
    pub fn cleanup_sync(&self) {
        info!("Start synchronously cleaning up LSP server resources...");

        // Abort all pending debounce tasks
        for entry in self.debouncers.iter() {
            entry.value().abort();
        }
        self.debouncers.clear();

        let documents_count = self.documents.len();
        self.documents.clear();

        let diagnostics_count = self.diagnostics.len();
        self.diagnostics.clear();

        let symbols_count = self.symbol_tables.len();
        self.symbol_tables.clear();

        info!(
            "Synchronization cleanup completed: {} documents, {} diagnostics, {} symbol tables",
            documents_count, diagnostics_count, symbols_count
        );
    }

    pub fn _position_to_offset(&self, rope: &Rope, position: Position) -> Option<usize> {
        let line_idx = position.line as usize;
        if line_idx >= rope.len_lines() {
            return None;
        }

        let line_start = rope.line_to_char(line_idx);
        let char_offset = position.character as usize;
        let line_content = rope.line(line_idx);

        if char_offset > line_content.len_chars() {
            return None;
        }

        Some(line_start + char_offset)
    }

    pub fn _offset_to_position(&self, rope: &Rope, offset: usize) -> Option<Position> {
        if offset > rope.len_chars() {
            return None;
        }

        let line_idx = rope.char_to_line(offset);
        let line_start = rope.line_to_char(line_idx);
        let char_offset = offset - line_start;

        Some(Position::new(line_idx as u32, char_offset as u32))
    }

    pub fn get_current_word(&self, line: &str, char_idx: usize) -> String {
        let chars: Vec<char> = line.chars().collect();

        if char_idx >= chars.len() {
            return String::new();
        }

        let mut start = char_idx;
        let mut end = char_idx;

        while start > 0 && (chars[start - 1].is_alphanumeric() || chars[start - 1] == '_') {
            start -= 1;
        }

        while end < chars.len() && (chars[end].is_alphanumeric() || chars[end] == '_') {
            end += 1;
        }

        chars[start..end].iter().collect()
    }
}

#[path = "backend/i18n.rs"]
pub mod i18n;

#[path = "backend/diagnostics.rs"]
pub mod diagnostics;

#[path = "backend/completion.rs"]
mod completion;

#[path = "backend/semantic_tokens.rs"]
mod semantic_tokens;

#[path = "backend/document_analysis.rs"]
mod document_analysis;

#[path = "backend/lsp_handlers.rs"]
mod lsp_handlers;

pub use completion::CompletionContext;
pub use diagnostics::{convert_diagnostics_to_lsp, parse_with_diagnostics};
pub use i18n::{detect_system_language, parse_language_from_args};