diff --git a/crates/emmylua_code_analysis/src/lib.rs b/crates/emmylua_code_analysis/src/lib.rs index 1cac88431..73ee49381 100644 --- a/crates/emmylua_code_analysis/src/lib.rs +++ b/crates/emmylua_code_analysis/src/lib.rs @@ -53,6 +53,8 @@ pub struct EmmyLuaAnalysis { pub compilation: LuaCompilation, pub diagnostic: LuaDiagnostic, pub emmyrc: Arc, + #[cfg(test)] + reindex_count: usize, } impl EmmyLuaAnalysis { @@ -62,6 +64,8 @@ impl EmmyLuaAnalysis { compilation: LuaCompilation::new(emmyrc.clone()), diagnostic: LuaDiagnostic::new(), emmyrc, + #[cfg(test)] + reindex_count: 0, } } @@ -239,17 +243,24 @@ impl EmmyLuaAnalysis { let mut kept_paths = open_paths.clone(); kept_paths.extend(files.iter().map(|(path, _)| path.clone())); - let stale_uris = { + let (had_existing_non_std_local_files, stale_uris) = { let db = self.compilation.get_db(); let vfs = db.get_vfs(); let module_index = db.get_module_index(); - vfs.get_all_local_file_ids() + let mut had_existing_non_std_local_files = false; + let stale_uris = vfs + .get_all_local_file_ids() .into_iter() - .filter(|file_id| !module_index.is_std(file_id)) + .filter(|file_id| { + let is_non_std = !module_index.is_std(file_id); + had_existing_non_std_local_files |= is_non_std; + is_non_std + }) .filter_map(|file_id| vfs.get_file_path(&file_id).cloned()) .filter(|path| !kept_paths.contains(path)) .filter_map(|path| file_path_to_uri(&path)) - .collect::>() + .collect::>(); + (had_existing_non_std_local_files, stale_uris) }; for uri in &stale_uris { self.remove_file_by_uri(uri); @@ -267,7 +278,9 @@ impl EmmyLuaAnalysis { .map(|(uri, text)| (uri, Some(text))) .collect(), ); - self.reindex(); + if had_existing_non_std_local_files { + self.reindex(); + } stale_uris } @@ -291,6 +304,10 @@ impl EmmyLuaAnalysis { } pub fn reindex(&mut self) { + #[cfg(test)] + { + self.reindex_count += 1; + } let file_ids = self.compilation.get_db().get_vfs().get_all_file_ids(); self.compilation.clear_index(); self.compilation.update_index(file_ids); @@ -417,3 +434,44 @@ impl Default for EmmyLuaAnalysis { unsafe impl Send for EmmyLuaAnalysis {} unsafe impl Sync for EmmyLuaAnalysis {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reload_workspace_files_skips_reindex_when_bootstrapping_workspace() { + let mut analysis = EmmyLuaAnalysis::new(); + let workspace_root = std::env::current_dir().unwrap(); + let file_path = workspace_root.join("__reload_workspace_startup_test.lua"); + analysis.add_main_workspace(workspace_root); + + analysis.reload_workspace_files( + vec![(file_path.clone(), Some("return true\n".to_string()))], + Vec::new(), + ); + + assert_eq!(analysis.reindex_count, 0); + assert!( + analysis + .get_file_id(&file_path_to_uri(&file_path).unwrap()) + .is_some() + ); + } + + #[test] + fn reload_workspace_files_reindexes_existing_workspace_files() { + let mut analysis = EmmyLuaAnalysis::new(); + let workspace_root = std::env::current_dir().unwrap(); + let file_path = workspace_root.join("__reload_workspace_existing_test.lua"); + analysis.add_main_workspace(workspace_root); + analysis.update_files_by_path(vec![(file_path.clone(), Some("return true\n".to_string()))]); + + analysis.reload_workspace_files( + vec![(file_path, Some("return false\n".to_string()))], + Vec::new(), + ); + + assert_eq!(analysis.reindex_count, 1); + } +} diff --git a/crates/emmylua_ls/src/handlers/configuration/mod.rs b/crates/emmylua_ls/src/handlers/configuration/mod.rs index 434c0dea1..d1f6d229a 100644 --- a/crates/emmylua_ls/src/handlers/configuration/mod.rs +++ b/crates/emmylua_ls/src/handlers/configuration/mod.rs @@ -31,6 +31,11 @@ pub async fn on_did_change_configuration( // Update config and reload - acquire write lock only when necessary { let mut workspace_manager = context.workspace_manager().write().await; + if workspace_manager.client_config == new_client_config { + log::info!("skip workspace reload; client config unchanged"); + return Some(()); + } + workspace_manager.client_config = new_client_config; log::info!("reloading workspace folders"); workspace_manager.add_reload_workspace_task(context.clone()); @@ -44,3 +49,103 @@ pub struct ConfigurationCapabilities; impl RegisterCapabilities for ConfigurationCapabilities { fn register_capabilities(_: &mut ServerCapabilities, _: &ClientCapabilities) {} } + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + fs, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, + }; + + use crate::{ + context::{ClientId, ServerContext}, + handlers::ClientConfig, + }; + use emmylua_code_analysis::WorkspaceFolder; + use lsp_server::{Connection, Message}; + use lsp_types::{DidChangeWatchedFilesClientCapabilities, WorkspaceClientCapabilities}; + + static TEST_WORKSPACE_COUNTER: AtomicU64 = AtomicU64::new(0); + + fn dynamic_watch_capabilities() -> ClientCapabilities { + ClientCapabilities { + workspace: Some(WorkspaceClientCapabilities { + did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities { + dynamic_registration: Some(true), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + } + } + + fn collect_watch_registration_methods(client: &Connection, timeout: Duration) -> Vec { + let mut methods = Vec::new(); + let deadline = Instant::now() + timeout; + + while let Some(remaining) = deadline.checked_duration_since(Instant::now()) { + match client.receiver.recv_timeout(remaining) { + Ok(Message::Request(request)) + if request.method == "client/registerCapability" + || request.method == "client/unregisterCapability" => + { + methods.push(request.method); + } + Ok(_) => {} + Err(_) => break, + } + } + + methods + } + + #[tokio::test(flavor = "multi_thread")] + async fn unchanged_config_notification_does_not_reload_workspace() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let counter = TEST_WORKSPACE_COUNTER.fetch_add(1, Ordering::Relaxed); + let workspace_root = std::env::temp_dir().join(format!( + "emmylua-config-handler-{}-{}-{}", + std::process::id(), + unique, + counter, + )); + fs::create_dir_all(&workspace_root).unwrap(); + + let (server, client) = Connection::memory(); + let context = ServerContext::new(server, dynamic_watch_capabilities()); + let snapshot = context.snapshot(); + { + let mut workspace_manager = snapshot.workspace_manager().write().await; + workspace_manager.workspace_folders = + vec![WorkspaceFolder::new(workspace_root.clone(), false)]; + workspace_manager.client_config = ClientConfig { + client_id: ClientId::Other, + encoding: "utf-8".to_string(), + ..Default::default() + }; + } + + on_did_change_configuration( + snapshot.clone(), + DidChangeConfigurationParams { + settings: serde_json::Value::Null, + }, + ) + .await; + + assert!(collect_watch_registration_methods(&client, Duration::from_millis(250)).is_empty()); + + snapshot + .file_diagnostic() + .cancel_workspace_diagnostic() + .await; + context.close().await; + fs::remove_dir_all(workspace_root).unwrap(); + } +} diff --git a/crates/emmylua_ls/src/handlers/initialized/client_config/mod.rs b/crates/emmylua_ls/src/handlers/initialized/client_config/mod.rs index db7f8adf8..88fad45f0 100644 --- a/crates/emmylua_ls/src/handlers/initialized/client_config/mod.rs +++ b/crates/emmylua_ls/src/handlers/initialized/client_config/mod.rs @@ -8,7 +8,7 @@ use vscode_config::get_client_config_vscode; use crate::context::{ClientId, ServerContextSnapshot}; #[allow(unused)] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct ClientConfig { pub client_id: ClientId, pub exclude: Vec,