lsp-mcp 0.1.0

MCP server providing unified access to Language Server Protocol features
Documentation
use crate::error::{Language, LspMcpError, Result};
use crate::lsp::client::LspClient;
use crate::lsp::config::LanguageServerRegistry;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, info, warn};

/// Key for identifying a language server instance
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ServerKey {
    workspace: PathBuf,
    language: Language,
}

/// Manages multiple language server instances
pub struct LanguageServerManager {
    /// Map of workspace/language -> LspClient
    servers: RwLock<HashMap<ServerKey, Arc<LspClient>>>,
    /// Configuration registry
    registry: LanguageServerRegistry,
    /// Currently active workspace
    active_workspace: RwLock<Option<PathBuf>>,
}

impl LanguageServerManager {
    /// Create a new manager with default configurations
    pub fn new() -> Self {
        Self {
            servers: RwLock::new(HashMap::new()),
            registry: LanguageServerRegistry::new(),
            active_workspace: RwLock::new(None),
        }
    }

    /// Create a new manager with custom registry
    pub fn with_registry(registry: LanguageServerRegistry) -> Self {
        Self {
            servers: RwLock::new(HashMap::new()),
            registry,
            active_workspace: RwLock::new(None),
        }
    }

    /// Activate a workspace and optionally start language servers
    pub fn activate_workspace(
        &self,
        workspace_path: PathBuf,
        languages: Option<Vec<Language>>,
    ) -> Result<Vec<Language>> {
        info!("Activating workspace: {:?}", workspace_path);

        if !workspace_path.exists() {
            return Err(LspMcpError::WorkspaceNotFound(workspace_path));
        }

        // Set as active workspace
        *self.active_workspace.write() = Some(workspace_path.clone());

        // Determine which languages to start
        let languages_to_start = match languages {
            Some(langs) => langs,
            None => self.detect_languages(&workspace_path),
        };

        let mut started = Vec::new();

        for language in languages_to_start {
            match self.start_server(workspace_path.clone(), language) {
                Ok(_) => started.push(language),
                Err(e) => {
                    warn!("Failed to start {:?} server: {}", language, e);
                }
            }
        }

        Ok(started)
    }

    /// Start a language server for a specific workspace and language
    pub fn start_server(&self, workspace: PathBuf, language: Language) -> Result<()> {
        let key = ServerKey {
            workspace: workspace.clone(),
            language,
        };

        // Check if already running
        if self.servers.read().contains_key(&key) {
            debug!("Server already running for {:?} in {:?}", language, workspace);
            return Ok(());
        }

        // Get configuration
        let config = self
            .registry
            .get(language)
            .ok_or_else(|| LspMcpError::UnsupportedLanguage(language.to_string()))?;

        // Create and initialize client
        let client = LspClient::new(config, language, workspace)?;
        client.initialize()?;

        // Store client
        self.servers.write().insert(key, Arc::new(client));

        Ok(())
    }

    /// Get a client for a specific file
    pub fn get_client_for_file(&self, file_path: &Path) -> Result<Arc<LspClient>> {
        // Detect language from file extension
        let extension = file_path
            .extension()
            .and_then(|e| e.to_str())
            .ok_or_else(|| LspMcpError::UnsupportedLanguage("unknown".to_string()))?;

        let language = Language::from_extension(extension)
            .ok_or_else(|| LspMcpError::UnsupportedLanguage(extension.to_string()))?;

        // Find workspace containing this file
        let workspace = self.find_workspace_for_file(file_path)?;

        self.get_client(workspace, language)
    }

    /// Get a client for a specific workspace and language
    pub fn get_client(&self, workspace: PathBuf, language: Language) -> Result<Arc<LspClient>> {
        let key = ServerKey { workspace: workspace.clone(), language };

        self.servers
            .read()
            .get(&key)
            .cloned()
            .ok_or_else(|| LspMcpError::ServerNotRunning { language, workspace })
    }

    /// Get the active workspace
    pub fn active_workspace(&self) -> Option<PathBuf> {
        self.active_workspace.read().clone()
    }

