coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! LSP manager for handling multiple language servers

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;

use super::client::LspClient;
use super::config::LspConfig;
use super::types::{
    LspError, LspDiagnostic, Position, Range, Location,
    CompletionItem, Hover, CodeAction, SymbolInformation
};
use super::LspService;
use async_trait::async_trait;

/// Manager for multiple LSP clients
pub struct LspManager {
    config: LspConfig,
    clients: Arc<RwLock<HashMap<String, LspClient>>>,
    workspace_root: Option<PathBuf>,
}

impl LspManager {
    /// Create a new LSP manager
    pub fn new(config: LspConfig) -> Self {
        Self {
            config,
            clients: Arc::new(RwLock::new(HashMap::new())),
            workspace_root: None,
        }
    }
    
    /// Initialize the LSP manager with a workspace root
    pub async fn initialize(&mut self, workspace_root: Option<PathBuf>) -> Result<(), LspError> {
        self.workspace_root = workspace_root.clone();
        
        if !self.config.enabled {
            tracing::info!("LSP integration is disabled");
            return Ok(());
        }
        
        // Start auto-start servers
        for (name, server_config) in &self.config.servers {
            if server_config.enabled && server_config.auto_start {
                if let Err(e) = self.start_server(name).await {
                    tracing::warn!("Failed to start LSP server {}: {}", name, e);
                    // Continue with other servers even if one fails
                }
            }
        }
        
        tracing::info!("LSP manager initialized with {} servers", self.config.servers.len());
        Ok(())
    }
    
    /// Start a specific LSP server
    pub async fn start_server(&self, server_name: &str) -> Result<(), LspError> {
        let server_config = self.config.servers.get(server_name)
            .ok_or_else(|| LspError::ServerNotFound(server_name.to_string()))?
            .clone();
        
        if !server_config.enabled {
            return Err(LspError::ServerNotFound(format!("Server {} is disabled", server_name)));
        }
        
        let mut client = LspClient::new(server_config);
        client.start(self.workspace_root.clone()).await?;
        
        let mut clients = self.clients.write().await;
        clients.insert(server_name.to_string(), client);
        
        tracing::info!("Started LSP server: {}", server_name);
        Ok(())
    }
    
    /// Stop a specific LSP server
    pub async fn stop_server(&self, server_name: &str) -> Result<(), LspError> {
        let mut clients = self.clients.write().await;
        if let Some(mut client) = clients.remove(server_name) {
            client.stop().await?;
            tracing::info!("Stopped LSP server: {}", server_name);
        }
        Ok(())
    }
    
    /// Stop all LSP servers
    pub async fn stop_all(&self) -> Result<(), LspError> {
        let mut clients = self.clients.write().await;
        for (name, mut client) in clients.drain() {
            if let Err(e) = client.stop().await {
                tracing::warn!("Failed to stop LSP server {}: {}", name, e);
            }
        }
        tracing::info!("Stopped all LSP servers");
        Ok(())
    }
    
    /// Find the appropriate LSP client for a file
    async fn find_client_for_file(&self, file_path: &Path) -> Option<String> {
        let clients = self.clients.read().await;
        
        for (name, client) in clients.iter() {
            if client.handles_file(file_path) {
                return Some(name.clone());
            }
        }
        
        None
    }
    
    /// Get or start the appropriate LSP client for a file
    async fn get_or_start_client(&self, file_path: &Path) -> Result<Option<String>, LspError> {
        // First check if we already have a client for this file
        if let Some(client_name) = self.find_client_for_file(file_path).await {
            return Ok(Some(client_name));
        }
        
        // Try to find a server configuration that handles this file
        for (name, server_config) in &self.config.servers {
            if server_config.handles_file(file_path) && server_config.enabled {
                // Try to start this server
                if let Err(e) = self.start_server(name).await {
                    tracing::warn!("Failed to start LSP server {} for file {}: {}", 
                        name, file_path.display(), e);
                    continue;
                }
                return Ok(Some(name.clone()));
            }
        }
        
        Ok(None)
    }
    
    /// Get list of active servers
    pub async fn active_servers(&self) -> Vec<String> {
        let clients = self.clients.read().await;
        clients.keys().cloned().collect()
    }

