use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Result;
use tokio::sync::{mpsc, RwLock};
use super::client::LspClient;
use super::registry::LspServerRegistry;
use super::types::Diagnostic;
use crate::config::LspConfig;
#[derive(Debug, Clone)]
pub enum LspConnectEvent {
Started { command: String, ext: String },
Failed {
command: String,
ext: String,
error: String,
},
Warning { ext: String, message: String },
}
fn extension_to_language_id(ext: &str) -> &str {
match ext {
"rs" => "rust",
"ts" => "typescript",
"tsx" => "typescriptreact",
"js" => "javascript",
"jsx" => "javascriptreact",
"py" => "python",
"go" => "go",
"java" => "java",
"c" => "c",
"cpp" | "cc" | "cxx" => "cpp",
"cs" => "csharp",
"rb" => "ruby",
"php" => "php",
"swift" => "swift",
"kt" | "kts" => "kotlin",
"scala" => "scala",
_ => ext,
}
}
pub struct LspManager {
clients: Arc<RwLock<HashMap<String, Arc<LspClient>>>>,
registry: LspServerRegistry,
project_root: PathBuf,
enabled: bool,
diagnostics_settle_delay_ms: u64,
connect_events: Option<mpsc::UnboundedSender<LspConnectEvent>>,
}
impl LspManager {
pub fn new(
project_root: PathBuf,
registry: LspServerRegistry,
enabled: bool,
diagnostics_settle_delay_ms: u64,
) -> Self {
Self {
clients: Arc::new(RwLock::new(HashMap::new())),
registry,
project_root,
enabled,
diagnostics_settle_delay_ms,
connect_events: None,
}
}
pub fn with_event_channel(
project_root: PathBuf,
registry: LspServerRegistry,
enabled: bool,
diagnostics_settle_delay_ms: u64,
) -> (Self, mpsc::UnboundedReceiver<LspConnectEvent>) {
let (tx, rx) = mpsc::unbounded_channel();
let mgr = Self {
clients: Arc::new(RwLock::new(HashMap::new())),
registry,
project_root,
enabled,
diagnostics_settle_delay_ms,
connect_events: Some(tx),
};
(mgr, rx)
}
fn emit(&self, event: LspConnectEvent) {
if let Some(tx) = &self.connect_events {
let _ = tx.send(event);
}
}
pub fn diagnostics_settle_delay_ms(&self) -> u64 {
self.diagnostics_settle_delay_ms
}
pub async fn ensure_server(&self, file_path: &Path) -> Result<bool> {
if !self.enabled {
return Ok(false);
}
let ext = match file_path.extension().and_then(|e| e.to_str()) {
Some(e) => e.to_string(),
None => return Ok(false),
};
{
let clients = self.clients.read().await;
if clients.contains_key(&ext) {
return Ok(true);
}
}
let config = match self.registry.get(&ext) {
Some(c) => c.clone(),
None => return Ok(false),
};
if which::which(&config.command).is_err() {
return Ok(false);
}
let language_id = extension_to_language_id(&ext);
let mut clients = self.clients.write().await;
if clients.contains_key(&ext) {
return Ok(true);
}
match LspClient::start(&config, &self.project_root, language_id).await {
Ok(client) => {
let arc = Arc::new(client);
clients.insert(ext.clone(), arc);
self.emit(LspConnectEvent::Started {
command: config.command.clone(),
ext,
});
Ok(true)
}
Err(e) => {
self.emit(LspConnectEvent::Failed {
command: config.command.clone(),
ext,
error: e.to_string(),
});
Ok(false)
}
}
}
pub async fn diagnostics(&self, path: &Path) -> Vec<Diagnostic> {
let ext = match path.extension().and_then(|e| e.to_str()) {
Some(e) => e.to_string(),
None => return Vec::new(),
};
let clients = self.clients.read().await;
match clients.get(&ext) {
Some(client) => client.diagnostics(path).await,
None => Vec::new(),
}
}
pub async fn all_diagnostics(&self) -> Vec<Diagnostic> {
let clients = self.clients.read().await;
let mut all = Vec::new();
for client in clients.values() {
all.extend(client.all_diagnostics().await);
}
all
}
pub async fn notify_file_changed(&self, path: &Path, content: &str) -> Result<bool> {
if !self.ensure_server(path).await? {
return Ok(false);
}
let ext = match path.extension().and_then(|e| e.to_str()) {
Some(e) => e.to_string(),
None => return Ok(false),
};
let clients = self.clients.read().await;
if let Some(client) = clients.get(&ext) {
let language_id = extension_to_language_id(&ext);
client.sync_document(path, content, language_id).await?;
return Ok(true);
}
Ok(false)
}
pub async fn active_servers(&self) -> Vec<String> {
let clients = self.clients.read().await;
let mut exts: Vec<String> = clients.keys().cloned().collect();
exts.sort();
exts
}
pub async fn shutdown(&self) {
let mut clients = self.clients.write().await;
for (ext, client) in clients.drain() {
if let Err(e) = client.shutdown().await {
self.emit(LspConnectEvent::Warning {
ext,
message: format!("shutdown error: {}", e),
});
}
}
}
}
pub fn build_lsp_manager(config: &LspConfig, project_root: &Path) -> Option<Arc<LspManager>> {
if !config.enabled {
return None;
}
let registry = build_registry(config);
let manager = LspManager::new(
project_root.to_path_buf(),
registry,
true,
config.diagnostics_settle_delay_ms,
);
Some(Arc::new(manager))
}
pub fn build_lsp_manager_with_events(
config: &LspConfig,
project_root: &Path,
) -> Option<(Arc<LspManager>, mpsc::UnboundedReceiver<LspConnectEvent>)> {
if !config.enabled {
return None;
}
let registry = build_registry(config);
let (manager, rx) = LspManager::with_event_channel(
project_root.to_path_buf(),
registry,
true,
config.diagnostics_settle_delay_ms,
);
Some((Arc::new(manager), rx))
}
fn build_registry(config: &LspConfig) -> LspServerRegistry {
let mut registry = if config.auto_detect {
LspServerRegistry::with_defaults()
} else {
LspServerRegistry::empty()
};
registry.merge_user_config(config.servers.clone());
registry
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extension_to_language_id_maps_common_langs() {
assert_eq!(extension_to_language_id("rs"), "rust");
assert_eq!(extension_to_language_id("ts"), "typescript");
assert_eq!(extension_to_language_id("tsx"), "typescriptreact");
assert_eq!(extension_to_language_id("py"), "python");
assert_eq!(extension_to_language_id("go"), "go");
assert_eq!(extension_to_language_id("java"), "java");
assert_eq!(extension_to_language_id("js"), "javascript");
}
#[test]
fn extension_to_language_id_unknown_returns_self() {
assert_eq!(extension_to_language_id("xyz"), "xyz");
}
#[tokio::test]
async fn disabled_manager_returns_false() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, false, 150);
let result = mgr.ensure_server(Path::new("test.rs")).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn no_config_for_extension_returns_false() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
let result = mgr.ensure_server(Path::new("test.xyz")).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn no_extension_returns_false() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
let result = mgr.ensure_server(Path::new("Makefile")).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn empty_diagnostics_for_unknown_file() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
let diags = mgr.diagnostics(Path::new("test.xyz")).await;
assert!(diags.is_empty());
}
#[tokio::test]
async fn active_servers_empty_initially() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
assert!(mgr.active_servers().await.is_empty());
}
#[tokio::test]
async fn all_diagnostics_empty_initially() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
assert!(mgr.all_diagnostics().await.is_empty());
}
#[tokio::test]
async fn shutdown_on_empty_is_noop() {
let registry = LspServerRegistry::with_defaults();
let mgr = LspManager::new(PathBuf::from("/tmp"), registry, true, 150);
mgr.shutdown().await;
}
#[test]
fn build_lsp_manager_returns_none_when_disabled() {
let config = LspConfig {
enabled: false,
auto_detect: true,
servers: Default::default(),
diagnostics_settle_delay_ms: 150,
};
let result = build_lsp_manager(&config, Path::new("/tmp"));
assert!(result.is_none());
}
#[test]
fn build_lsp_manager_returns_some_when_enabled() {
let config = LspConfig {
enabled: true,
auto_detect: true,
servers: Default::default(),
diagnostics_settle_delay_ms: 150,
};
let result = build_lsp_manager(&config, Path::new("/tmp"));
assert!(result.is_some());
}
#[test]
fn build_lsp_manager_respects_auto_detect() {
let config = LspConfig {
enabled: true,
auto_detect: false,
servers: Default::default(),
diagnostics_settle_delay_ms: 150,
};
let result = build_lsp_manager(&config, Path::new("/tmp"));
assert!(result.is_some());
}
#[test]
fn build_lsp_manager_merges_user_servers() {
let mut servers = std::collections::HashMap::new();
servers.insert(
"xyz".to_string(),
super::super::registry::LspServerConfig {
command: "my-lsp".to_string(),
args: vec![],
root_markers: vec![],
},
);
let config = LspConfig {
enabled: true,
auto_detect: true,
servers,
diagnostics_settle_delay_ms: 150,
};
let result = build_lsp_manager(&config, Path::new("/tmp"));
assert!(result.is_some());
}
#[tokio::test]
async fn with_event_channel_yields_empty_receiver_initially() {
let registry = LspServerRegistry::with_defaults();
let (mgr, mut rx) =
LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
assert!(mgr.active_servers().await.is_empty());
assert!(rx.try_recv().is_err(), "no events expected before any ensure_server call");
}
#[tokio::test]
async fn ensure_server_silent_when_command_missing() {
let mut servers = std::collections::HashMap::new();
servers.insert(
"xyz".to_string(),
super::super::registry::LspServerConfig {
command: "atomcode-lsp-does-not-exist".to_string(),
args: vec![],
root_markers: vec![],
},
);
let mut registry = LspServerRegistry::empty();
registry.merge_user_config(servers);
let (mgr, mut rx) =
LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
let result = mgr.ensure_server(Path::new("test.xyz")).await.unwrap();
assert!(!result, "missing command must return Ok(false)");
assert!(rx.try_recv().is_err(), "no event expected for missing command");
}
#[tokio::test]
async fn emit_no_op_when_receiver_dropped() {
let registry = LspServerRegistry::with_defaults();
let (mgr, rx) =
LspManager::with_event_channel(PathBuf::from("/tmp"), registry, true, 150);
drop(rx);
mgr.emit(LspConnectEvent::Warning {
ext: "rs".to_string(),
message: "synthetic post-drop emit".to_string(),
});
}
}