use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use super::ServerContextSnapshot;
use super::workspace_state::WorkspaceState;
use crate::handlers::ClientConfig;
const CONFIG_RELOAD_DELAY: Duration = Duration::from_secs(2);
pub struct WorkspaceManager {
config_reload_token: Arc<PendingTask>,
reload_lock: Arc<AsyncMutex<()>>,
reload_generation: Arc<AtomicU64>,
workspace_version: Arc<AtomicI64>,
pub state: WorkspaceState,
}
impl WorkspaceManager {
pub fn new() -> Self {
Self {
config_reload_token: Arc::new(PendingTask::default()),
reload_lock: Arc::new(AsyncMutex::new(())),
reload_generation: Arc::new(AtomicU64::new(0)),
workspace_version: Arc::new(AtomicI64::new(0)),
state: WorkspaceState::new(ClientConfig::default()),
}
}
pub fn set_roots(&mut self, roots: Vec<PathBuf>) {
if roots.len() > 1 {
log::warn!(
"{} workspace folders were opened; analysing the first one until multi-root is implemented",
roots.len()
);
}
self.state.set_roots(roots);
}
pub fn root(&self) -> Option<&std::path::Path> {
self.state.root()
}
pub fn set_client_config(&mut self, client_config: ClientConfig) {
self.state.set_client_config(client_config);
}
pub fn workspace_version(&self) -> i64 {
self.workspace_version.load(Ordering::Acquire)
}
pub fn is_configuration_file(&self, path: &std::path::Path) -> bool {
self.state
.watch_filter()
.is_some_and(|filter| filter.is_configuration(path))
}
pub async fn add_update_config_task(
&self,
context: ServerContextSnapshot,
config_path: PathBuf,
) {
if !self.is_configuration_file(&config_path) {
return;
}
let (cancel_token, cancelled_existing) =
self.config_reload_token.replace(CONFIG_RELOAD_DELAY).await;
if cancelled_existing {
log::debug!("a reload was already pending; waiting for the events to stop");
}
let config_reload_token = self.config_reload_token.clone();
let handles = self.reload_task_handles();
tokio::spawn(async move {
cancel_token.wait().await;
if cancel_token.is_cancelled() {
config_reload_token.clear(&cancel_token).await;
return;
}
log::info!("reloading the workspace: {} changed", config_path.display());
spawn_workspace_reload_task(handles, context);
config_reload_token.clear(&cancel_token).await;
});
}
pub fn add_reload_workspace_task(&self, context: ServerContextSnapshot) {
spawn_workspace_reload_task(self.reload_task_handles(), context);
}
pub async fn clear_workspace(&self) {
self.workspace_version.fetch_add(1, Ordering::AcqRel);
}
fn reload_task_handles(&self) -> ReloadTaskHandles {
ReloadTaskHandles {
reload_lock: self.reload_lock.clone(),
reload_generation: self.reload_generation.clone(),
workspace_version: self.workspace_version.clone(),
}
}
}
impl Deref for WorkspaceManager {
type Target = WorkspaceState;
fn deref(&self) -> &Self::Target {
&self.state
}
}
impl DerefMut for WorkspaceManager {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.state
}
}
#[derive(Clone)]
struct ReloadTaskHandles {
reload_lock: Arc<AsyncMutex<()>>,
reload_generation: Arc<AtomicU64>,
workspace_version: Arc<AtomicI64>,
}
fn spawn_workspace_reload_task(handles: ReloadTaskHandles, context: ServerContextSnapshot) {
let generation = handles.reload_generation.fetch_add(1, Ordering::AcqRel) + 1;
tokio::spawn(async move {
let _reload_guard = handles.reload_lock.lock().await;
if generation != handles.reload_generation.load(Ordering::Acquire) {
return;
}
{
let workspace_manager = context.workspace_manager().lock().await;
workspace_manager.clear_workspace().await;
}
crate::handlers::start_analysis(context.clone()).await;
handles.workspace_version.fetch_add(1, Ordering::AcqRel);
});
}
#[derive(Debug)]
struct DebounceToken {
cancelled: CancellationToken,
quiet: Duration,
}
impl DebounceToken {
fn new(quiet: Duration) -> Self {
Self {
cancelled: CancellationToken::new(),
quiet,
}
}
async fn wait(&self) {
let _ = tokio::time::timeout(self.quiet, self.cancelled.cancelled()).await;
}
fn cancel(&self) {
self.cancelled.cancel();
}
fn is_cancelled(&self) -> bool {
self.cancelled.is_cancelled()
}
}
#[derive(Debug, Default)]
struct PendingTask(AsyncMutex<Option<Arc<DebounceToken>>>);
impl PendingTask {
async fn replace(&self, delay: Duration) -> (Arc<DebounceToken>, bool) {
let mut current = self.0.lock().await;
let had_existing = current.is_some();
if let Some(token) = current.as_ref() {
token.cancel();
}
let next = Arc::new(DebounceToken::new(delay));
current.replace(next.clone());
(next, had_existing)
}
async fn clear(&self, finished: &Arc<DebounceToken>) {
let mut current = self.0.lock().await;
if current
.as_ref()
.is_some_and(|token| Arc::ptr_eq(token, finished))
{
current.take();
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::{DebounceToken, PendingTask};
#[tokio::test]
async fn a_superseded_wait_ends_immediately_and_says_so() {
let pending = PendingTask::default();
let (first, _) = pending.replace(Duration::from_secs(60)).await;
let started = Instant::now();
let (second, replaced) = pending.replace(Duration::from_secs(60)).await;
first.wait().await;
assert!(replaced, "the caller is told that it superseded a wait");
assert!(
first.is_cancelled(),
"the superseded wait knows it must not reload"
);
assert!(!second.is_cancelled());
assert!(started.elapsed() < Duration::from_secs(1));
}
#[tokio::test]
async fn an_unsuperseded_wait_finishes_after_its_quiet_period() {
let token = DebounceToken::new(Duration::from_millis(20));
let started = Instant::now();
token.wait().await;
assert!(!token.is_cancelled());
assert!(
started.elapsed() >= Duration::from_millis(20),
"the quiet period is what the reload waits for"
);
}
#[tokio::test]
async fn clearing_a_stale_token_leaves_the_newer_one_pending() {
let pending = PendingTask::default();
let (first, _) = pending.replace(Duration::from_millis(1)).await;
let (second, _) = pending.replace(Duration::from_millis(1)).await;
assert!(!second.is_cancelled());
pending.clear(&first).await;
let (third, had_existing) = pending.replace(Duration::from_millis(1)).await;
assert!(
had_existing,
"the newer token was still pending, so replacing it found one"
);
assert!(
second.is_cancelled(),
"and it is the one that got cancelled"
);
assert!(!third.is_cancelled());
}
}