//! Periodically shuts down idle LSP servers. //! //! Every `SCAN_INTERVAL` this singleton scans for workspaces that have a running LSP server //! but no associated: //! 1. Active local `TerminalView` whose repo root matches the workspace root. //! 2. Open `LocalCodeEditorView` with a file that belongs to that workspace root. //! //! For those workspaces, we call `LspManagerModel::stop` to tear down the LSP server. use std::path::{Path, PathBuf}; use std::time::Duration; use futures::stream::AbortHandle; use galaxyui::r#async::Timer; use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity}; use lsp::LspManagerModel; use crate::code::local_code_editor::LocalCodeEditorView; use crate::terminal::TerminalView; const SCAN_INTERVAL: Duration = Duration::from_secs(10); pub struct LanguageServerShutdownManager { in_progress_scan: Option, } impl LanguageServerShutdownManager { pub fn new() -> Self { Self { in_progress_scan: None, } } pub fn has_in_progress_scan(&self) -> bool { self.in_progress_scan.is_some() } pub fn schedule_next_scan(&mut self, ctx: &mut ModelContext) { if let Some(scan) = self.in_progress_scan.take() { scan.abort(); } self.in_progress_scan = Some( ctx.spawn( async { Timer::after(SCAN_INTERVAL).await; }, |me, _, ctx| { if me.scan_and_shutdown_unused_servers(ctx) { me.schedule_next_scan(ctx); } else { me.in_progress_scan = None; } }, ) .abort_handle(), ); } /// Scans for unused LSP servers and shuts them down. /// /// Returns `true` if there are still active workspace roots remaining (indicating more scans /// may be needed), or `false` if all workspace roots were shut down or there were no roots. fn scan_and_shutdown_unused_servers(&self, ctx: &mut ModelContext) -> bool { let lsp_manager_handle = LspManagerModel::handle(ctx); let workspace_roots: Vec = lsp_manager_handle .as_ref(ctx) .workspace_roots() .cloned() .collect(); if workspace_roots.is_empty() { return false; } let mut unused_roots = Vec::new(); for root in &workspace_roots { if !workspace_root_in_use(root, ctx) { unused_roots.push(root); } } // There will be remaining active roots if not all of the workspace roots are unused. let has_active_roots = unused_roots.len() < workspace_roots.len(); if unused_roots.is_empty() { return has_active_roots; } // Stop servers for all workspaces that are no longer in use. lsp_manager_handle.update(ctx, |manager, m_ctx| { for root in unused_roots { log::info!("Stopping unused LSP for workspace {}", root.display()); manager.stop_all(root.clone(), m_ctx); } }); has_active_roots } } fn workspace_root_in_use(root: &Path, app: &AppContext) -> bool { has_terminal_for_workspace(root, app) || has_open_file_for_workspace(root, app) } fn has_terminal_for_workspace(root: &Path, app: &AppContext) -> bool { for window_id in app.window_ids() { if let Some(terminals) = app.views_of_type::(window_id) { for terminal in terminals { let Some(pwd) = terminal.as_ref(app).pwd_if_local(app) else { continue; }; let Ok(cwd) = PathBuf::from(pwd).canonicalize() else { continue; }; if cwd.starts_with(root) { return true; } } } } false } fn has_open_file_for_workspace(root: &Path, app: &AppContext) -> bool { for window_id in app.window_ids() { if let Some(editors) = app.views_of_type::(window_id) { for editor in editors { let editor_ref = editor.as_ref(app); if !editor_ref.language_server_enabled() { continue; } let Some(path) = editor_ref.file_path() else { continue; }; if path.starts_with(root) { return true; } } } } false } impl Entity for LanguageServerShutdownManager { type Event = (); } impl SingletonEntity for LanguageServerShutdownManager {}