use crate::client::LspClient;
use crate::server::{LspRegistry, LspServerInfo};
use crate::types::*;
use anyhow::{anyhow, Result};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, error, info, warn};
pub struct Lsp {
registry: LspRegistry,
clients: RwLock<HashMap<(PathBuf, String), Arc<LspClient>>>,
broken: RwLock<HashSet<(PathBuf, String)>>,
spawning: Mutex<HashMap<(PathBuf, String), tokio::sync::broadcast::Sender<()>>>,
working_dir: PathBuf,
}
impl Lsp {
pub fn new(working_dir: PathBuf) -> Self {
Self {
registry: LspRegistry::new(),
clients: RwLock::new(HashMap::new()),
broken: RwLock::new(HashSet::new()),
spawning: Mutex::new(HashMap::new()),
working_dir,
}
}
pub fn with_registry(working_dir: PathBuf, registry: LspRegistry) -> Self {
Self {
registry,
clients: RwLock::new(HashMap::new()),
broken: RwLock::new(HashSet::new()),
spawning: Mutex::new(HashMap::new()),
working_dir,
}
}
pub fn working_dir(&self) -> &Path {
&self.working_dir
}
pub async fn has_clients(&self, path: &Path) -> bool {
let path = self.resolve_path(path);
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e))
.unwrap_or_default();
for server in self.registry.servers_for_extension(&ext) {
if let Some(root) = server.detect_root(&path) {
let key = (root, server.id.clone());
let broken = self.broken.read().await;
if !broken.contains(&key) {
return true;
}
}
}
false
}
pub async fn clients_for_file(&self, path: &Path) -> Result<Vec<Arc<LspClient>>> {
let path = self.resolve_path(path);
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e))
.unwrap_or_default();
let mut result = Vec::new();
for server in self.registry.servers_for_extension(&ext) {
if let Some(client) = self.get_or_spawn_client(&path, server).await? {
result.push(client);
}
}
Ok(result)
}
pub async fn touch_file(&self, path: &Path, wait_for_diagnostics: bool) -> Result<()> {
let path = self.resolve_path(path);
let clients = self.clients_for_file(&path).await?;
for client in clients {
if let Err(e) = client.open_file(&path).await {
warn!(
server_id = %client.server_id(),
path = ?path,
error = ?e,
"Failed to open file in LSP"
);
}
}
if wait_for_diagnostics {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Ok(())
}
pub async fn hover(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<Option<Hover>>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
self.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.hover(&p, line, character).await }
})
.await
}
pub async fn definition(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<Location>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
let results = self
.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.definition(&p, line, character).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn references(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<Location>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
let results = self
.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.references(&p, line, character, true).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn implementation(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<Location>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
let results = self
.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.implementation(&p, line, character).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn document_symbols(&self, path: &Path) -> Result<Vec<DocumentSymbolResponse>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
self.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.document_symbols(&p).await }
})
.await
}
pub async fn workspace_symbols(&self, query: &str) -> Result<Vec<SymbolInformation>> {
let clients = self.clients.read().await;
let mut results = Vec::new();
for client in clients.values() {
match client.workspace_symbols(query).await {
Ok(symbols) => {
let filtered: Vec<_> = symbols
.into_iter()
.filter(|s| IMPORTANT_SYMBOL_KINDS.contains(&s.kind))
.take(10)
.collect();
results.extend(filtered);
}
Err(e) => {
warn!(
server_id = %client.server_id(),
error = ?e,
"Failed to get workspace symbols"
);
}
}
}
Ok(results)
}
pub async fn prepare_call_hierarchy(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<CallHierarchyItem>> {
let path = self.resolve_path(path);
let path_clone = path.clone();
let results = self
.run_on_file(&path, move |client| {
let p = path_clone.clone();
async move { client.prepare_call_hierarchy(&p, line, character).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn incoming_calls(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<CallHierarchyIncomingCall>> {
let path = self.resolve_path(path);
let items = self.prepare_call_hierarchy(&path, line, character).await?;
if items.is_empty() {
return Ok(vec![]);
}
let item = items.into_iter().next().unwrap();
let results = self
.run_on_file(&path, |client| {
let item = item.clone();
async move { client.incoming_calls(item).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn outgoing_calls(
&self,
path: &Path,
line: u32,
character: u32,
) -> Result<Vec<CallHierarchyOutgoingCall>> {
let path = self.resolve_path(path);
let items = self.prepare_call_hierarchy(&path, line, character).await?;
if items.is_empty() {
return Ok(vec![]);
}
let item = items.into_iter().next().unwrap();
let results = self
.run_on_file(&path, |client| {
let item = item.clone();
async move { client.outgoing_calls(item).await }
})
.await?;
Ok(results.into_iter().flatten().collect())
}
pub async fn diagnostics(&self) -> HashMap<PathBuf, Vec<Diagnostic>> {
let clients = self.clients.read().await;
let mut result = HashMap::new();
for client in clients.values() {
for (path, diags) in client.diagnostics() {
result
.entry(path)
.or_insert_with(Vec::new)
.extend(diags);
}
}
result
}
pub async fn diagnostics_for_file(&self, path: &Path) -> Vec<Diagnostic> {
let path = self.resolve_path(path);
let clients = self.clients.read().await;
let mut result = Vec::new();
for client in clients.values() {
result.extend(client.diagnostics_for_file(&path));
}
result
}
pub async fn status(&self) -> Vec<LspStatus> {
let clients = self.clients.read().await;
clients
.values()
.map(|client| LspStatus {
id: client.server_id().to_string(),
name: client.server_id().to_string(),
root: client
.root()
.strip_prefix(&self.working_dir)
.unwrap_or(client.root())
.to_string_lossy()
.to_string(),
status: LspConnectionStatus::Connected,
})
.collect()
}
pub async fn shutdown(self) {
info!("Shutting down all LSP clients");
let clients = self.clients.into_inner();
for (_, client) in clients {
if let Ok(client) = Arc::try_unwrap(client) {
client.shutdown().await;
}
}
}
fn resolve_path(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.working_dir.join(path)
}
}
async fn get_or_spawn_client(
&self,
path: &Path,
server: &LspServerInfo,
) -> Result<Option<Arc<LspClient>>> {
let root = match server.detect_root(path) {
Some(root) => root,
None => return Ok(None),
};
let key = (root.clone(), server.id.clone());
{
let broken = self.broken.read().await;
if broken.contains(&key) {
return Ok(None);
}
}
{
let clients = self.clients.read().await;
if let Some(client) = clients.get(&key) {
return Ok(Some(client.clone()));
}
}
{
let spawning = self.spawning.lock().await;
if let Some(tx) = spawning.get(&key) {
let mut rx = tx.subscribe();
drop(spawning);
let _ = rx.recv().await;
let clients = self.clients.read().await;
return Ok(clients.get(&key).cloned());
}
}
info!(server_id = %server.id, root = ?root, "Spawning LSP server");
let (tx, _) = tokio::sync::broadcast::channel(1);
{
let mut spawning = self.spawning.lock().await;
spawning.insert(key.clone(), tx.clone());
}
let result = self.spawn_client(server, &root).await;
{
let mut spawning = self.spawning.lock().await;
spawning.remove(&key);
}
let _ = tx.send(());
match result {
Ok(client) => {
let client = Arc::new(client);
let mut clients = self.clients.write().await;
clients.insert(key, client.clone());
Ok(Some(client))
}
Err(e) => {
error!(
server_id = %server.id,
root = ?root,
error = ?e,
"Failed to spawn LSP server"
);
let mut broken = self.broken.write().await;
broken.insert(key);
Ok(None)
}
}
}
async fn spawn_client(&self, server: &LspServerInfo, root: &Path) -> Result<LspClient> {
let handle = server.spawn(root)?;
LspClient::new(&server.id, handle, root.to_path_buf()).await
}
async fn run_on_file<F, Fut, T>(&self, path: &Path, f: F) -> Result<Vec<T>>
where
F: Fn(Arc<LspClient>) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let clients = self.clients_for_file(path).await?;
for client in &clients {
if let Err(e) = client.open_file(path).await {
warn!(
server_id = %client.server_id(),
path = ?path,
error = ?e,
"Failed to open file"
);
}
}
let mut results = Vec::new();
for client in clients {
match f(client.clone()).await {
Ok(result) => results.push(result),
Err(e) => {
warn!(
server_id = %client.server_id(),
error = ?e,
"LSP operation failed"
);
}
}
}
Ok(results)
}
}
use std::sync::OnceLock;
static LSP_INSTANCE: OnceLock<Lsp> = OnceLock::new();
pub fn init(working_dir: PathBuf) -> &'static Lsp {
LSP_INSTANCE.get_or_init(|| Lsp::new(working_dir))
}
pub fn lsp() -> Option<&'static Lsp> {
LSP_INSTANCE.get()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_lsp_new() {
let temp = tempdir().unwrap();
let lsp = Lsp::new(temp.path().to_path_buf());
assert_eq!(lsp.working_dir(), temp.path());
}
#[tokio::test]
async fn test_has_clients_no_root() {
let temp = tempdir().unwrap();
let lsp = Lsp::new(temp.path().to_path_buf());
let file = temp.path().join("orphan.rs");
std::fs::write(&file, "fn main() {}").unwrap();
assert!(!lsp.has_clients(&file).await);
}
#[tokio::test]
async fn test_status_empty() {
let temp = tempdir().unwrap();
let lsp = Lsp::new(temp.path().to_path_buf());
let status = lsp.status().await;
assert!(status.is_empty());
}
}