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