use super::LspError;
use super::LspResult;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct LanguageServerConfig {
pub name: String,
pub command: String,
pub args: Vec<String>,
pub extensions: Vec<String>,
pub init_timeout_secs: u64,
}
impl LanguageServerConfig {
pub fn new(
name: impl Into<String>,
command: impl Into<String>,
args: Vec<String>,
extensions: Vec<String>,
) -> Self {
Self {
name: name.into(),
command: command.into(),
args,
extensions,
init_timeout_secs: 30,
}
}
}
#[derive(Debug)]
pub struct LanguageServer {
pub config: LanguageServerConfig,
pub process: Option<Child>,
pub root_path: PathBuf,
}
impl LanguageServer {
pub fn new(config: LanguageServerConfig, root_path: PathBuf) -> LspResult<Self> {
Ok(Self {
config,
process: None,
root_path,
})
}
pub fn start(&mut self) -> LspResult<()> {
let mut command = Command::new(&self.config.command);
command.current_dir(&self.root_path);
for arg in &self.config.args {
command.arg(arg);
}
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = command.spawn().map_err(|e| {
LspError::ServerCrashed(format!("Failed to start {}: {}", self.config.name, e))
})?;
self.process = Some(child);
tracing::info!("Started LSP server: {}", self.config.name);
Ok(())
}
pub fn stop(&mut self) -> LspResult<()> {
if let Some(mut process) = self.process.take() {
let _ = process.kill();
tracing::info!("Stopped LSP server: {}", self.config.name);
}
Ok(())
}
pub fn config(&self) -> &LanguageServerConfig {
&self.config
}
pub fn stdin(&self) -> Option<&std::process::ChildStdin> {
self.process.as_ref().and_then(|p| p.stdin.as_ref())
}
pub fn stdout(&self) -> Option<&std::process::ChildStdout> {
self.process.as_ref().and_then(|p| p.stdout.as_ref())
}
pub fn is_running(&self) -> bool {
self.process.is_some()
}
pub fn root_path(&self) -> &PathBuf {
&self.root_path
}
}
impl Drop for LanguageServer {
fn drop(&mut self) {
let _ = self.stop();
}
}
pub struct LanguageServerManager {
pub servers: Arc<Mutex<HashMap<String, LanguageServer>>>,
configs: HashMap<String, LanguageServerConfig>,
}
impl LanguageServerManager {
pub fn new() -> Self {
let mut configs = HashMap::new();
configs.insert(
"rust".to_string(),
LanguageServerConfig::new(
"rust-analyzer",
"rust-analyzer",
vec![],
vec!["rs".to_string()],
),
);
configs.insert(
"python".to_string(),
LanguageServerConfig::new(
"pyright",
"pyright-langserver",
vec!["--stdio".to_string()],
vec!["py".to_string()],
),
);
configs.insert(
"typescript".to_string(),
LanguageServerConfig::new(
"typescript-language-server",
"typescript-language-server",
vec!["--stdio".to_string()],
vec![
"ts".to_string(),
"tsx".to_string(),
"js".to_string(),
"jsx".to_string(),
],
),
);
configs.insert(
"go".to_string(),
LanguageServerConfig::new(
"gopls",
"gopls",
vec!["serve".to_string()],
vec!["go".to_string()],
),
);
configs.insert(
"cpp".to_string(),
LanguageServerConfig::new(
"clangd",
"clangd",
vec!["--background-index".to_string()],
vec![
"c".to_string(),
"cpp".to_string(),
"h".to_string(),
"hpp".to_string(),
],
),
);
Self {
servers: Arc::new(Mutex::new(HashMap::new())),
configs,
}
}
pub fn get_language_from_extension(ext: &str) -> Option<&'static str> {
match ext {
"rs" => Some("rust"),
"py" => Some("python"),
"ts" | "tsx" => Some("typescript"),
"js" | "jsx" => Some("typescript"), "go" => Some("go"),
"c" | "cpp" | "h" | "hpp" => Some("cpp"),
_ => None,
}
}
pub fn get_config(&self, language: &str) -> Option<&LanguageServerConfig> {
self.configs.get(language)
}
pub async fn start_server(&self, language: &str, root_path: PathBuf) -> LspResult<()> {
let config = self
.configs
.get(language)
.ok_or_else(|| LspError::ServerNotFound(language.to_string()))?
.clone();
let mut server = LanguageServer::new(config, root_path)?;
server.start()?;
let mut servers = self.servers.lock().await;
servers.insert(language.to_string(), server);
Ok(())
}
pub async fn get_server(&self, language: &str) -> Option<LanguageServerHandle> {
let servers = self.servers.lock().await;
servers.get(language).map(|_s| LanguageServerHandle {
language: language.to_string(),
servers: self.servers.clone(),
})
}
pub async fn stop_server(&self, language: &str) -> LspResult<()> {
let mut servers = self.servers.lock().await;
if let Some(mut server) = servers.remove(language) {
server.stop()?;
}
Ok(())
}
pub async fn stop_all(&self) -> LspResult<()> {
let mut servers = self.servers.lock().await;
for (_, mut server) in servers.drain() {
let _ = server.stop();
}
Ok(())
}
pub async fn is_running(&self, language: &str) -> bool {
let servers = self.servers.lock().await;
servers
.get(language)
.map(|s| s.is_running())
.unwrap_or(false)
}
pub fn add_custom_config(&mut self, language: String, config: LanguageServerConfig) {
self.configs.insert(language, config);
}
}
impl Default for LanguageServerManager {
fn default() -> Self {
Self::new()
}
}
pub struct LanguageServerHandle {
language: String,
servers: Arc<Mutex<HashMap<String, LanguageServer>>>,
}
impl LanguageServerHandle {
pub fn name(&self) -> String {
self.language.clone()
}
pub async fn config(&self) -> Option<LanguageServerConfig> {
let servers = self.servers.lock().await;
servers.get(&self.language).map(|s| s.config().clone())
}
pub async fn root_path(&self) -> Option<PathBuf> {
let servers = self.servers.lock().await;
servers.get(&self.language).map(|s| s.root_path().clone())
}
}
pub fn rust_analyzer_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"rust-analyzer",
"rust-analyzer",
vec!["--stdio".to_string()],
vec!["rs".to_string()],
)
}
pub fn pyright_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"pyright",
"pyright-langserver",
vec!["--stdio".to_string()],
vec!["py".to_string()],
)
}
pub fn typescript_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"typescript-language-server",
"typescript-language-server",
vec!["--stdio".to_string()],
vec![
"ts".to_string(),
"tsx".to_string(),
"js".to_string(),
"jsx".to_string(),
],
)
}
pub fn gopls_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"gopls",
"gopls",
vec!["serve".to_string(), "--stdio".to_string()],
vec!["go".to_string()],
)
}
pub fn clangd_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"clangd",
"clangd",
vec![
"--background-index".to_string(),
"--header-insertion=iwyu".to_string(),
],
vec![
"c".to_string(),
"cpp".to_string(),
"h".to_string(),
"hpp".to_string(),
],
)
}
pub fn pylance_config() -> LanguageServerConfig {
LanguageServerConfig::new(
"pylance",
"pylance",
vec!["--stdio".to_string()],
vec!["py".to_string()],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manager_creation() {
let manager = LanguageServerManager::new();
assert!(manager.get_config("rust").is_some());
assert!(manager.get_config("python").is_some());
assert!(manager.get_config("typescript").is_some());
assert!(manager.get_config("go").is_some());
assert!(manager.get_config("cpp").is_some());
}
#[test]
fn test_get_language_from_extension() {
assert_eq!(
LanguageServerManager::get_language_from_extension("rs"),
Some("rust")
);
assert_eq!(
LanguageServerManager::get_language_from_extension("py"),
Some("python")
);
assert_eq!(
LanguageServerManager::get_language_from_extension("ts"),
Some("typescript")
);
assert_eq!(
LanguageServerManager::get_language_from_extension("go"),
Some("go")
);
assert_eq!(
LanguageServerManager::get_language_from_extension("cpp"),
Some("cpp")
);
assert_eq!(
LanguageServerManager::get_language_from_extension("xyz"),
None
);
}
#[test]
fn test_config_creation() {
let config = rust_analyzer_config();
assert_eq!(config.name, "rust-analyzer");
assert_eq!(config.command, "rust-analyzer");
assert_eq!(config.extensions, vec!["rs".to_string()]);
}
}