Files
galaxy/app/src/code/language_server_shutdown_manager.rs
T

157 lines
4.7 KiB
Rust

//! 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<AbortHandle>,
}
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<Self>) {
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<Self>) -> bool {
let lsp_manager_handle = LspManagerModel::handle(ctx);
let workspace_roots: Vec<PathBuf> = 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::<TerminalView>(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::<LocalCodeEditorView>(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 {}