1use std::any::Any;
7use std::sync::Arc;
8use tower_lsp_server::ls_types::{CompletionItem, Position, Range, Uri};
9
10use deps_core::parser::DependencySource;
11use deps_core::{
12 Ecosystem, ParseResult as ParseResultTrait, Registry, Result, Version, completion::Completions,
13 lsp_helpers::EcosystemFormatter,
14};
15
16use crate::formatter::CargoFormatter;
17use crate::parser::CargoParseContext;
18use crate::registry::CargoRegistry;
19
20pub struct CargoEcosystem {
30 registry: Arc<CargoRegistry>,
31 formatter: CargoFormatter,
32 context: CargoParseContext,
38}
39
40enum CompletionSource {
43 NotInManifest,
48 Resolved(DependencySource),
50 Ambiguous,
54}
55
56fn resolve_completion_source(
58 parse_result: &dyn ParseResultTrait,
59 package_name: &deps_core::PackageName,
60) -> CompletionSource {
61 let mut sources = parse_result
62 .dependencies()
63 .into_iter()
64 .filter(|d| d.name() == package_name)
65 .map(deps_core::Dependency::source);
66
67 let Some(first) = sources.next() else {
68 return CompletionSource::NotInManifest;
69 };
70 if sources.all(|s| s == first) {
71 CompletionSource::Resolved(first)
72 } else {
73 tracing::warn!(
74 package = %package_name,
75 "ambiguous dependency source for version/feature completion; offering none"
76 );
77 CompletionSource::Ambiguous
78 }
79}
80
81impl CargoEcosystem {
82 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
88 Self::with_context(cache, CargoParseContext::default())
89 }
90
91 pub fn with_context(cache: Arc<deps_core::HttpCache>, ctx: CargoParseContext) -> Self {
96 Self {
97 registry: Arc::new(CargoRegistry::new(cache)),
98 formatter: CargoFormatter,
99 context: ctx,
100 }
101 }
102
103 async fn complete_package_names(&self, prefix: &str, range: Range) -> Vec<CompletionItem> {
104 deps_core::completion::complete_package_names_generic(
109 self.registry.as_ref(),
110 prefix,
111 20,
112 range,
113 )
114 .await
115 }
116
117 async fn complete_versions(
138 &self,
139 parse_result: &dyn ParseResultTrait,
140 position: Position,
141 prefix: &str,
142 freshness: deps_core::FreshnessSettings,
143 ) -> Vec<CompletionItem> {
144 deps_core::completion::complete_versions_at_position(
145 self.registry.as_ref(),
146 &self.formatter,
147 parse_result,
148 position,
149 prefix,
150 &['^', '~', '=', '<', '>'],
151 freshness,
152 )
153 .await
154 }
155
156 async fn complete_features(
166 &self,
167 parse_result: &dyn ParseResultTrait,
168 package_name: &deps_core::PackageName,
169 prefix: &str,
170 ) -> Vec<CompletionItem> {
171 use deps_core::completion::build_feature_completion;
172
173 let versions_result: Result<Vec<Box<dyn Version>>> =
174 match resolve_completion_source(parse_result, package_name) {
175 CompletionSource::Ambiguous => return vec![],
176 CompletionSource::NotInManifest
177 | CompletionSource::Resolved(DependencySource::Registry) => {
178 Registry::get_versions(self.registry.as_ref(), package_name).await
179 }
180 CompletionSource::Resolved(DependencySource::AlternateRegistry {
181 index, ..
182 }) => match self.registry.alternate_client(&index) {
183 Some(client) => Registry::get_versions(client.as_ref(), package_name).await,
184 None => return vec![],
185 },
186 CompletionSource::Resolved(_) => return vec![],
187 };
188
189 let versions = match versions_result {
190 Ok(v) => v,
191 Err(e) => {
192 tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
193 return vec![];
194 }
195 };
196
197 let latest = match versions.iter().find(|v| v.is_stable()) {
198 Some(v) => v,
199 None => {
200 tracing::warn!("No stable version found for '{}'", package_name);
201 return vec![];
202 }
203 };
204
205 let features = latest.features();
207 features
208 .into_iter()
209 .filter(|f| f.starts_with(prefix))
210 .map(|feature| build_feature_completion(&feature, package_name, None))
211 .collect()
212 }
213}
214
215impl deps_core::ecosystem::private::Sealed for CargoEcosystem {}
216
217impl Ecosystem for CargoEcosystem {
218 fn id(&self) -> &'static str {
219 "cargo"
220 }
221
222 fn display_name(&self) -> &'static str {
223 "Cargo (Rust)"
224 }
225
226 fn manifest_filenames(&self) -> &[&'static str] {
227 &["Cargo.toml"]
228 }
229
230 fn lockfile_filenames(&self) -> &[&'static str] {
231 &["Cargo.lock"]
232 }
233
234 fn parse_manifest<'a>(
235 &'a self,
236 content: &'a str,
237 uri: &'a Uri,
238 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
239 Box::pin(async move {
240 let result = crate::parser::parse_cargo_toml_with_context(content, uri, &self.context)?;
241 for (index, auth) in result.resolved_registries.clone() {
247 self.registry.register_alternate(index, auth);
248 }
249 Ok(Box::new(result) as Box<dyn ParseResultTrait>)
250 })
251 }
252
253 fn registry(&self) -> Arc<dyn Registry> {
254 self.registry.clone() as Arc<dyn Registry>
255 }
256
257 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
258 Some(Arc::new(crate::lockfile::CargoLockParser))
259 }
260
261 fn formatter(&self) -> &dyn EcosystemFormatter {
262 &self.formatter
263 }
264
265 fn generate_completions<'a>(
266 &'a self,
267 parse_result: &'a dyn ParseResultTrait,
268 position: Position,
269 content: &'a str,
270 freshness: deps_core::FreshnessSettings,
271 ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
272 Box::pin(async move {
273 use deps_core::completion::{CompletionContext, detect_completion_context};
274
275 let context = detect_completion_context(parse_result, position, content);
276
277 match context {
278 CompletionContext::PackageName { prefix, range } => {
279 self.complete_package_names(&prefix, range).await
280 }
281 CompletionContext::Version { prefix, .. } => {
282 self.complete_versions(parse_result, position, &prefix, freshness)
283 .await
284 }
285 CompletionContext::Feature {
286 package_name,
287 prefix,
288 } => {
289 self.complete_features(parse_result, &package_name, &prefix)
290 .await
291 }
292 CompletionContext::None => vec![],
293 }
294 .into()
295 })
296 }
297
298 fn as_any(&self) -> &dyn Any {
299 self
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::types::{DependencySection, DependencySource, ParsedDependency};
307 use deps_core::{EcosystemConfig, PackageVersions, VersionData};
308 use std::collections::HashMap;
309 use tower_lsp_server::ls_types::{InlayHintLabel, Position, Range};
310
311 fn pkg(s: &str) -> deps_core::PackageName {
312 deps_core::PackageName::new(s)
313 }
314
315 fn mock_dependency(
317 name: &str,
318 version: Option<&str>,
319 name_line: u32,
320 version_line: u32,
321 ) -> ParsedDependency {
322 ParsedDependency {
323 name: name.into(),
324 name_range: Range::new(
325 Position::new(name_line, 0),
326 Position::new(name_line, name.len() as u32),
327 ),
328 version_req: version.map(Into::into),
329 version_range: version.map(|_| {
330 Range::new(
331 Position::new(version_line, 0),
332 Position::new(version_line, 10),
333 )
334 }),
335 features: vec![],
336 features_range: None,
337 source: DependencySource::Registry,
338 section: DependencySection::Dependencies,
339 }
340 }
341
342 struct MockParseResult {
344 dependencies: Vec<ParsedDependency>,
345 }
346
347 impl deps_core::ParseResult for MockParseResult {
348 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
349 self.dependencies
350 .iter()
351 .map(|d| d as &dyn deps_core::Dependency)
352 .collect()
353 }
354
355 fn workspace_root(&self) -> Option<&std::path::Path> {
356 None
357 }
358
359 fn uri(&self) -> &Uri {
360 static URI: std::sync::LazyLock<Uri> =
361 std::sync::LazyLock::new(|| deps_core::test_util::test_uri("/test/Cargo.toml"));
362 &URI
363 }
364
365 fn as_any(&self) -> &dyn Any {
366 self
367 }
368 }
369
370 fn empty_parse_result() -> MockParseResult {
375 MockParseResult {
376 dependencies: vec![],
377 }
378 }
379
380 #[test]
381 fn test_ecosystem_id() {
382 let cache = Arc::new(deps_core::HttpCache::new());
383 let ecosystem = CargoEcosystem::new(cache);
384 assert_eq!(ecosystem.id(), "cargo");
385 }
386
387 #[test]
388 fn test_ecosystem_display_name() {
389 let cache = Arc::new(deps_core::HttpCache::new());
390 let ecosystem = CargoEcosystem::new(cache);
391 assert_eq!(ecosystem.display_name(), "Cargo (Rust)");
392 }
393
394 #[test]
395 fn test_ecosystem_manifest_filenames() {
396 let cache = Arc::new(deps_core::HttpCache::new());
397 let ecosystem = CargoEcosystem::new(cache);
398 assert_eq!(ecosystem.manifest_filenames(), &["Cargo.toml"]);
399 }
400
401 #[test]
402 fn test_ecosystem_lockfile_filenames() {
403 let cache = Arc::new(deps_core::HttpCache::new());
404 let ecosystem = CargoEcosystem::new(cache);
405 assert_eq!(ecosystem.lockfile_filenames(), &["Cargo.lock"]);
406 }
407
408 #[test]
409 fn test_generate_inlay_hints_up_to_date_exact_match() {
410 let cache = Arc::new(deps_core::HttpCache::new());
411 let ecosystem = CargoEcosystem::new(cache);
412
413 let parse_result = MockParseResult {
414 dependencies: vec![mock_dependency("serde", Some("1.0.214"), 5, 5)],
415 };
416
417 let mut cached_versions = HashMap::new();
418 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
419
420 let config = EcosystemConfig {
421 loading_text: "⏳".to_string(),
422 show_loading_hints: true,
423 offline: false,
424 show_up_to_date_hints: true,
425 up_to_date_text: "✅".to_string(),
426 needs_update_text: "❌ {}".to_string(),
427 };
428
429 let mut resolved_versions = HashMap::new();
431 resolved_versions.insert("serde".into(), "1.0.214".into());
432 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
433 &parse_result,
434 VersionData::new(&cached_versions, &resolved_versions),
435 deps_core::LoadingState::Loaded,
436 &config,
437 ));
438
439 assert_eq!(hints.len(), 1);
440 match &hints[0].label {
441 InlayHintLabel::String(s) => assert_eq!(s, "✅ 1.0.214"),
442 _ => panic!("Expected String label"),
443 }
444 }
445
446 #[test]
447 fn test_generate_inlay_hints_up_to_date_caret_version() {
448 let cache = Arc::new(deps_core::HttpCache::new());
449 let ecosystem = CargoEcosystem::new(cache);
450
451 let parse_result = MockParseResult {
452 dependencies: vec![mock_dependency("serde", Some("^1.0"), 5, 5)],
453 };
454
455 let mut cached_versions = HashMap::new();
456 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
457
458 let config = EcosystemConfig {
459 loading_text: "⏳".to_string(),
460 show_loading_hints: true,
461 offline: false,
462 show_up_to_date_hints: true,
463 up_to_date_text: "✅".to_string(),
464 needs_update_text: "❌ {}".to_string(),
465 };
466
467 let mut resolved_versions = HashMap::new();
469 resolved_versions.insert("serde".into(), "1.0.214".into());
470 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
471 &parse_result,
472 VersionData::new(&cached_versions, &resolved_versions),
473 deps_core::LoadingState::Loaded,
474 &config,
475 ));
476
477 assert_eq!(hints.len(), 1);
478 match &hints[0].label {
479 InlayHintLabel::String(s) => assert_eq!(s, "✅ 1.0.214"),
480 _ => panic!("Expected String label"),
481 }
482 }
483
484 #[test]
485 fn test_generate_inlay_hints_needs_update() {
486 let cache = Arc::new(deps_core::HttpCache::new());
487 let ecosystem = CargoEcosystem::new(cache);
488
489 let parse_result = MockParseResult {
490 dependencies: vec![mock_dependency("serde", Some("1.0.100"), 5, 5)],
491 };
492
493 let mut cached_versions = HashMap::new();
494 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
495
496 let config = EcosystemConfig {
497 loading_text: "⏳".to_string(),
498 show_loading_hints: true,
499 offline: false,
500 show_up_to_date_hints: true,
501 up_to_date_text: "✅".to_string(),
502 needs_update_text: "❌ {}".to_string(),
503 };
504
505 let resolved_versions = HashMap::new();
506 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
507 &parse_result,
508 VersionData::new(&cached_versions, &resolved_versions),
509 deps_core::LoadingState::Loaded,
510 &config,
511 ));
512
513 assert_eq!(hints.len(), 1);
514 match &hints[0].label {
515 InlayHintLabel::String(s) => assert_eq!(s, "❌ 1.0.214"),
516 _ => panic!("Expected String label"),
517 }
518 }
519
520 #[test]
521 fn test_generate_inlay_hints_hide_up_to_date() {
522 let cache = Arc::new(deps_core::HttpCache::new());
523 let ecosystem = CargoEcosystem::new(cache);
524
525 let parse_result = MockParseResult {
526 dependencies: vec![mock_dependency("serde", Some("1.0.214"), 5, 5)],
527 };
528
529 let mut cached_versions = HashMap::new();
530 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
531
532 let config = EcosystemConfig {
533 loading_text: "⏳".to_string(),
534 show_loading_hints: true,
535 offline: false,
536 show_up_to_date_hints: false,
537 up_to_date_text: "✅".to_string(),
538 needs_update_text: "❌ {}".to_string(),
539 };
540
541 let mut resolved_versions = HashMap::new();
543 resolved_versions.insert("serde".into(), "1.0.214".into());
544 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
545 &parse_result,
546 VersionData::new(&cached_versions, &resolved_versions),
547 deps_core::LoadingState::Loaded,
548 &config,
549 ));
550
551 assert_eq!(hints.len(), 0);
552 }
553
554 #[test]
555 fn test_generate_inlay_hints_no_version_range() {
556 let cache = Arc::new(deps_core::HttpCache::new());
557 let ecosystem = CargoEcosystem::new(cache);
558
559 let mut dep = mock_dependency("serde", Some("1.0.214"), 5, 5);
560 dep.version_range = None;
561
562 let parse_result = MockParseResult {
563 dependencies: vec![dep],
564 };
565
566 let mut cached_versions = HashMap::new();
567 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
568
569 let config = EcosystemConfig {
570 loading_text: "⏳".to_string(),
571 show_loading_hints: true,
572 offline: false,
573 show_up_to_date_hints: true,
574 up_to_date_text: "✅".to_string(),
575 needs_update_text: "❌ {}".to_string(),
576 };
577
578 let resolved_versions = HashMap::new();
579 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
580 &parse_result,
581 VersionData::new(&cached_versions, &resolved_versions),
582 deps_core::LoadingState::Loaded,
583 &config,
584 ));
585
586 assert_eq!(hints.len(), 0);
587 }
588
589 #[test]
590 fn test_generate_inlay_hints_caret_edge_case() {
591 let cache = Arc::new(deps_core::HttpCache::new());
592 let ecosystem = CargoEcosystem::new(cache);
593
594 let dep = mock_dependency("serde", Some("^"), 5, 5);
596
597 let parse_result = MockParseResult {
598 dependencies: vec![dep],
599 };
600
601 let mut cached_versions = HashMap::new();
602 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
603
604 let config = EcosystemConfig {
605 loading_text: "⏳".to_string(),
606 show_loading_hints: true,
607 offline: false,
608 show_up_to_date_hints: true,
609 up_to_date_text: "✅".to_string(),
610 needs_update_text: "❌ {}".to_string(),
611 };
612
613 let resolved_versions = HashMap::new();
615 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
616 &parse_result,
617 VersionData::new(&cached_versions, &resolved_versions),
618 deps_core::LoadingState::Loaded,
619 &config,
620 ));
621
622 assert_eq!(hints.len(), 1);
623 }
624
625 #[test]
626 fn test_as_any() {
627 let cache = Arc::new(deps_core::HttpCache::new());
628 let ecosystem = CargoEcosystem::new(cache);
629
630 let any = ecosystem.as_any();
632 assert!(any.is::<CargoEcosystem>());
633 }
634
635 #[tokio::test]
636 async fn test_package_name_completion_context_has_real_range() {
637 let cache = Arc::new(deps_core::HttpCache::new());
640 let ecosystem = CargoEcosystem::new(cache);
641 let content = "[dependencies]\nserd = \"1.0\"\n";
642 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
643
644 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
645 let position = Position::new(1, 3); let context = deps_core::completion::detect_completion_context(
648 parse_result.as_ref(),
649 position,
650 content,
651 );
652
653 match context {
654 deps_core::completion::CompletionContext::PackageName { prefix, range } => {
655 assert_eq!(prefix, "ser");
656 assert_ne!(range, Range::default());
657 assert_eq!(range, Range::new(Position::new(1, 0), Position::new(1, 4)));
658 }
659 other => panic!("Expected PackageName context, got {other:?}"),
660 }
661 }
662
663 #[tokio::test]
664 async fn test_complete_package_names_minimum_prefix() {
665 let cache = Arc::new(deps_core::HttpCache::new());
666 let ecosystem = CargoEcosystem::new(cache);
667
668 let results = ecosystem
670 .complete_package_names("s", Range::default())
671 .await;
672 assert!(results.is_empty());
673
674 let results = ecosystem.complete_package_names("", Range::default()).await;
676 assert!(results.is_empty());
677 }
678
679 #[tokio::test]
680 #[ignore] async fn test_complete_package_names_real_search() {
682 let cache = Arc::new(deps_core::HttpCache::new());
683 let ecosystem = CargoEcosystem::new(cache);
684
685 let results = ecosystem
686 .complete_package_names("serd", Range::default())
687 .await;
688 assert!(!results.is_empty());
689 assert!(results.iter().any(|r| r.label == "serde"));
690 }
691
692 #[tokio::test]
693 #[ignore] async fn test_complete_versions_real() {
695 let cache = Arc::new(deps_core::HttpCache::new());
696 let ecosystem = CargoEcosystem::new(cache);
697 let dep = mock_dependency("serde", Some("1.0"), 0, 0);
698 let position = dep.version_range.unwrap().start;
699 let parse_result = MockParseResult {
700 dependencies: vec![dep],
701 };
702
703 let results = ecosystem
704 .complete_versions(
705 &parse_result,
706 position,
707 "1.0",
708 deps_core::FreshnessSettings::default(),
709 )
710 .await;
711 assert!(!results.is_empty());
712 assert!(results.iter().all(|r| r.label.starts_with("1.0")));
713 }
714
715 #[tokio::test]
716 #[ignore] async fn test_complete_versions_with_operator() {
718 let cache = Arc::new(deps_core::HttpCache::new());
719 let ecosystem = CargoEcosystem::new(cache);
720 let dep = mock_dependency("serde", Some("^1.0"), 0, 0);
721 let position = dep.version_range.unwrap().start;
722 let parse_result = MockParseResult {
723 dependencies: vec![dep],
724 };
725
726 let results = ecosystem
727 .complete_versions(
728 &parse_result,
729 position,
730 "^1.0",
731 deps_core::FreshnessSettings::default(),
732 )
733 .await;
734 assert!(!results.is_empty());
735 assert!(results.iter().all(|r| r.label.starts_with("1.0")));
736 }
737
738 #[tokio::test]
739 #[ignore] async fn test_complete_features_real() {
741 let cache = Arc::new(deps_core::HttpCache::new());
742 let ecosystem = CargoEcosystem::new(cache);
743
744 let results = ecosystem
745 .complete_features(&empty_parse_result(), &pkg("serde"), "")
746 .await;
747 assert!(!results.is_empty());
748 assert!(results.iter().any(|r| r.label == "derive"));
749 }
750
751 #[tokio::test]
752 #[ignore] async fn test_complete_features_with_prefix() {
754 let cache = Arc::new(deps_core::HttpCache::new());
755 let ecosystem = CargoEcosystem::new(cache);
756
757 let results = ecosystem
758 .complete_features(&empty_parse_result(), &pkg("serde"), "der")
759 .await;
760 assert!(!results.is_empty());
761 assert!(results.iter().all(|r| r.label.starts_with("der")));
762 }
763
764 #[tokio::test]
769 async fn test_complete_versions_same_name_different_sources_routes_by_position() {
770 let cache = Arc::new(deps_core::HttpCache::new());
771 let ecosystem = CargoEcosystem::new(cache);
772
773 let mut registry_dep = mock_dependency("shared-name", Some("1.0"), 0, 0);
774 registry_dep.source = DependencySource::Registry;
775 let mut alternate_dep = mock_dependency("shared-name", Some("1.0"), 1, 1);
776 alternate_dep.source = DependencySource::AlternateRegistry {
777 index: "https://index.mycorp.dev/never-registered".into(),
778 mirrors_crates_io: false,
779 };
780 let alternate_position = alternate_dep.version_range.unwrap().start;
781 let parse_result = MockParseResult {
782 dependencies: vec![registry_dep, alternate_dep],
783 };
784
785 let results = ecosystem
790 .complete_versions(
791 &parse_result,
792 alternate_position,
793 "1",
794 deps_core::FreshnessSettings::default(),
795 )
796 .await;
797 assert!(
798 results.is_empty(),
799 "unregistered alternate index must offer no completions"
800 );
801 }
802
803 #[tokio::test]
809 async fn test_complete_versions_routes_to_registered_alternate_client() {
810 let mut server = mockito::Server::new_async().await;
811 let _mock = server
812 .mock("GET", "/se/rd/serde")
813 .with_status(200)
814 .with_body(
815 "{\"name\":\"serde\",\"vers\":\"1.0.0\",\"yanked\":false,\"features\":{},\"deps\":[]}\n\
816 {\"name\":\"serde\",\"vers\":\"1.5.0\",\"yanked\":false,\"features\":{},\"deps\":[]}\n",
817 )
818 .create_async()
819 .await;
820
821 let cache = Arc::new(deps_core::HttpCache::new());
822 let ecosystem = CargoEcosystem::new(cache);
823
824 let policy = deps_core::net_policy::RegistryAccessPolicy::default();
825 let registry_index = crate::config::RegistryIndex::new(
826 &server.url(),
827 crate::config::IndexTrust::Trusted,
828 &policy,
829 )
830 .unwrap();
831 let index_key = registry_index.as_str().to_string();
832 ecosystem.registry.register_alternate(registry_index, None);
833
834 let mut dep = mock_dependency("serde", Some("1.0"), 0, 0);
835 dep.source = DependencySource::AlternateRegistry {
836 index: index_key,
837 mirrors_crates_io: false,
838 };
839 let position = dep.version_range.unwrap().start;
840 let parse_result = MockParseResult {
841 dependencies: vec![dep],
842 };
843
844 let results = ecosystem
845 .complete_versions(
846 &parse_result,
847 position,
848 "1.",
849 deps_core::FreshnessSettings::default(),
850 )
851 .await;
852 assert!(
853 !results.is_empty(),
854 "a registered alternate index must route completions through its own client"
855 );
856 }
857
858 #[tokio::test]
861 async fn test_complete_features_ambiguous_source_offers_nothing() {
862 let cache = Arc::new(deps_core::HttpCache::new());
863 let ecosystem = CargoEcosystem::new(cache);
864
865 let mut registry_dep = mock_dependency("shared-name", Some("1.0"), 0, 0);
866 registry_dep.source = DependencySource::Registry;
867 let mut alternate_dep = mock_dependency("shared-name", Some("1.0"), 1, 1);
868 alternate_dep.source = DependencySource::AlternateRegistry {
869 index: "https://index.mycorp.dev".into(),
870 mirrors_crates_io: false,
871 };
872 let parse_result = MockParseResult {
873 dependencies: vec![registry_dep, alternate_dep],
874 };
875
876 let results = ecosystem
877 .complete_features(&parse_result, &pkg("shared-name"), "")
878 .await;
879 assert!(
880 results.is_empty(),
881 "an ambiguous source must offer no feature completions"
882 );
883 }
884
885 #[tokio::test]
886 async fn test_complete_versions_unknown_package() {
887 let cache = Arc::new(deps_core::HttpCache::new());
888 let ecosystem = CargoEcosystem::new(cache);
889 let dep = mock_dependency("this-package-does-not-exist-12345", Some("1.0"), 0, 0);
890 let position = dep.version_range.unwrap().start;
891 let parse_result = MockParseResult {
892 dependencies: vec![dep],
893 };
894
895 let results = ecosystem
897 .complete_versions(
898 &parse_result,
899 position,
900 "1.0",
901 deps_core::FreshnessSettings::default(),
902 )
903 .await;
904 assert!(results.is_empty());
905 }
906
907 #[tokio::test]
908 async fn test_complete_features_unknown_package() {
909 let cache = Arc::new(deps_core::HttpCache::new());
910 let ecosystem = CargoEcosystem::new(cache);
911
912 let results = ecosystem
914 .complete_features(
915 &empty_parse_result(),
916 &pkg("this-package-does-not-exist-12345"),
917 "",
918 )
919 .await;
920 assert!(results.is_empty());
921 }
922
923 #[tokio::test]
924 async fn test_complete_package_names_special_characters() {
925 let cache = Arc::new(deps_core::HttpCache::new());
926 let ecosystem = CargoEcosystem::new(cache);
927
928 let results = ecosystem
930 .complete_package_names("tokio-ut", Range::default())
931 .await;
932 assert!(results.is_empty() || !results.is_empty());
934 }
935
936 #[tokio::test]
937 async fn test_complete_package_names_max_length() {
938 let cache = Arc::new(deps_core::HttpCache::new());
939 let ecosystem = CargoEcosystem::new(cache);
940
941 let long_prefix = "a".repeat(201);
943 let results = ecosystem
944 .complete_package_names(&long_prefix, Range::default())
945 .await;
946 assert!(results.is_empty());
947
948 let max_prefix = "a".repeat(100);
950 let results = ecosystem
951 .complete_package_names(&max_prefix, Range::default())
952 .await;
953 assert!(results.is_empty() || !results.is_empty());
955 }
956
957 #[tokio::test]
958 #[ignore] async fn test_complete_versions_limit_20() {
960 let cache = Arc::new(deps_core::HttpCache::new());
961 let ecosystem = CargoEcosystem::new(cache);
962
963 let dep = mock_dependency("serde", Some("1.0"), 0, 0);
965 let position = dep.version_range.unwrap().start;
966 let parse_result = MockParseResult {
967 dependencies: vec![dep],
968 };
969 let results = ecosystem
970 .complete_versions(
971 &parse_result,
972 position,
973 "1",
974 deps_core::FreshnessSettings::default(),
975 )
976 .await;
977 assert!(results.len() <= 20);
978 }
979
980 #[tokio::test]
981 #[ignore] async fn test_complete_features_empty_list() {
983 let cache = Arc::new(deps_core::HttpCache::new());
984 let ecosystem = CargoEcosystem::new(cache);
985
986 let results = ecosystem
989 .complete_features(&empty_parse_result(), &pkg("anyhow"), "nonexistent")
990 .await;
991 assert!(results.is_empty());
992 }
993
994 #[tokio::test]
995 #[ignore] async fn test_complete_package_names_special_chars_real() {
997 let cache = Arc::new(deps_core::HttpCache::new());
998 let ecosystem = CargoEcosystem::new(cache);
999
1000 let results = ecosystem
1002 .complete_package_names("tokio-ut", Range::default())
1003 .await;
1004 assert!(!results.is_empty());
1005 assert!(results.iter().any(|r| r.label.contains('-')));
1006 }
1007
1008 #[test]
1009 fn test_generate_inlay_hints_loading_state() {
1010 let cache = Arc::new(deps_core::HttpCache::new());
1011 let ecosystem = CargoEcosystem::new(cache);
1012
1013 let parse_result = MockParseResult {
1014 dependencies: vec![mock_dependency("tokio", Some("1.0"), 5, 5)],
1015 };
1016
1017 let cached_versions = HashMap::new();
1019 let resolved_versions = HashMap::new();
1020
1021 let config = EcosystemConfig {
1022 loading_text: "⏳".to_string(),
1023 show_loading_hints: true,
1024 offline: false,
1025 show_up_to_date_hints: true,
1026 up_to_date_text: "✅".to_string(),
1027 needs_update_text: "❌ {}".to_string(),
1028 };
1029
1030 let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
1031 &parse_result,
1032 VersionData::new(&cached_versions, &resolved_versions),
1033 deps_core::LoadingState::Loading,
1034 &config,
1035 ));
1036
1037 assert_eq!(hints.len(), 1);
1038 match &hints[0].label {
1039 InlayHintLabel::String(s) => assert_eq!(s, "⏳", "Expected loading indicator"),
1040 _ => panic!("Expected String label"),
1041 }
1042
1043 if let Some(tower_lsp_server::ls_types::InlayHintTooltip::String(tooltip)) =
1044 &hints[0].tooltip
1045 {
1046 assert_eq!(tooltip, "Fetching latest version...");
1047 } else {
1048 panic!("Expected tooltip for loading state");
1049 }
1050 }
1051}