use std::collections::HashMap;
use super::session::{InitializeParams, LspSession};
pub struct LspPool {
sessions: HashMap<String, LspSession>,
}
impl LspPool {
pub fn new() -> Self {
Self {
sessions: HashMap::new(),
}
}
pub async fn get_or_spawn(
&mut self,
language: &str,
cmd: &str,
root_path: Option<&str>,
extra_args: &[String],
) -> Result<&mut LspSession, anyhow::Error> {
let key = pool_key(language, cmd, root_path);
if !self.sessions.contains_key(&key) {
let mut session = LspSession::spawn_with_args(cmd, extra_args)?;
let init_params = InitializeParams {
root_uri: root_path.map(|s| s.to_string()),
};
session.initialize(init_params).await?;
tracing::info!(key, "LSP session initialized");
self.sessions.insert(key.clone(), session);
}
Ok(self.sessions.get_mut(&key).unwrap())
}
pub fn get_mut_by_key(&mut self, key: &str) -> Result<&mut LspSession, anyhow::Error> {
self.sessions
.get_mut(key)
.ok_or_else(|| anyhow::anyhow!("no session for key: {key}"))
}
pub fn contains_key(&self, key: &str) -> bool {
self.sessions.contains_key(key)
}
pub fn session_keys(&self) -> Vec<String> {
self.sessions.keys().cloned().collect()
}
pub fn reap_idle(&mut self, idle_threshold: std::time::Duration) -> usize {
let now = std::time::Instant::now();
let before = self.sessions.len();
self.sessions
.retain(|_, s| now.duration_since(s.last_used_at()) < idle_threshold);
before - self.sessions.len()
}
pub fn is_empty(&self) -> bool {
self.sessions.is_empty()
}
}
#[cfg(test)]
impl LspPool {
pub fn insert_session_for_test(&mut self, key: &str, session: LspSession) {
self.sessions.insert(key.to_string(), session);
}
}
impl Default for LspPool {
fn default() -> Self {
Self::new()
}
}
pub fn pool_key(language: &str, cmd: &str, root_path: Option<&str>) -> String {
match root_path {
Some(root) => format!("{language}:{cmd}:{root}"),
None => format!("{language}:{cmd}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::mock::MockTransport;
use std::time::Duration;
fn make_pool(keys: &[&str]) -> LspPool {
let mut pool = LspPool::new();
for k in keys {
pool.insert_session_for_test(
k,
LspSession::with_transport(Box::new(MockTransport::new())),
);
}
pool
}
#[test]
fn is_empty_for_fresh_pool() {
assert!(LspPool::new().is_empty());
}
#[test]
fn reap_idle_zero_reaps_all() {
let mut pool = make_pool(&["rust:rust-analyzer:/p", "go:gopls:/p"]);
assert_eq!(pool.session_keys().len(), 2);
let reaped = pool.reap_idle(Duration::ZERO);
assert_eq!(reaped, 2);
assert!(pool.is_empty());
}
#[test]
fn reap_idle_large_threshold_keeps_all() {
let mut pool = make_pool(&["rust:rust-analyzer:/p"]);
let reaped = pool.reap_idle(Duration::from_secs(3600));
assert_eq!(reaped, 0);
assert!(!pool.is_empty());
}
#[test]
fn reap_idle_is_selective() {
let mut pool = make_pool(&["a:x:/p", "b:y:/p"]);
assert_eq!(pool.reap_idle(Duration::from_secs(3600)), 0);
assert_eq!(pool.session_keys().len(), 2);
assert_eq!(pool.reap_idle(Duration::ZERO), 2);
assert!(pool.is_empty());
}
}