Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions crates/emmylua_code_analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct EmmyLuaAnalysis {
pub compilation: LuaCompilation,
pub diagnostic: LuaDiagnostic,
pub emmyrc: Arc<Emmyrc>,
#[cfg(test)]
reindex_count: usize,
}

impl EmmyLuaAnalysis {
Expand All @@ -62,6 +64,8 @@ impl EmmyLuaAnalysis {
compilation: LuaCompilation::new(emmyrc.clone()),
diagnostic: LuaDiagnostic::new(),
emmyrc,
#[cfg(test)]
reindex_count: 0,
}
}

Expand Down Expand Up @@ -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
})
Comment on lines +254 to +258
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a side-effect inside a filter closure makes the code less readable and harder to reason about. A more idiomatic and clearer approach would be to separate the check for existing files from the filtering logic. For example, you could use peekable() on the iterator to check if it's empty before proceeding to collect the stale URIs. This would make the intent of the code more explicit.

.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::<Vec<_>>()
.collect::<Vec<_>>();
(had_existing_non_std_local_files, stale_uris)
};
for uri in &stale_uris {
self.remove_file_by_uri(uri);
Expand All @@ -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
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
Comment on lines +439 to +477
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These tests create files in the current working directory and do not clean them up, which can pollute the development environment. It's best practice for tests to be self-contained. Please use a temporary directory for test files and ensure they are cleaned up. The tempfile crate is great for this and will handle cleanup automatically. You'll need to add tempfile to your [dev-dependencies].

mod tests {
    use super::*;

    #[test]
    fn reload_workspace_files_skips_reindex_when_bootstrapping_workspace() {
        let temp_dir = tempfile::tempdir().unwrap();
        let workspace_root = temp_dir.path().to_path_buf();
        let mut analysis = EmmyLuaAnalysis::new();
        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 temp_dir = tempfile::tempdir().unwrap();
        let workspace_root = temp_dir.path().to_path_buf();
        let mut analysis = EmmyLuaAnalysis::new();
        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);
    }
}

105 changes: 105 additions & 0 deletions crates/emmylua_ls/src/handlers/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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<String> {
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand Down
Loading