    /// List all active workspaces and their servers
    pub fn list_workspaces(&self) -> Vec<WorkspaceInfo> {
        let servers = self.servers.read();
        let mut workspaces: HashMap<PathBuf, Vec<Language>> = HashMap::new();

        for key in servers.keys() {
            workspaces
                .entry(key.workspace.clone())
                .or_default()
                .push(key.language);
        }

        workspaces
            .into_iter()
            .map(|(path, languages)| WorkspaceInfo { path, languages })
            .collect()
    }

    /// Stop all servers for a workspace
    pub fn deactivate_workspace(&self, workspace: &Path) -> Result<()> {
        info!("Deactivating workspace: {:?}", workspace);

        let mut servers = self.servers.write();
        let keys_to_remove: Vec<ServerKey> = servers
            .keys()
            .filter(|k| k.workspace == workspace)
            .cloned()
            .collect();

        for key in keys_to_remove {
            if let Some(client) = servers.remove(&key) {
                if let Err(e) = client.shutdown() {
                    warn!("Error shutting down {:?} server: {}", key.language, e);
                }
            }
        }

        // Clear active workspace if it matches
        let mut active = self.active_workspace.write();
        if active.as_ref() == Some(&workspace.to_path_buf()) {
            *active = None;
        }

        Ok(())
    }

    /// Shutdown all language servers
    pub fn shutdown_all(&self) -> Result<()> {
        info!("Shutting down all language servers");

        let mut servers = self.servers.write();
        for (key, client) in servers.drain() {
            if let Err(e) = client.shutdown() {
                warn!("Error shutting down {:?} server: {}", key.language, e);
            }
        }

        *self.active_workspace.write() = None;

        Ok(())
    }

    /// Detect languages in a workspace by scanning for project files
    fn detect_languages(&self, workspace: &Path) -> Vec<Language> {
        let mut detected = Vec::new();

        // Check for Rust
        if workspace.join("Cargo.toml").exists() {
            detected.push(Language::Rust);
        }

        // Check for TypeScript/JavaScript
        if workspace.join("package.json").exists()
            || workspace.join("tsconfig.json").exists()
        {
            detected.push(Language::TypeScript);
        }

        // Check for Python
        if workspace.join("pyproject.toml").exists()
            || workspace.join("setup.py").exists()
            || workspace.join("requirements.txt").exists()
        {
            detected.push(Language::Python);
        }

        // Check for Go
        if workspace.join("go.mod").exists() {
            detected.push(Language::Go);
        }

        // Check for C/C++
        if workspace.join("CMakeLists.txt").exists()
            || workspace.join("compile_commands.json").exists()
        {
            detected.push(Language::Cpp);
        }

        debug!("Detected languages in {:?}: {:?}", workspace, detected);
        detected
    }

    /// Find the workspace containing a file
    fn find_workspace_for_file(&self, file_path: &Path) -> Result<PathBuf> {
        // First check active workspace
        if let Some(active) = self.active_workspace.read().as_ref() {
            if file_path.starts_with(active) {
                return Ok(active.clone());
            }
        }

        // Check all registered workspaces
        let servers = self.servers.read();
        for key in servers.keys() {
            if file_path.starts_with(&key.workspace) {
                return Ok(key.workspace.clone());
            }
        }

        // Try to find a workspace root by looking for project files
        let mut current = file_path.parent();
        while let Some(dir) = current {
            if dir.join("Cargo.toml").exists()
                || dir.join("package.json").exists()
                || dir.join("pyproject.toml").exists()
                || dir.join("go.mod").exists()
                || dir.join(".git").exists()
            {
                return Ok(dir.to_path_buf());
            }
            current = dir.parent();
        }

        Err(LspMcpError::NoActiveWorkspace)
    }
}

impl Default for LanguageServerManager {
    fn default() -> Self {
        Self::new()
    }
}

/// Information about an active workspace
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
    pub path: PathBuf,
    pub languages: Vec<Language>,
}