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};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ServerKey {
workspace: PathBuf,
language: Language,
}
pub struct LanguageServerManager {
servers: RwLock<HashMap<ServerKey, Arc<LspClient>>>,
registry: LanguageServerRegistry,
active_workspace: RwLock<Option<PathBuf>>,
}
impl LanguageServerManager {
pub fn new() -> Self {
Self {
servers: RwLock::new(HashMap::new()),
registry: LanguageServerRegistry::new(),
active_workspace: RwLock::new(None),
}
}
pub fn with_registry(registry: LanguageServerRegistry) -> Self {
Self {
servers: RwLock::new(HashMap::new()),
registry,
active_workspace: RwLock::new(None),
}
}
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));
}
*self.active_workspace.write() = Some(workspace_path.clone());
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)
}
pub fn start_server(&self, workspace: PathBuf, language: Language) -> Result<()> {
let key = ServerKey {
workspace: workspace.clone(),
language,
};
if self.servers.read().contains_key(&key) {
debug!("Server already running for {:?} in {:?}", language, workspace);
return Ok(());
}
let config = self
.registry
.get(language)
.ok_or_else(|| LspMcpError::UnsupportedLanguage(language.to_string()))?;
let client = LspClient::new(config, language, workspace)?;
client.initialize()?;
self.servers.write().insert(key, Arc::new(client));
Ok(())
}
pub fn get_client_for_file(&self, file_path: &Path) -> Result<Arc<LspClient>> {
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()))?;
let workspace = self.find_workspace_for_file(file_path)?;
self.get_client(workspace, 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 })
}
pub fn active_workspace(&self) -> Option<PathBuf> {
self.active_workspace.read().clone()
}
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()
}
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);
}
}
}
let mut active = self.active_workspace.write();
if active.as_ref() == Some(&workspace.to_path_buf()) {
*active = None;
}
Ok(())
}
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(())
}
fn detect_languages(&self, workspace: &Path) -> Vec<Language> {
let mut detected = Vec::new();
if workspace.join("Cargo.toml").exists() {
detected.push(Language::Rust);
}
if workspace.join("package.json").exists()
|| workspace.join("tsconfig.json").exists()
{
detected.push(Language::TypeScript);
}
if workspace.join("pyproject.toml").exists()
|| workspace.join("setup.py").exists()
|| workspace.join("requirements.txt").exists()
{
detected.push(Language::Python);
}
if workspace.join("go.mod").exists() {
detected.push(Language::Go);
}
if workspace.join("CMakeLists.txt").exists()
|| workspace.join("compile_commands.json").exists()
{
detected.push(Language::Cpp);
}
debug!("Detected languages in {:?}: {:?}", workspace, detected);
detected
}
fn find_workspace_for_file(&self, file_path: &Path) -> Result<PathBuf> {
if let Some(active) = self.active_workspace.read().as_ref() {
if file_path.starts_with(active) {
return Ok(active.clone());
}
}
let servers = self.servers.read();
for key in servers.keys() {
if file_path.starts_with(&key.workspace) {
return Ok(key.workspace.clone());
}
}
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()
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
pub path: PathBuf,
pub languages: Vec<Language>,
}