diff --git a/crates/emmylua_code_analysis/src/semantic_db/index/types.rs b/crates/emmylua_code_analysis/src/semantic_db/index/types.rs index f6e412341..3ad210955 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/index/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/index/types.rs @@ -51,6 +51,8 @@ impl WorkspaceTypeIndex { }) } + /// Remove only the given file's definitions, preserving same-name declarations + /// from other files even when they share a named semantic ID. pub(crate) fn remove_file(&mut self, file_id: FileId) { let Some(keys) = self.by_file.remove(&file_id) else { return; @@ -61,7 +63,7 @@ impl WorkspaceTypeIndex { }; let new_defs: Vec = defs .iter() - .filter(|def| def.id != type_id) + .filter(|def| def.id != type_id || def.file_id != file_id) .cloned() .collect(); if new_defs.is_empty() { diff --git a/crates/emmylua_code_analysis/src/semantic_db/mod.rs b/crates/emmylua_code_analysis/src/semantic_db/mod.rs index a7c0f61b6..0812ff3b7 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/mod.rs @@ -17,6 +17,7 @@ pub(crate) mod types; pub(crate) mod update; use hashbrown::{HashMap, HashSet}; +use itertools::Itertools; use std::fmt; use std::path::PathBuf; use std::sync::Arc; @@ -258,7 +259,9 @@ fn compute_workspace_ids(roots: &[WorkspaceRoot]) -> Vec { let mut ids: Vec = if roots.is_empty() { vec![WorkspaceId::MAIN] } else { - roots.iter().map(|root| root.id).collect() + // Several roots can belong to the same logical workspace (notably MAIN). + // Aggregate each workspace index once, preserving registration order. + roots.iter().map(|root| root.id).unique().collect() }; if !ids.contains(&WorkspaceId::REMOTE) { ids.push(WorkspaceId::REMOTE); diff --git a/crates/emmylua_code_analysis/src/semantic_db/phase_tests/p10_tests.rs b/crates/emmylua_code_analysis/src/semantic_db/phase_tests/p10_tests.rs index 276e2d862..b89f6e066 100644 --- a/crates/emmylua_code_analysis/src/semantic_db/phase_tests/p10_tests.rs +++ b/crates/emmylua_code_analysis/src/semantic_db/phase_tests/p10_tests.rs @@ -119,6 +119,65 @@ fn p10_workspace_id_caches_follow_root_changes() { assert_eq!(db.workspace_id_of(fid), Some(WorkspaceId::MAIN)); } +#[test] +fn p10_multiple_main_roots_do_not_duplicate_type_definitions() { + let mut db = setup(); + let other_root = p10_base().join("other"); + db.add_main_workspace(other_root.clone()); + db.add_library_workspace(&WorkspaceFolder::new(p10_library_root(), true)); + let library_id = db + .all_workspace_ids() + .iter() + .copied() + .find(|id| id.is_library()) + .unwrap(); + let expected_ids = [WorkspaceId::MAIN, library_id, WorkspaceId::REMOTE]; + assert_eq!(db.all_workspace_ids(), expected_ids); + assert_eq!(db.workspace_lookup_order(), expected_ids); + + let source = "---@class Metrics\nlocal Metrics = {}"; + let first = set_test_file(&mut db, 1, &p10_main_file("metrics.lua"), source); + assert_eq!(db.analysis().type_def_locations(first, "Metrics").len(), 1); + + // Rebuilding indexes after a root change must preserve the same count. + db.add_main_workspace(p10_base().join("third")); + assert_eq!(db.all_workspace_ids(), expected_ids); + assert_eq!(db.workspace_lookup_order(), expected_ids); + assert_eq!(db.analysis().type_def_locations(first, "Metrics").len(), 1); + + // A real declaration edit must remove the old name and index the new one. + set_test_file( + &mut db, + 1, + &p10_main_file("metrics.lua"), + &source.replace("Metrics", "RenamedMetrics"), + ); + assert!( + db.analysis() + .type_def_locations(first, "Metrics") + .is_empty() + ); + assert_eq!( + db.analysis() + .type_def_locations(first, "RenamedMetrics") + .len(), + 1 + ); + set_test_file(&mut db, 1, &p10_main_file("metrics.lua"), source); + assert!( + db.analysis() + .type_def_locations(first, "RenamedMetrics") + .is_empty() + ); + assert_eq!(db.analysis().type_def_locations(first, "Metrics").len(), 1); + + // Different declarations with the same name still count as duplicates. + set_test_file(&mut db, 2, &other_root.join("metrics.lua"), source); + assert_eq!(db.analysis().type_def_locations(first, "Metrics").len(), 2); + set_test_file(&mut db, 2, &other_root.join("metrics.lua"), "return {}"); + assert_eq!(db.analysis().type_def_locations(first, "Metrics").len(), 1); +} + #[test] fn p10_type_buckets_share_arc_and_invalidate_on_write() { let mut db = setup();