    /// Get code completions at a position
    pub async fn get_completions(&self, file_path: &Path, position: Position) -> Result<Vec<CompletionItem>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.get_completions(file_path, position).await;
            }
        }
        Ok(Vec::new())
    }

    /// Go to definition of symbol at position
    pub async fn goto_definition(&self, file_path: &Path, position: Position) -> Result<Vec<Location>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.goto_definition(file_path, position).await;
            }
        }
        Ok(Vec::new())
    }

    /// Get hover information at position
    pub async fn get_hover(&self, file_path: &Path, position: Position) -> Result<Option<Hover>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.get_hover(file_path, position).await;
            }
        }
        Ok(None)
    }

    /// Find references to symbol at position
    pub async fn find_references(&self, file_path: &Path, position: Position, include_declaration: bool) -> Result<Vec<Location>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.find_references(file_path, position, include_declaration).await;
            }
        }
        Ok(Vec::new())
    }

    /// Get code actions for a range
    pub async fn get_code_actions(&self, file_path: &Path, range: Range) -> Result<Vec<CodeAction>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.get_code_actions(file_path, range).await;
            }
        }
        Ok(Vec::new())
    }

    /// Get document symbols
    pub async fn get_document_symbols(&self, file_path: &Path) -> Result<Vec<SymbolInformation>, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.get_document_symbols(file_path).await;
            }
        }
        Ok(Vec::new())
    }

    /// Format document
    pub async fn format_document(&self, file_path: &Path) -> Result<String, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.format_document(file_path).await;
            }
        }
        Err(LspError::ServerNotFound(format!("No LSP server for {}", file_path.display())))
    }

    /// Format range in document
    pub async fn format_range(&self, file_path: &Path, range: Range) -> Result<String, LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.format_range(file_path, range).await;
            }
        }
        Err(LspError::ServerNotFound(format!("No LSP server for {}", file_path.display())))
    }

    /// Check if LSP is available for a file type
    pub fn has_server_for_file(&self, file_path: &Path) -> bool {
        self.config.servers.values()
            .any(|config| config.handles_file(file_path) && config.enabled)
    }
}

#[async_trait]
impl LspService for LspManager {
    /// Get diagnostics for a file
    async fn get_diagnostics(&self, file_path: &Path) -> Result<Vec<LspDiagnostic>, LspError> {
        if let Some(client_name) = self.find_client_for_file(file_path).await {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.get_diagnostics(file_path).await;
            }
        }
        
        // No client available for this file
        Ok(Vec::new())
    }
    
    /// Notify LSP of file changes
    async fn did_change_file(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.did_change(file_path, content).await;
            }
        }
        
        // No client available - this is not an error
        Ok(())
    }
    
    /// Notify LSP of file open
    async fn did_open_file(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
        if let Some(client_name) = self.get_or_start_client(file_path).await? {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.did_open(file_path, content).await;
            }
        }
        
        // No client available - this is not an error
        Ok(())
    }
    
    /// Notify LSP of file close
    async fn did_close_file(&self, file_path: &Path) -> Result<(), LspError> {
        if let Some(client_name) = self.find_client_for_file(file_path).await {
            let clients = self.clients.read().await;
            if let Some(client) = clients.get(&client_name) {
                return client.did_close(file_path).await;
            }
        }
        
        // No client available - this is not an error
        Ok(())
    }
    
    /// Check if LSP is available for a file type
    fn supports_file(&self, file_path: &Path) -> bool {
        self.has_server_for_file(file_path)
    }

    async fn get_completions(&self, file_path: &Path, position: Position) -> Result<Vec<CompletionItem>, LspError> {
        self.get_completions(file_path, position).await
    }

    async fn goto_definition(&self, file_path: &Path, position: Position) -> Result<Vec<Location>, LspError> {
        self.goto_definition(file_path, position).await
    }

    async fn get_hover(&self, file_path: &Path, position: Position) -> Result<Option<Hover>, LspError> {
        self.get_hover(file_path, position).await
    }

    async fn find_references(&self, file_path: &Path, position: Position, include_declaration: bool) -> Result<Vec<Location>, LspError> {
        self.find_references(file_path, position, include_declaration).await
    }

    async fn get_code_actions(&self, file_path: &Path, range: Range) -> Result<Vec<CodeAction>, LspError> {
        self.get_code_actions(file_path, range).await
    }

    async fn get_document_symbols(&self, file_path: &Path) -> Result<Vec<SymbolInformation>, LspError> {
        self.get_document_symbols(file_path).await
    }

    async fn format_document(&self, file_path: &Path) -> Result<String, LspError> {
        self.format_document(file_path).await
    }

    async fn format_range(&self, file_path: &Path, range: Range) -> Result<String, LspError> {
        self.format_range(file_path, range).await
    }
}

impl Drop for LspManager {
    fn drop(&mut self) {
        // Stop all servers when the manager is dropped
        let clients = self.clients.clone();
        tokio::spawn(async move {
            let mut clients = clients.write().await;
            for (name, mut client) in clients.drain() {
                if let Err(e) = client.stop().await {
                    tracing::warn!("Failed to stop LSP server {} in drop: {}", name, e);
                }
            }
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;
    
    #[tokio::test]
    async fn test_manager_creation() {
        let config = LspConfig::default();
        let manager = LspManager::new(config);
        
        assert!(manager.active_servers().await.is_empty());
    }
    
    #[tokio::test]
    async fn test_file_support_detection() {
        let config = LspConfig::default();
        let manager = LspManager::new(config);
        
        assert!(manager.supports_file(&PathBuf::from("main.rs")));
        assert!(manager.supports_file(&PathBuf::from("main.go")));
        assert!(!manager.supports_file(&PathBuf::from("unknown.xyz")));
    }
    
    #[tokio::test]
    async fn test_client_finding() {
        let config = LspConfig::default();
        let manager = LspManager::new(config);
        
        // No clients started yet
        assert!(manager.find_client_for_file(&PathBuf::from("main.rs")).await.is_none());
    }
}