1use dashmap::DashMap;
2use std::sync::Arc;
3use tower_lsp_server::ls_types::Uri;
4
5use crate::Ecosystem;
6
7pub struct EcosystemRegistry {
36 ecosystems: DashMap<&'static str, Arc<dyn Ecosystem>>,
38 filename_map: DashMap<&'static str, &'static str>,
40}
41
42impl EcosystemRegistry {
43 pub fn new() -> Self {
54 Self {
55 ecosystems: DashMap::new(),
56 filename_map: DashMap::new(),
57 }
58 }
59
60 pub fn register(&self, ecosystem: Arc<dyn Ecosystem>) {
79 let id = ecosystem.id();
80
81 for filename in ecosystem.manifest_filenames() {
83 self.filename_map.insert(*filename, id);
84 }
85
86 self.ecosystems.insert(id, ecosystem);
88 }
89
90 pub fn get(&self, id: &str) -> Option<Arc<dyn Ecosystem>> {
112 self.ecosystems.get(id).map(|e| Arc::clone(&e))
113 }
114
115 pub fn get_for_filename(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
137 let id = self.filename_map.get(filename)?;
138 self.get(*id)
139 }
140
141 pub fn get_for_uri(&self, uri: &Uri) -> Option<Arc<dyn Ecosystem>> {
168 let path = uri.path().as_str();
169 let filename = path.rsplit('/').next()?;
170 self.get_for_filename(filename)
171 }
172
173 pub fn ecosystem_ids(&self) -> Vec<&'static str> {
196 self.ecosystems.iter().map(|e| *e.key()).collect()
197 }
198
199 pub fn get_for_lockfile(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
223 for entry in self.ecosystems.iter() {
224 let ecosystem = entry.value();
225 if ecosystem.lockfile_filenames().contains(&filename) {
226 return Some(Arc::clone(ecosystem));
227 }
228 }
229 None
230 }
231
232 pub fn all_lockfile_patterns(&self) -> Vec<String> {
251 let mut patterns = Vec::new();
252 for entry in self.ecosystems.iter() {
253 let ecosystem = entry.value();
254 for filename in ecosystem.lockfile_filenames() {
255 patterns.push(format!("**/{}", filename));
256 }
257 }
258 patterns
259 }
260}
261
262impl Default for EcosystemRegistry {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use async_trait::async_trait;
272 use std::any::Any;
273 use tower_lsp_server::ls_types::{
274 CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position,
275 };
276
277 use crate::{EcosystemConfig, ParseResult, Registry};
278
279 struct MockEcosystem {
281 id: &'static str,
282 display_name: &'static str,
283 filenames: &'static [&'static str],
284 lockfiles: &'static [&'static str],
285 }
286
287 #[async_trait]
288 impl Ecosystem for MockEcosystem {
289 fn id(&self) -> &'static str {
290 self.id
291 }
292
293 fn display_name(&self) -> &'static str {
294 self.display_name
295 }
296
297 fn manifest_filenames(&self) -> &[&'static str] {
298 self.filenames
299 }
300
301 fn lockfile_filenames(&self) -> &[&'static str] {
302 self.lockfiles
303 }
304
305 async fn parse_manifest(
306 &self,
307 _content: &str,
308 _uri: &Uri,
309 ) -> crate::error::Result<Box<dyn ParseResult>> {
310 unimplemented!()
311 }
312
313 fn registry(&self) -> Arc<dyn Registry> {
314 unimplemented!()
315 }
316
317 async fn generate_inlay_hints(
318 &self,
319 _parse_result: &dyn ParseResult,
320 _cached_versions: &std::collections::HashMap<String, String>,
321 _resolved_versions: &std::collections::HashMap<String, String>,
322 _loading_state: crate::LoadingState,
323 _config: &EcosystemConfig,
324 ) -> Vec<InlayHint> {
325 vec![]
326 }
327
328 async fn generate_hover(
329 &self,
330 _parse_result: &dyn ParseResult,
331 _position: Position,
332 _cached_versions: &std::collections::HashMap<String, String>,
333 _resolved_versions: &std::collections::HashMap<String, String>,
334 ) -> Option<Hover> {
335 None
336 }
337
338 async fn generate_code_actions(
339 &self,
340 _parse_result: &dyn ParseResult,
341 _position: Position,
342 _cached_versions: &std::collections::HashMap<String, String>,
343 _uri: &Uri,
344 ) -> Vec<CodeAction> {
345 vec![]
346 }
347
348 async fn generate_diagnostics(
349 &self,
350 _parse_result: &dyn ParseResult,
351 _cached_versions: &std::collections::HashMap<String, String>,
352 _resolved_versions: &std::collections::HashMap<String, String>,
353 _uri: &Uri,
354 ) -> Vec<Diagnostic> {
355 vec![]
356 }
357
358 async fn generate_completions(
359 &self,
360 _parse_result: &dyn ParseResult,
361 _position: Position,
362 _content: &str,
363 ) -> Vec<CompletionItem> {
364 vec![]
365 }
366
367 fn as_any(&self) -> &dyn Any {
368 self
369 }
370 }
371
372 #[test]
373 fn test_new_registry_is_empty() {
374 let registry = EcosystemRegistry::new();
375 assert_eq!(registry.ecosystem_ids().len(), 0);
376 }
377
378 #[test]
379 fn test_register_ecosystem() {
380 let registry = EcosystemRegistry::new();
381 let ecosystem = Arc::new(MockEcosystem {
382 id: "test",
383 display_name: "Test Ecosystem",
384 filenames: &["test.toml"],
385 lockfiles: &[],
386 });
387
388 registry.register(ecosystem);
389
390 assert_eq!(registry.ecosystem_ids().len(), 1);
391 assert!(registry.get("test").is_some());
392 }
393
394 #[test]
395 fn test_get_by_id() {
396 let registry = EcosystemRegistry::new();
397 let ecosystem = Arc::new(MockEcosystem {
398 id: "test",
399 display_name: "Test Ecosystem",
400 filenames: &["test.toml"],
401 lockfiles: &[],
402 });
403
404 registry.register(ecosystem);
405
406 let retrieved = registry.get("test").unwrap();
407 assert_eq!(retrieved.id(), "test");
408 assert_eq!(retrieved.display_name(), "Test Ecosystem");
409 }
410
411 #[test]
412 fn test_get_by_filename() {
413 let registry = EcosystemRegistry::new();
414 let ecosystem = Arc::new(MockEcosystem {
415 id: "test",
416 display_name: "Test Ecosystem",
417 filenames: &["test.toml", "test.json"],
418 lockfiles: &[],
419 });
420
421 registry.register(ecosystem);
422
423 let retrieved1 = registry.get_for_filename("test.toml").unwrap();
424 assert_eq!(retrieved1.id(), "test");
425
426 let retrieved2 = registry.get_for_filename("test.json").unwrap();
427 assert_eq!(retrieved2.id(), "test");
428
429 assert!(registry.get_for_filename("unknown.toml").is_none());
430 }
431
432 #[test]
433 fn test_get_by_uri() {
434 let registry = EcosystemRegistry::new();
435 let ecosystem = Arc::new(MockEcosystem {
436 id: "test",
437 display_name: "Test Ecosystem",
438 filenames: &["test.toml"],
439 lockfiles: &[],
440 });
441
442 registry.register(ecosystem);
443
444 let uri = Uri::from_file_path("/home/user/project/test.toml").unwrap();
445 let retrieved = registry.get_for_uri(&uri).unwrap();
446 assert_eq!(retrieved.id(), "test");
447
448 let unknown_uri = Uri::from_file_path("/home/user/project/unknown.toml").unwrap();
449 assert!(registry.get_for_uri(&unknown_uri).is_none());
450 }
451
452 #[test]
453 fn test_multiple_ecosystems() {
454 let registry = EcosystemRegistry::new();
455
456 let eco1 = Arc::new(MockEcosystem {
457 id: "cargo",
458 display_name: "Cargo",
459 filenames: &["Cargo.toml"],
460 lockfiles: &["Cargo.lock"],
461 });
462
463 let eco2 = Arc::new(MockEcosystem {
464 id: "npm",
465 display_name: "npm",
466 filenames: &["package.json"],
467 lockfiles: &["package-lock.json"],
468 });
469
470 registry.register(eco1);
471 registry.register(eco2);
472
473 assert_eq!(registry.ecosystem_ids().len(), 2);
474
475 assert_eq!(
476 registry.get_for_filename("Cargo.toml").unwrap().id(),
477 "cargo"
478 );
479 assert_eq!(
480 registry.get_for_filename("package.json").unwrap().id(),
481 "npm"
482 );
483 }
484
485 #[test]
486 fn test_get_for_lockfile() {
487 let registry = EcosystemRegistry::new();
488 let ecosystem = Arc::new(MockEcosystem {
489 id: "cargo",
490 display_name: "Cargo",
491 filenames: &["Cargo.toml"],
492 lockfiles: &["Cargo.lock"],
493 });
494
495 registry.register(ecosystem);
496
497 let retrieved = registry.get_for_lockfile("Cargo.lock").unwrap();
498 assert_eq!(retrieved.id(), "cargo");
499 assert_eq!(retrieved.display_name(), "Cargo");
500
501 assert!(registry.get_for_lockfile("unknown.lock").is_none());
503 }
504
505 #[test]
506 fn test_get_for_lockfile_multiple_lockfiles() {
507 let registry = EcosystemRegistry::new();
508 let ecosystem = Arc::new(MockEcosystem {
509 id: "pypi",
510 display_name: "PyPI",
511 filenames: &["pyproject.toml"],
512 lockfiles: &["poetry.lock", "uv.lock"],
513 });
514
515 registry.register(ecosystem);
516
517 let retrieved1 = registry.get_for_lockfile("poetry.lock").unwrap();
518 assert_eq!(retrieved1.id(), "pypi");
519
520 let retrieved2 = registry.get_for_lockfile("uv.lock").unwrap();
521 assert_eq!(retrieved2.id(), "pypi");
522 }
523
524 #[test]
525 fn test_all_lockfile_patterns_empty() {
526 let registry = EcosystemRegistry::new();
527 assert!(registry.all_lockfile_patterns().is_empty());
528 }
529
530 #[test]
531 fn test_all_lockfile_patterns_single_ecosystem() {
532 let registry = EcosystemRegistry::new();
533 let ecosystem = Arc::new(MockEcosystem {
534 id: "cargo",
535 display_name: "Cargo",
536 filenames: &["Cargo.toml"],
537 lockfiles: &["Cargo.lock"],
538 });
539
540 registry.register(ecosystem);
541
542 let patterns = registry.all_lockfile_patterns();
543 assert_eq!(patterns.len(), 1);
544 assert_eq!(patterns[0], "**/Cargo.lock");
545 }
546
547 #[test]
548 fn test_all_lockfile_patterns_multiple_ecosystems() {
549 let registry = EcosystemRegistry::new();
550
551 let eco1 = Arc::new(MockEcosystem {
552 id: "cargo",
553 display_name: "Cargo",
554 filenames: &["Cargo.toml"],
555 lockfiles: &["Cargo.lock"],
556 });
557
558 let eco2 = Arc::new(MockEcosystem {
559 id: "npm",
560 display_name: "npm",
561 filenames: &["package.json"],
562 lockfiles: &["package-lock.json"],
563 });
564
565 let eco3 = Arc::new(MockEcosystem {
566 id: "pypi",
567 display_name: "PyPI",
568 filenames: &["pyproject.toml"],
569 lockfiles: &["poetry.lock", "uv.lock"],
570 });
571
572 registry.register(eco1);
573 registry.register(eco2);
574 registry.register(eco3);
575
576 let patterns = registry.all_lockfile_patterns();
577 assert_eq!(patterns.len(), 4);
578 assert!(patterns.contains(&"**/Cargo.lock".to_string()));
579 assert!(patterns.contains(&"**/package-lock.json".to_string()));
580 assert!(patterns.contains(&"**/poetry.lock".to_string()));
581 assert!(patterns.contains(&"**/uv.lock".to_string()));
582 }
583
584 #[test]
585 fn test_all_lockfile_patterns_no_lockfiles() {
586 let registry = EcosystemRegistry::new();
587 let ecosystem = Arc::new(MockEcosystem {
588 id: "test",
589 display_name: "Test",
590 filenames: &["test.toml"],
591 lockfiles: &[],
592 });
593
594 registry.register(ecosystem);
595
596 let patterns = registry.all_lockfile_patterns();
597 assert!(patterns.is_empty());
598 }
599}