1use async_trait::async_trait;
7use std::any::Any;
8use std::collections::HashMap;
9use std::sync::Arc;
10use tower_lsp_server::ls_types::{
11 CodeAction, CompletionItem, Diagnostic, Hover, InlayHint, Position, Uri,
12};
13
14use deps_core::{
15 Ecosystem, EcosystemConfig, ParseResult as ParseResultTrait, Registry, Result, Version,
16 lsp_helpers,
17};
18
19use crate::formatter::CargoFormatter;
20use crate::registry::CratesIoRegistry;
21
22pub struct CargoEcosystem {
32 registry: Arc<CratesIoRegistry>,
33 formatter: CargoFormatter,
34}
35
36impl CargoEcosystem {
37 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
39 Self {
40 registry: Arc::new(CratesIoRegistry::new(cache)),
41 formatter: CargoFormatter,
42 }
43 }
44
45 async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> {
49 use deps_core::completion::build_package_completion;
50
51 if prefix.len() < 2 || prefix.len() > 100 {
53 return vec![];
54 }
55
56 let results = match self.registry.search(prefix, 20).await {
58 Ok(r) => r,
59 Err(e) => {
60 tracing::warn!("Package search failed for '{}': {}", prefix, e);
61 return vec![];
62 }
63 };
64
65 let insert_range = tower_lsp_server::ls_types::Range::default();
67
68 results
69 .into_iter()
70 .map(|metadata| {
71 let boxed: Box<dyn deps_core::Metadata> = Box::new(metadata);
72 build_package_completion(boxed.as_ref(), insert_range)
73 })
74 .collect()
75 }
76
77 async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
78 deps_core::completion::complete_versions_generic(
79 self.registry.as_ref(),
80 package_name,
81 prefix,
82 &['^', '~', '=', '<', '>'],
83 )
84 .await
85 }
86
87 async fn complete_features(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
91 use deps_core::completion::build_feature_completion;
92
93 let versions = match self.registry.get_versions(package_name).await {
95 Ok(v) => v,
96 Err(e) => {
97 tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
98 return vec![];
99 }
100 };
101
102 let latest = match versions.iter().find(|v| v.is_stable()) {
103 Some(v) => v,
104 None => {
105 tracing::warn!("No stable version found for '{}'", package_name);
106 return vec![];
107 }
108 };
109
110 let insert_range = tower_lsp_server::ls_types::Range::default();
111
112 let features = latest.features();
114 features
115 .into_iter()
116 .filter(|f| f.starts_with(prefix))
117 .map(|feature| build_feature_completion(&feature, package_name, insert_range))
118 .collect()
119 }
120}
121
122#[async_trait]
123impl Ecosystem for CargoEcosystem {
124 fn id(&self) -> &'static str {
125 "cargo"
126 }
127
128 fn display_name(&self) -> &'static str {
129 "Cargo (Rust)"
130 }
131
132 fn manifest_filenames(&self) -> &[&'static str] {
133 &["Cargo.toml"]
134 }
135
136 fn lockfile_filenames(&self) -> &[&'static str] {
137 &["Cargo.lock"]
138 }
139
140 async fn parse_manifest(&self, content: &str, uri: &Uri) -> Result<Box<dyn ParseResultTrait>> {
141 let result = crate::parser::parse_cargo_toml(content, uri)?;
142 Ok(Box::new(result))
143 }
144
145 fn registry(&self) -> Arc<dyn Registry> {
146 self.registry.clone() as Arc<dyn Registry>
147 }
148
149 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
150 Some(Arc::new(crate::lockfile::CargoLockParser))
151 }
152
153 async fn generate_inlay_hints(
154 &self,
155 parse_result: &dyn ParseResultTrait,
156 cached_versions: &HashMap<String, String>,
157 resolved_versions: &HashMap<String, String>,
158 loading_state: deps_core::LoadingState,
159 config: &EcosystemConfig,
160 ) -> Vec<InlayHint> {
161 lsp_helpers::generate_inlay_hints(
162 parse_result,
163 cached_versions,
164 resolved_versions,
165 loading_state,
166 config,
167 &self.formatter,
168 )
169 }
170
171 async fn generate_hover(
172 &self,
173 parse_result: &dyn ParseResultTrait,
174 position: Position,
175 cached_versions: &HashMap<String, String>,
176 resolved_versions: &HashMap<String, String>,
177 ) -> Option<Hover> {
178 lsp_helpers::generate_hover(
179 parse_result,
180 position,
181 cached_versions,
182 resolved_versions,
183 self.registry.as_ref(),
184 &self.formatter,
185 )
186 .await
187 }
188
189 async fn generate_code_actions(
190 &self,
191 parse_result: &dyn ParseResultTrait,
192 position: Position,
193 _cached_versions: &HashMap<String, String>,
194 uri: &Uri,
195 ) -> Vec<CodeAction> {
196 lsp_helpers::generate_code_actions(
197 parse_result,
198 position,
199 uri,
200 self.registry.as_ref(),
201 &self.formatter,
202 )
203 .await
204 }
205
206 async fn generate_diagnostics(
207 &self,
208 parse_result: &dyn ParseResultTrait,
209 cached_versions: &HashMap<String, String>,
210 resolved_versions: &HashMap<String, String>,
211 _uri: &Uri,
212 ) -> Vec<Diagnostic> {
213 lsp_helpers::generate_diagnostics_from_cache(
214 parse_result,
215 cached_versions,
216 resolved_versions,
217 &self.formatter,
218 )
219 }
220
221 async fn generate_completions(
222 &self,
223 parse_result: &dyn ParseResultTrait,
224 position: Position,
225 content: &str,
226 ) -> Vec<CompletionItem> {
227 use deps_core::completion::{CompletionContext, detect_completion_context};
228
229 let context = detect_completion_context(parse_result, position, content);
230
231 match context {
232 CompletionContext::PackageName { prefix } => self.complete_package_names(&prefix).await,
233 CompletionContext::Version {
234 package_name,
235 prefix,
236 } => self.complete_versions(&package_name, &prefix).await,
237 CompletionContext::Feature {
238 package_name,
239 prefix,
240 } => self.complete_features(&package_name, &prefix).await,
241 CompletionContext::None => vec![],
242 }
243 }
244
245 fn as_any(&self) -> &dyn Any {
246 self
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::types::{DependencySection, DependencySource, ParsedDependency};
254 use std::collections::HashMap;
255 use tower_lsp_server::ls_types::{InlayHintLabel, Position, Range};
256
257 fn mock_dependency(
259 name: &str,
260 version: Option<&str>,
261 name_line: u32,
262 version_line: u32,
263 ) -> ParsedDependency {
264 ParsedDependency {
265 name: name.to_string(),
266 name_range: Range::new(
267 Position::new(name_line, 0),
268 Position::new(name_line, name.len() as u32),
269 ),
270 version_req: version.map(String::from),
271 version_range: version.map(|_| {
272 Range::new(
273 Position::new(version_line, 0),
274 Position::new(version_line, 10),
275 )
276 }),
277 features: vec![],
278 features_range: None,
279 source: DependencySource::Registry,
280 workspace_inherited: false,
281 section: DependencySection::Dependencies,
282 }
283 }
284
285 struct MockParseResult {
287 dependencies: Vec<ParsedDependency>,
288 }
289
290 impl deps_core::ParseResult for MockParseResult {
291 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
292 self.dependencies
293 .iter()
294 .map(|d| d as &dyn deps_core::Dependency)
295 .collect()
296 }
297
298 fn workspace_root(&self) -> Option<&std::path::Path> {
299 None
300 }
301
302 fn uri(&self) -> &Uri {
303 static URI: std::sync::LazyLock<Uri> =
304 std::sync::LazyLock::new(|| Uri::from_file_path("/test/Cargo.toml").unwrap());
305 &URI
306 }
307
308 fn as_any(&self) -> &dyn Any {
309 self
310 }
311 }
312
313 #[test]
314 fn test_ecosystem_id() {
315 let cache = Arc::new(deps_core::HttpCache::new());
316 let ecosystem = CargoEcosystem::new(cache);
317 assert_eq!(ecosystem.id(), "cargo");
318 }
319
320 #[test]
321 fn test_ecosystem_display_name() {
322 let cache = Arc::new(deps_core::HttpCache::new());
323 let ecosystem = CargoEcosystem::new(cache);
324 assert_eq!(ecosystem.display_name(), "Cargo (Rust)");
325 }
326
327 #[test]
328 fn test_ecosystem_manifest_filenames() {
329 let cache = Arc::new(deps_core::HttpCache::new());
330 let ecosystem = CargoEcosystem::new(cache);
331 assert_eq!(ecosystem.manifest_filenames(), &["Cargo.toml"]);
332 }
333
334 #[test]
335 fn test_ecosystem_lockfile_filenames() {
336 let cache = Arc::new(deps_core::HttpCache::new());
337 let ecosystem = CargoEcosystem::new(cache);
338 assert_eq!(ecosystem.lockfile_filenames(), &["Cargo.lock"]);
339 }
340
341 #[test]
342 fn test_generate_inlay_hints_up_to_date_exact_match() {
343 let cache = Arc::new(deps_core::HttpCache::new());
344 let ecosystem = CargoEcosystem::new(cache);
345
346 let parse_result = MockParseResult {
347 dependencies: vec![mock_dependency("serde", Some("1.0.214"), 5, 5)],
348 };
349
350 let mut cached_versions = HashMap::new();
351 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
352
353 let config = EcosystemConfig {
354 loading_text: "⏳".to_string(),
355 show_loading_hints: true,
356 show_up_to_date_hints: true,
357 up_to_date_text: "✅".to_string(),
358 needs_update_text: "❌ {}".to_string(),
359 };
360
361 let mut resolved_versions = HashMap::new();
363 resolved_versions.insert("serde".to_string(), "1.0.214".to_string());
364 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
365 &parse_result,
366 &cached_versions,
367 &resolved_versions,
368 deps_core::LoadingState::Loaded,
369 &config,
370 ));
371
372 assert_eq!(hints.len(), 1);
373 match &hints[0].label {
374 InlayHintLabel::String(s) => assert_eq!(s, "✅ 1.0.214"),
375 _ => panic!("Expected String label"),
376 }
377 }
378
379 #[test]
380 fn test_generate_inlay_hints_up_to_date_caret_version() {
381 let cache = Arc::new(deps_core::HttpCache::new());
382 let ecosystem = CargoEcosystem::new(cache);
383
384 let parse_result = MockParseResult {
385 dependencies: vec![mock_dependency("serde", Some("^1.0"), 5, 5)],
386 };
387
388 let mut cached_versions = HashMap::new();
389 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
390
391 let config = EcosystemConfig {
392 loading_text: "⏳".to_string(),
393 show_loading_hints: true,
394 show_up_to_date_hints: true,
395 up_to_date_text: "✅".to_string(),
396 needs_update_text: "❌ {}".to_string(),
397 };
398
399 let mut resolved_versions = HashMap::new();
401 resolved_versions.insert("serde".to_string(), "1.0.214".to_string());
402 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
403 &parse_result,
404 &cached_versions,
405 &resolved_versions,
406 deps_core::LoadingState::Loaded,
407 &config,
408 ));
409
410 assert_eq!(hints.len(), 1);
411 match &hints[0].label {
412 InlayHintLabel::String(s) => assert_eq!(s, "✅ 1.0.214"),
413 _ => panic!("Expected String label"),
414 }
415 }
416
417 #[test]
418 fn test_generate_inlay_hints_needs_update() {
419 let cache = Arc::new(deps_core::HttpCache::new());
420 let ecosystem = CargoEcosystem::new(cache);
421
422 let parse_result = MockParseResult {
423 dependencies: vec![mock_dependency("serde", Some("1.0.100"), 5, 5)],
424 };
425
426 let mut cached_versions = HashMap::new();
427 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
428
429 let config = EcosystemConfig {
430 loading_text: "⏳".to_string(),
431 show_loading_hints: true,
432 show_up_to_date_hints: true,
433 up_to_date_text: "✅".to_string(),
434 needs_update_text: "❌ {}".to_string(),
435 };
436
437 let resolved_versions = HashMap::new();
438 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
439 &parse_result,
440 &cached_versions,
441 &resolved_versions,
442 deps_core::LoadingState::Loaded,
443 &config,
444 ));
445
446 assert_eq!(hints.len(), 1);
447 match &hints[0].label {
448 InlayHintLabel::String(s) => assert_eq!(s, "❌ 1.0.214"),
449 _ => panic!("Expected String label"),
450 }
451 }
452
453 #[test]
454 fn test_generate_inlay_hints_hide_up_to_date() {
455 let cache = Arc::new(deps_core::HttpCache::new());
456 let ecosystem = CargoEcosystem::new(cache);
457
458 let parse_result = MockParseResult {
459 dependencies: vec![mock_dependency("serde", Some("1.0.214"), 5, 5)],
460 };
461
462 let mut cached_versions = HashMap::new();
463 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
464
465 let config = EcosystemConfig {
466 loading_text: "⏳".to_string(),
467 show_loading_hints: true,
468 show_up_to_date_hints: false,
469 up_to_date_text: "✅".to_string(),
470 needs_update_text: "❌ {}".to_string(),
471 };
472
473 let mut resolved_versions = HashMap::new();
475 resolved_versions.insert("serde".to_string(), "1.0.214".to_string());
476 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
477 &parse_result,
478 &cached_versions,
479 &resolved_versions,
480 deps_core::LoadingState::Loaded,
481 &config,
482 ));
483
484 assert_eq!(hints.len(), 0);
485 }
486
487 #[test]
488 fn test_generate_inlay_hints_no_version_range() {
489 let cache = Arc::new(deps_core::HttpCache::new());
490 let ecosystem = CargoEcosystem::new(cache);
491
492 let mut dep = mock_dependency("serde", Some("1.0.214"), 5, 5);
493 dep.version_range = None;
494
495 let parse_result = MockParseResult {
496 dependencies: vec![dep],
497 };
498
499 let mut cached_versions = HashMap::new();
500 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
501
502 let config = EcosystemConfig {
503 loading_text: "⏳".to_string(),
504 show_loading_hints: true,
505 show_up_to_date_hints: true,
506 up_to_date_text: "✅".to_string(),
507 needs_update_text: "❌ {}".to_string(),
508 };
509
510 let resolved_versions = HashMap::new();
511 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
512 &parse_result,
513 &cached_versions,
514 &resolved_versions,
515 deps_core::LoadingState::Loaded,
516 &config,
517 ));
518
519 assert_eq!(hints.len(), 0);
520 }
521
522 #[test]
523 fn test_generate_inlay_hints_caret_edge_case() {
524 let cache = Arc::new(deps_core::HttpCache::new());
525 let ecosystem = CargoEcosystem::new(cache);
526
527 let dep = mock_dependency("serde", Some("^"), 5, 5);
529
530 let parse_result = MockParseResult {
531 dependencies: vec![dep],
532 };
533
534 let mut cached_versions = HashMap::new();
535 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
536
537 let config = EcosystemConfig {
538 loading_text: "⏳".to_string(),
539 show_loading_hints: true,
540 show_up_to_date_hints: true,
541 up_to_date_text: "✅".to_string(),
542 needs_update_text: "❌ {}".to_string(),
543 };
544
545 let resolved_versions = HashMap::new();
547 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
548 &parse_result,
549 &cached_versions,
550 &resolved_versions,
551 deps_core::LoadingState::Loaded,
552 &config,
553 ));
554
555 assert_eq!(hints.len(), 1);
556 }
557
558 #[test]
559 fn test_as_any() {
560 let cache = Arc::new(deps_core::HttpCache::new());
561 let ecosystem = CargoEcosystem::new(cache);
562
563 let any = ecosystem.as_any();
565 assert!(any.is::<CargoEcosystem>());
566 }
567
568 #[tokio::test]
569 async fn test_complete_package_names_minimum_prefix() {
570 let cache = Arc::new(deps_core::HttpCache::new());
571 let ecosystem = CargoEcosystem::new(cache);
572
573 let results = ecosystem.complete_package_names("s").await;
575 assert!(results.is_empty());
576
577 let results = ecosystem.complete_package_names("").await;
579 assert!(results.is_empty());
580 }
581
582 #[tokio::test]
583 #[ignore] async fn test_complete_package_names_real_search() {
585 let cache = Arc::new(deps_core::HttpCache::new());
586 let ecosystem = CargoEcosystem::new(cache);
587
588 let results = ecosystem.complete_package_names("serd").await;
589 assert!(!results.is_empty());
590 assert!(results.iter().any(|r| r.label == "serde"));
591 }
592
593 #[tokio::test]
594 #[ignore] async fn test_complete_versions_real() {
596 let cache = Arc::new(deps_core::HttpCache::new());
597 let ecosystem = CargoEcosystem::new(cache);
598
599 let results = ecosystem.complete_versions("serde", "1.0").await;
600 assert!(!results.is_empty());
601 assert!(results.iter().all(|r| r.label.starts_with("1.0")));
602 }
603
604 #[tokio::test]
605 #[ignore] async fn test_complete_versions_with_operator() {
607 let cache = Arc::new(deps_core::HttpCache::new());
608 let ecosystem = CargoEcosystem::new(cache);
609
610 let results = ecosystem.complete_versions("serde", "^1.0").await;
611 assert!(!results.is_empty());
612 assert!(results.iter().all(|r| r.label.starts_with("1.0")));
613 }
614
615 #[tokio::test]
616 #[ignore] async fn test_complete_features_real() {
618 let cache = Arc::new(deps_core::HttpCache::new());
619 let ecosystem = CargoEcosystem::new(cache);
620
621 let results = ecosystem.complete_features("serde", "").await;
622 assert!(!results.is_empty());
623 assert!(results.iter().any(|r| r.label == "derive"));
624 }
625
626 #[tokio::test]
627 #[ignore] async fn test_complete_features_with_prefix() {
629 let cache = Arc::new(deps_core::HttpCache::new());
630 let ecosystem = CargoEcosystem::new(cache);
631
632 let results = ecosystem.complete_features("serde", "der").await;
633 assert!(!results.is_empty());
634 assert!(results.iter().all(|r| r.label.starts_with("der")));
635 }
636
637 #[tokio::test]
638 async fn test_complete_versions_unknown_package() {
639 let cache = Arc::new(deps_core::HttpCache::new());
640 let ecosystem = CargoEcosystem::new(cache);
641
642 let results = ecosystem
644 .complete_versions("this-package-does-not-exist-12345", "1.0")
645 .await;
646 assert!(results.is_empty());
647 }
648
649 #[tokio::test]
650 async fn test_complete_features_unknown_package() {
651 let cache = Arc::new(deps_core::HttpCache::new());
652 let ecosystem = CargoEcosystem::new(cache);
653
654 let results = ecosystem
656 .complete_features("this-package-does-not-exist-12345", "")
657 .await;
658 assert!(results.is_empty());
659 }
660
661 #[tokio::test]
662 async fn test_complete_package_names_special_characters() {
663 let cache = Arc::new(deps_core::HttpCache::new());
664 let ecosystem = CargoEcosystem::new(cache);
665
666 let results = ecosystem.complete_package_names("tokio-ut").await;
668 assert!(results.is_empty() || !results.is_empty());
670 }
671
672 #[tokio::test]
673 async fn test_complete_package_names_max_length() {
674 let cache = Arc::new(deps_core::HttpCache::new());
675 let ecosystem = CargoEcosystem::new(cache);
676
677 let long_prefix = "a".repeat(101);
679 let results = ecosystem.complete_package_names(&long_prefix).await;
680 assert!(results.is_empty());
681
682 let max_prefix = "a".repeat(100);
684 let results = ecosystem.complete_package_names(&max_prefix).await;
685 assert!(results.is_empty() || !results.is_empty());
687 }
688
689 #[tokio::test]
690 #[ignore] async fn test_complete_versions_limit_20() {
692 let cache = Arc::new(deps_core::HttpCache::new());
693 let ecosystem = CargoEcosystem::new(cache);
694
695 let results = ecosystem.complete_versions("serde", "1").await;
697 assert!(results.len() <= 20);
698 }
699
700 #[tokio::test]
701 #[ignore] async fn test_complete_features_empty_list() {
703 let cache = Arc::new(deps_core::HttpCache::new());
704 let ecosystem = CargoEcosystem::new(cache);
705
706 let results = ecosystem.complete_features("anyhow", "nonexistent").await;
709 assert!(results.is_empty());
710 }
711
712 #[tokio::test]
713 #[ignore] async fn test_complete_package_names_special_chars_real() {
715 let cache = Arc::new(deps_core::HttpCache::new());
716 let ecosystem = CargoEcosystem::new(cache);
717
718 let results = ecosystem.complete_package_names("tokio-ut").await;
720 assert!(!results.is_empty());
721 assert!(results.iter().any(|r| r.label.contains('-')));
722 }
723
724 #[test]
725 fn test_generate_inlay_hints_loading_state() {
726 let cache = Arc::new(deps_core::HttpCache::new());
727 let ecosystem = CargoEcosystem::new(cache);
728
729 let parse_result = MockParseResult {
730 dependencies: vec![mock_dependency("tokio", Some("1.0"), 5, 5)],
731 };
732
733 let cached_versions = HashMap::new();
735 let resolved_versions = HashMap::new();
736
737 let config = EcosystemConfig {
738 loading_text: "⏳".to_string(),
739 show_loading_hints: true,
740 show_up_to_date_hints: true,
741 up_to_date_text: "✅".to_string(),
742 needs_update_text: "❌ {}".to_string(),
743 };
744
745 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
746 &parse_result,
747 &cached_versions,
748 &resolved_versions,
749 deps_core::LoadingState::Loading,
750 &config,
751 ));
752
753 assert_eq!(hints.len(), 1);
754 match &hints[0].label {
755 InlayHintLabel::String(s) => assert_eq!(s, "⏳", "Expected loading indicator"),
756 _ => panic!("Expected String label"),
757 }
758
759 if let Some(tower_lsp_server::ls_types::InlayHintTooltip::String(tooltip)) =
760 &hints[0].tooltip
761 {
762 assert_eq!(tooltip, "Fetching latest version...");
763 } else {
764 panic!("Expected tooltip for loading state");
765 }
766 }
767}