1use std::collections::HashMap;
4use tower_lsp_server::ls_types::{
5 CodeAction, CodeActionKind, Diagnostic, DiagnosticSeverity, Hover, HoverContents, InlayHint,
6 InlayHintKind, InlayHintLabel, InlayHintTooltip, MarkupContent, MarkupKind, Position, Range,
7 TextEdit, Uri, WorkspaceEdit,
8};
9
10use crate::{Dependency, EcosystemConfig, ParseResult, Registry};
11
12pub fn ranges_overlap(range: Range, position: Position) -> bool {
14 !(range.end.line < position.line
15 || (range.end.line == position.line && range.end.character <= position.character)
16 || position.line < range.start.line
17 || (position.line == range.start.line && position.character < range.start.character))
18}
19
20pub fn is_same_major_minor(v1: &str, v2: &str) -> bool {
22 if v1.is_empty() || v2.is_empty() {
23 return false;
24 }
25
26 let mut parts1 = v1.split('.');
27 let mut parts2 = v2.split('.');
28
29 if parts1.next() != parts2.next() {
30 return false;
31 }
32
33 match (parts1.next(), parts2.next()) {
34 (Some(m1), Some(m2)) => m1 == m2,
35 _ => true,
36 }
37}
38
39pub trait EcosystemFormatter: Send + Sync {
41 fn normalize_package_name(&self, name: &str) -> String {
43 name.to_string()
44 }
45
46 fn format_version_for_code_action(&self, version: &str) -> String;
48
49 fn version_satisfies_requirement(&self, version: &str, requirement: &str) -> bool {
51 if let Some(req) = requirement.strip_prefix('^') {
54 let req_parts: Vec<&str> = req.split('.').collect();
55 let ver_parts: Vec<&str> = version.split('.').collect();
56
57 if req_parts.first() != ver_parts.first() {
59 return false;
60 }
61
62 if req_parts.first().is_some_and(|m| *m != "0") {
64 return true;
65 }
66
67 if req_parts.len() >= 2 && ver_parts.len() >= 2 {
69 return req_parts[1] == ver_parts[1];
70 }
71
72 return true;
73 }
74
75 if let Some(req) = requirement.strip_prefix('~') {
78 return is_same_major_minor(req, version);
79 }
80
81 let req_parts: Vec<&str> = requirement.split('.').collect();
83 let is_partial_version = req_parts.len() <= 2;
84
85 version == requirement
86 || (is_partial_version && is_same_major_minor(requirement, version))
87 || (is_partial_version && version.starts_with(requirement))
88 }
89
90 fn package_url(&self, name: &str) -> String;
92
93 fn yanked_message(&self) -> &'static str {
95 "This version has been yanked"
96 }
97
98 fn yanked_label(&self) -> &'static str {
100 "*(yanked)*"
101 }
102
103 fn is_position_on_dependency(&self, dep: &dyn Dependency, position: Position) -> bool {
105 dep.version_range()
106 .is_some_and(|r| ranges_overlap(r, position))
107 }
108}
109
110pub fn generate_inlay_hints(
111 parse_result: &dyn ParseResult,
112 cached_versions: &HashMap<String, String>,
113 resolved_versions: &HashMap<String, String>,
114 loading_state: crate::LoadingState,
115 config: &EcosystemConfig,
116 formatter: &dyn EcosystemFormatter,
117) -> Vec<InlayHint> {
118 let deps = parse_result.dependencies();
119 let mut hints = Vec::with_capacity(deps.len());
120
121 for dep in deps {
122 let Some(version_range) = dep.version_range() else {
123 continue;
124 };
125
126 let normalized_name = formatter.normalize_package_name(dep.name());
127 let latest_version = cached_versions
128 .get(&normalized_name)
129 .or_else(|| cached_versions.get(dep.name()));
130 let resolved_version = resolved_versions
131 .get(&normalized_name)
132 .or_else(|| resolved_versions.get(dep.name()));
133
134 if loading_state == crate::LoadingState::Loading
136 && config.show_loading_hints
137 && latest_version.is_none()
138 {
139 hints.push(InlayHint {
140 position: version_range.end,
141 label: InlayHintLabel::String(config.loading_text.clone()),
142 kind: Some(InlayHintKind::TYPE),
143 tooltip: Some(InlayHintTooltip::String(
144 "Fetching latest version...".to_string(),
145 )),
146 padding_left: Some(true),
147 padding_right: None,
148 text_edits: None,
149 data: None,
150 });
151 continue;
152 }
153
154 let Some(latest) = latest_version else {
155 if let Some(resolved) = resolved_version
156 && config.show_up_to_date_hints
157 {
158 hints.push(InlayHint {
159 position: version_range.end,
160 label: InlayHintLabel::String(format!(
161 "{} {}",
162 config.up_to_date_text, resolved
163 )),
164 kind: Some(InlayHintKind::TYPE),
165 padding_left: Some(true),
166 padding_right: None,
167 text_edits: None,
168 tooltip: None,
169 data: None,
170 });
171 }
172 continue;
173 };
174
175 let is_up_to_date = if let Some(resolved) = resolved_version {
179 resolved.as_str() == latest.as_str()
180 } else {
181 let version_req = dep.version_requirement().unwrap_or("");
182 formatter.version_satisfies_requirement(latest, version_req)
183 };
184
185 let label_text = if is_up_to_date {
186 if config.show_up_to_date_hints {
187 if let Some(resolved) = resolved_version {
188 format!("{} {}", config.up_to_date_text, resolved)
189 } else {
190 config.up_to_date_text.clone()
191 }
192 } else {
193 continue;
194 }
195 } else {
196 config.needs_update_text.replace("{}", latest)
197 };
198
199 hints.push(InlayHint {
200 position: version_range.end,
201 label: InlayHintLabel::String(label_text),
202 kind: Some(InlayHintKind::TYPE),
203 padding_left: Some(true),
204 padding_right: None,
205 text_edits: None,
206 tooltip: None,
207 data: None,
208 });
209 }
210
211 hints
212}
213
214pub async fn generate_hover<R: Registry + ?Sized>(
215 parse_result: &dyn ParseResult,
216 position: Position,
217 cached_versions: &HashMap<String, String>,
218 resolved_versions: &HashMap<String, String>,
219 registry: &R,
220 formatter: &dyn EcosystemFormatter,
221) -> Option<Hover> {
222 use std::fmt::Write;
223
224 let dep = parse_result.dependencies().into_iter().find(|d| {
225 let on_name = ranges_overlap(d.name_range(), position);
226 let on_version = d
227 .version_range()
228 .is_some_and(|r| ranges_overlap(r, position));
229 on_name || on_version
230 })?;
231
232 let versions = registry.get_versions(dep.name()).await.ok()?;
233
234 let url = formatter.package_url(dep.name());
235
236 let mut markdown = String::with_capacity(512);
238 write!(&mut markdown, "# [{}]({})\n\n", dep.name(), url).unwrap();
239
240 let normalized_name = formatter.normalize_package_name(dep.name());
241
242 let resolved = resolved_versions
243 .get(&normalized_name)
244 .or_else(|| resolved_versions.get(dep.name()));
245 if let Some(resolved_ver) = resolved {
246 write!(&mut markdown, "**Current**: `{}`\n\n", resolved_ver).unwrap();
247 } else if let Some(version_req) = dep.version_requirement() {
248 write!(&mut markdown, "**Requirement**: `{}`\n\n", version_req).unwrap();
249 }
250
251 let latest = cached_versions
252 .get(&normalized_name)
253 .or_else(|| cached_versions.get(dep.name()));
254 if let Some(latest_ver) = latest {
255 write!(&mut markdown, "**Latest**: `{}`\n\n", latest_ver).unwrap();
256 }
257
258 markdown.push_str("**Recent versions**:\n");
259 for (i, version) in versions.iter().take(8).enumerate() {
260 if i == 0 {
261 writeln!(&mut markdown, "- {} *(latest)*", version.version_string()).unwrap();
262 } else if version.is_yanked() {
263 writeln!(
264 &mut markdown,
265 "- {} {}",
266 version.version_string(),
267 formatter.yanked_label()
268 )
269 .unwrap();
270 } else {
271 writeln!(&mut markdown, "- {}", version.version_string()).unwrap();
272 }
273 }
274
275 markdown.push_str("\n---\n⌨️ **Press `Cmd+.` to update version**");
276
277 Some(Hover {
278 contents: HoverContents::Markup(MarkupContent {
279 kind: MarkupKind::Markdown,
280 value: markdown,
281 }),
282 range: Some(dep.name_range()),
283 })
284}
285
286pub async fn generate_code_actions<R: Registry + ?Sized>(
287 parse_result: &dyn ParseResult,
288 position: Position,
289 uri: &Uri,
290 registry: &R,
291 formatter: &dyn EcosystemFormatter,
292) -> Vec<CodeAction> {
293 use crate::completion::prepare_version_display_items;
294
295 let deps = parse_result.dependencies();
296 let mut actions = Vec::with_capacity(deps.len().min(5));
297
298 let Some(dep) = deps
299 .into_iter()
300 .find(|d| formatter.is_position_on_dependency(*d, position))
301 else {
302 return actions;
303 };
304
305 let Some(version_range) = dep.version_range() else {
306 return actions;
307 };
308
309 let Ok(versions) = registry.get_versions(dep.name()).await else {
310 return actions;
311 };
312
313 let display_items = prepare_version_display_items(&versions, dep.name());
314
315 for item in display_items {
316 let new_text = formatter.format_version_for_code_action(&item.version);
317
318 let mut edits = HashMap::new();
319 edits.insert(
320 uri.clone(),
321 vec![TextEdit {
322 range: version_range,
323 new_text,
324 }],
325 );
326
327 actions.push(CodeAction {
328 title: item.label,
329 kind: Some(CodeActionKind::REFACTOR),
330 edit: Some(WorkspaceEdit {
331 changes: Some(edits),
332 ..Default::default()
333 }),
334 is_preferred: Some(item.is_latest),
335 ..Default::default()
336 });
337 }
338
339 actions
340}
341
342pub fn generate_diagnostics_from_cache(
354 parse_result: &dyn ParseResult,
355 cached_versions: &HashMap<String, String>,
356 _resolved_versions: &HashMap<String, String>,
357 formatter: &dyn EcosystemFormatter,
358) -> Vec<Diagnostic> {
359 let deps = parse_result.dependencies();
360 let mut diagnostics = Vec::with_capacity(deps.len());
361
362 for dep in deps {
363 let normalized_name = formatter.normalize_package_name(dep.name());
364 let latest_version = cached_versions
365 .get(&normalized_name)
366 .or_else(|| cached_versions.get(dep.name()));
367
368 let Some(latest) = latest_version else {
369 diagnostics.push(Diagnostic {
370 range: dep.name_range(),
371 severity: Some(DiagnosticSeverity::WARNING),
372 message: format!("Unknown package '{}' (or failed to fetch)", dep.name()),
373 source: Some("deps-lsp".into()),
374 ..Default::default()
375 });
376 continue;
377 };
378
379 let Some(version_range) = dep.version_range() else {
380 continue;
381 };
382
383 let version_req = dep.version_requirement().unwrap_or("");
384 let requirement_allows_latest =
385 formatter.version_satisfies_requirement(latest, version_req);
386
387 if !requirement_allows_latest {
388 diagnostics.push(Diagnostic {
389 range: version_range,
390 severity: Some(DiagnosticSeverity::HINT),
391 message: format!("Newer version available: {}", latest),
392 source: Some("deps-lsp".into()),
393 ..Default::default()
394 });
395 }
396 }
397
398 diagnostics
399}
400
401#[allow(dead_code)]
406pub async fn generate_diagnostics<R: Registry + ?Sized>(
407 parse_result: &dyn ParseResult,
408 registry: &R,
409 formatter: &dyn EcosystemFormatter,
410) -> Vec<Diagnostic> {
411 let deps = parse_result.dependencies();
412 let mut diagnostics = Vec::with_capacity(deps.len());
413
414 for dep in deps {
415 let versions = match registry.get_versions(dep.name()).await {
416 Ok(v) => v,
417 Err(_) => {
418 diagnostics.push(Diagnostic {
419 range: dep.name_range(),
420 severity: Some(DiagnosticSeverity::WARNING),
421 message: format!("Unknown package '{}'", dep.name()),
422 source: Some("deps-lsp".into()),
423 ..Default::default()
424 });
425 continue;
426 }
427 };
428
429 let Some(version_req) = dep.version_requirement() else {
430 continue;
431 };
432 let Some(version_range) = dep.version_range() else {
433 continue;
434 };
435
436 let matching = registry
437 .get_latest_matching(dep.name(), version_req)
438 .await
439 .ok()
440 .flatten();
441
442 if let Some(current) = matching {
443 if current.is_yanked() {
444 diagnostics.push(Diagnostic {
445 range: version_range,
446 severity: Some(DiagnosticSeverity::WARNING),
447 message: formatter.yanked_message().into(),
448 source: Some("deps-lsp".into()),
449 ..Default::default()
450 });
451 }
452
453 let latest = crate::registry::find_latest_stable(&versions);
454 if let Some(latest) = latest
455 && latest.version_string() != current.version_string()
456 {
457 diagnostics.push(Diagnostic {
458 range: version_range,
459 severity: Some(DiagnosticSeverity::HINT),
460 message: format!("Newer version available: {}", latest.version_string()),
461 source: Some("deps-lsp".into()),
462 ..Default::default()
463 });
464 }
465 }
466 }
467
468 diagnostics
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_ranges_overlap_inside() {
477 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
478 let position = Position::new(5, 15);
479 assert!(ranges_overlap(range, position));
480 }
481
482 #[test]
483 fn test_ranges_overlap_at_start() {
484 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
485 let position = Position::new(5, 10);
486 assert!(ranges_overlap(range, position));
487 }
488
489 #[test]
490 fn test_ranges_overlap_at_end() {
491 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
492 let position = Position::new(5, 20);
493 assert!(!ranges_overlap(range, position));
494 }
495
496 #[test]
497 fn test_ranges_overlap_before() {
498 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
499 let position = Position::new(5, 5);
500 assert!(!ranges_overlap(range, position));
501 }
502
503 #[test]
504 fn test_ranges_overlap_after() {
505 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
506 let position = Position::new(5, 25);
507 assert!(!ranges_overlap(range, position));
508 }
509
510 #[test]
511 fn test_ranges_overlap_different_line_before() {
512 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
513 let position = Position::new(4, 15);
514 assert!(!ranges_overlap(range, position));
515 }
516
517 #[test]
518 fn test_ranges_overlap_different_line_after() {
519 let range = Range::new(Position::new(5, 10), Position::new(5, 20));
520 let position = Position::new(6, 15);
521 assert!(!ranges_overlap(range, position));
522 }
523
524 #[test]
525 fn test_ranges_overlap_multiline() {
526 let range = Range::new(Position::new(5, 10), Position::new(7, 5));
527 let position = Position::new(6, 0);
528 assert!(ranges_overlap(range, position));
529 }
530
531 #[test]
532 fn test_is_same_major_minor_full_match() {
533 assert!(is_same_major_minor("1.2.3", "1.2.9"));
534 }
535
536 #[test]
537 fn test_is_same_major_minor_exact_match() {
538 assert!(is_same_major_minor("1.2.3", "1.2.3"));
539 }
540
541 #[test]
542 fn test_is_same_major_minor_major_only_match() {
543 assert!(is_same_major_minor("1", "1.2.3"));
544 assert!(is_same_major_minor("1.2.3", "1"));
545 }
546
547 #[test]
548 fn test_is_same_major_minor_no_match_different_minor() {
549 assert!(!is_same_major_minor("1.2.3", "1.3.0"));
550 }
551
552 #[test]
553 fn test_is_same_major_minor_no_match_different_major() {
554 assert!(!is_same_major_minor("1.2.3", "2.2.3"));
555 }
556
557 #[test]
558 fn test_is_same_major_minor_empty_strings() {
559 assert!(!is_same_major_minor("", ""));
560 assert!(!is_same_major_minor("1.2.3", ""));
561 assert!(!is_same_major_minor("", "1.2.3"));
562 }
563
564 #[test]
565 fn test_is_same_major_minor_partial_versions() {
566 assert!(is_same_major_minor("1.2", "1.2.3"));
567 assert!(is_same_major_minor("1.2.3", "1.2"));
568 }
569
570 struct MockFormatter;
571
572 impl EcosystemFormatter for MockFormatter {
573 fn format_version_for_code_action(&self, version: &str) -> String {
574 format!("\"{}\"", version)
575 }
576
577 fn package_url(&self, name: &str) -> String {
578 format!("https://example.com/{}", name)
579 }
580 }
581
582 #[test]
583 fn test_ecosystem_formatter_defaults() {
584 let formatter = MockFormatter;
585 assert_eq!(formatter.normalize_package_name("test-pkg"), "test-pkg");
586 assert_eq!(formatter.yanked_message(), "This version has been yanked");
587 assert_eq!(formatter.yanked_label(), "*(yanked)*");
588 }
589
590 #[test]
591 fn test_ecosystem_formatter_version_satisfies() {
592 let formatter = MockFormatter;
593
594 assert!(formatter.version_satisfies_requirement("1.2.3", "1.2.3"));
595
596 assert!(formatter.version_satisfies_requirement("1.2.3", "^1.2"));
597 assert!(formatter.version_satisfies_requirement("1.2.3", "~1.2"));
598
599 assert!(formatter.version_satisfies_requirement("1.2.3", "1"));
600 assert!(formatter.version_satisfies_requirement("1.2.3", "1.2"));
601
602 assert!(!formatter.version_satisfies_requirement("1.2.3", "2.0.0"));
603 assert!(!formatter.version_satisfies_requirement("1.2.3", "1.3"));
604 }
605
606 #[test]
607 fn test_ecosystem_formatter_custom_normalize() {
608 struct PyPIFormatter;
609
610 impl EcosystemFormatter for PyPIFormatter {
611 fn normalize_package_name(&self, name: &str) -> String {
612 name.to_lowercase().replace('-', "_")
613 }
614
615 fn format_version_for_code_action(&self, version: &str) -> String {
616 format!(
617 ">={},<{}",
618 version,
619 version.split('.').next().unwrap_or("0")
620 )
621 }
622
623 fn package_url(&self, name: &str) -> String {
624 format!("https://pypi.org/project/{}", name)
625 }
626 }
627
628 let formatter = PyPIFormatter;
629 assert_eq!(
630 formatter.normalize_package_name("Test-Package"),
631 "test_package"
632 );
633 assert_eq!(
634 formatter.format_version_for_code_action("1.2.3"),
635 ">=1.2.3,<1"
636 );
637 assert_eq!(
638 formatter.package_url("requests"),
639 "https://pypi.org/project/requests"
640 );
641 }
642
643 #[test]
644 fn test_inlay_hint_exact_version_shows_update_needed() {
645 use std::any::Any;
646 use std::collections::HashMap;
647 use tower_lsp_server::ls_types::{Position, Range, Uri};
648
649 let formatter = MockFormatter;
650 let config = EcosystemConfig {
651 show_up_to_date_hints: true,
652 up_to_date_text: "✅".to_string(),
653 needs_update_text: "❌ {}".to_string(),
654 loading_text: "⏳".to_string(),
655 show_loading_hints: true,
656 };
657
658 struct MockParseResult {
659 deps: Vec<MockDep>,
660 uri: Uri,
661 }
662
663 impl ParseResult for MockParseResult {
664 fn dependencies(&self) -> Vec<&dyn Dependency> {
665 self.deps.iter().map(|d| d as &dyn Dependency).collect()
666 }
667 fn workspace_root(&self) -> Option<&std::path::Path> {
668 None
669 }
670 fn uri(&self) -> &Uri {
671 &self.uri
672 }
673 fn as_any(&self) -> &dyn Any {
674 self
675 }
676 }
677
678 struct MockDep {
679 name: String,
680 version_req: String,
681 version_range: Range,
682 name_range: Range,
683 }
684
685 impl Dependency for MockDep {
686 fn name(&self) -> &str {
687 &self.name
688 }
689 fn name_range(&self) -> Range {
690 self.name_range
691 }
692 fn version_requirement(&self) -> Option<&str> {
693 Some(&self.version_req)
694 }
695 fn version_range(&self) -> Option<Range> {
696 Some(self.version_range)
697 }
698 fn source(&self) -> crate::parser::DependencySource {
699 crate::parser::DependencySource::Registry
700 }
701 fn as_any(&self) -> &dyn Any {
702 self
703 }
704 }
705
706 let parse_result = MockParseResult {
707 deps: vec![MockDep {
708 name: "serde".to_string(),
709 version_req: "=2.0.12".to_string(),
710 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
711 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
712 }],
713 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
714 };
715
716 let mut cached_versions = HashMap::new();
717 cached_versions.insert("serde".to_string(), "2.1.1".to_string());
718
719 let mut resolved_versions = HashMap::new();
720 resolved_versions.insert("serde".to_string(), "2.0.12".to_string());
721
722 let hints = generate_inlay_hints(
723 &parse_result,
724 &cached_versions,
725 &resolved_versions,
726 crate::LoadingState::Loaded,
727 &config,
728 &formatter,
729 );
730
731 assert_eq!(hints.len(), 1);
732 match &hints[0].label {
733 InlayHintLabel::String(text) => {
734 assert_eq!(text, "❌ 2.1.1");
735 }
736 _ => panic!("Expected string label"),
737 }
738 }
739
740 #[test]
741 fn test_inlay_hint_caret_version_up_to_date() {
742 use std::any::Any;
743 use std::collections::HashMap;
744 use tower_lsp_server::ls_types::{Position, Range, Uri};
745
746 let formatter = MockFormatter;
747 let config = EcosystemConfig {
748 show_up_to_date_hints: true,
749 up_to_date_text: "✅".to_string(),
750 needs_update_text: "❌ {}".to_string(),
751 loading_text: "⏳".to_string(),
752 show_loading_hints: true,
753 };
754
755 struct MockParseResult {
756 deps: Vec<MockDep>,
757 uri: Uri,
758 }
759
760 impl ParseResult for MockParseResult {
761 fn dependencies(&self) -> Vec<&dyn Dependency> {
762 self.deps.iter().map(|d| d as &dyn Dependency).collect()
763 }
764 fn workspace_root(&self) -> Option<&std::path::Path> {
765 None
766 }
767 fn uri(&self) -> &Uri {
768 &self.uri
769 }
770 fn as_any(&self) -> &dyn Any {
771 self
772 }
773 }
774
775 struct MockDep {
776 name: String,
777 version_req: String,
778 version_range: Range,
779 name_range: Range,
780 }
781
782 impl Dependency for MockDep {
783 fn name(&self) -> &str {
784 &self.name
785 }
786 fn name_range(&self) -> Range {
787 self.name_range
788 }
789 fn version_requirement(&self) -> Option<&str> {
790 Some(&self.version_req)
791 }
792 fn version_range(&self) -> Option<Range> {
793 Some(self.version_range)
794 }
795 fn source(&self) -> crate::parser::DependencySource {
796 crate::parser::DependencySource::Registry
797 }
798 fn as_any(&self) -> &dyn Any {
799 self
800 }
801 }
802
803 let parse_result = MockParseResult {
804 deps: vec![MockDep {
805 name: "serde".to_string(),
806 version_req: "^2.0".to_string(),
807 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
808 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
809 }],
810 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
811 };
812
813 let mut cached_versions = HashMap::new();
814 cached_versions.insert("serde".to_string(), "2.1.1".to_string());
815
816 let mut resolved_versions = HashMap::new();
817 resolved_versions.insert("serde".to_string(), "2.1.1".to_string());
818
819 let hints = generate_inlay_hints(
820 &parse_result,
821 &cached_versions,
822 &resolved_versions,
823 crate::LoadingState::Loaded,
824 &config,
825 &formatter,
826 );
827
828 assert_eq!(hints.len(), 1);
829 match &hints[0].label {
830 InlayHintLabel::String(text) => {
831 assert!(
832 text.starts_with("✅"),
833 "Expected up-to-date hint, got: {}",
834 text
835 );
836 }
837 _ => panic!("Expected string label"),
838 }
839 }
840
841 #[test]
842 fn test_loading_hint_shows_when_no_cached_version() {
843 use std::any::Any;
844 use std::collections::HashMap;
845 use tower_lsp_server::ls_types::{Position, Range, Uri};
846
847 let formatter = MockFormatter;
848 let config = EcosystemConfig {
849 show_up_to_date_hints: true,
850 up_to_date_text: "✅".to_string(),
851 needs_update_text: "❌ {}".to_string(),
852 loading_text: "⏳".to_string(),
853 show_loading_hints: true,
854 };
855
856 struct MockParseResult {
857 deps: Vec<MockDep>,
858 uri: Uri,
859 }
860
861 impl ParseResult for MockParseResult {
862 fn dependencies(&self) -> Vec<&dyn Dependency> {
863 self.deps.iter().map(|d| d as &dyn Dependency).collect()
864 }
865 fn workspace_root(&self) -> Option<&std::path::Path> {
866 None
867 }
868 fn uri(&self) -> &Uri {
869 &self.uri
870 }
871 fn as_any(&self) -> &dyn Any {
872 self
873 }
874 }
875
876 struct MockDep {
877 name: String,
878 version_req: String,
879 version_range: Range,
880 name_range: Range,
881 }
882
883 impl Dependency for MockDep {
884 fn name(&self) -> &str {
885 &self.name
886 }
887 fn name_range(&self) -> Range {
888 self.name_range
889 }
890 fn version_requirement(&self) -> Option<&str> {
891 Some(&self.version_req)
892 }
893 fn version_range(&self) -> Option<Range> {
894 Some(self.version_range)
895 }
896 fn source(&self) -> crate::parser::DependencySource {
897 crate::parser::DependencySource::Registry
898 }
899 fn as_any(&self) -> &dyn Any {
900 self
901 }
902 }
903
904 let parse_result = MockParseResult {
905 deps: vec![MockDep {
906 name: "tokio".to_string(),
907 version_req: "1.0".to_string(),
908 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
909 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
910 }],
911 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
912 };
913
914 let cached_versions = HashMap::new();
915 let resolved_versions = HashMap::new();
916
917 let hints = generate_inlay_hints(
918 &parse_result,
919 &cached_versions,
920 &resolved_versions,
921 crate::LoadingState::Loading,
922 &config,
923 &formatter,
924 );
925
926 assert_eq!(hints.len(), 1);
927 match &hints[0].label {
928 InlayHintLabel::String(text) => {
929 assert_eq!(text, "⏳", "Expected loading hint");
930 }
931 _ => panic!("Expected string label"),
932 }
933
934 if let Some(InlayHintTooltip::String(tooltip)) = &hints[0].tooltip {
935 assert_eq!(tooltip, "Fetching latest version...");
936 } else {
937 panic!("Expected tooltip");
938 }
939 }
940
941 #[test]
942 fn test_loading_hint_disabled_when_config_false() {
943 use std::any::Any;
944 use std::collections::HashMap;
945 use tower_lsp_server::ls_types::{Position, Range, Uri};
946
947 let formatter = MockFormatter;
948 let config = EcosystemConfig {
949 show_up_to_date_hints: true,
950 up_to_date_text: "✅".to_string(),
951 needs_update_text: "❌ {}".to_string(),
952 loading_text: "⏳".to_string(),
953 show_loading_hints: false,
954 };
955
956 struct MockParseResult {
957 deps: Vec<MockDep>,
958 uri: Uri,
959 }
960
961 impl ParseResult for MockParseResult {
962 fn dependencies(&self) -> Vec<&dyn Dependency> {
963 self.deps.iter().map(|d| d as &dyn Dependency).collect()
964 }
965 fn workspace_root(&self) -> Option<&std::path::Path> {
966 None
967 }
968 fn uri(&self) -> &Uri {
969 &self.uri
970 }
971 fn as_any(&self) -> &dyn Any {
972 self
973 }
974 }
975
976 struct MockDep {
977 name: String,
978 version_req: String,
979 version_range: Range,
980 name_range: Range,
981 }
982
983 impl Dependency for MockDep {
984 fn name(&self) -> &str {
985 &self.name
986 }
987 fn name_range(&self) -> Range {
988 self.name_range
989 }
990 fn version_requirement(&self) -> Option<&str> {
991 Some(&self.version_req)
992 }
993 fn version_range(&self) -> Option<Range> {
994 Some(self.version_range)
995 }
996 fn source(&self) -> crate::parser::DependencySource {
997 crate::parser::DependencySource::Registry
998 }
999 fn as_any(&self) -> &dyn Any {
1000 self
1001 }
1002 }
1003
1004 let parse_result = MockParseResult {
1005 deps: vec![MockDep {
1006 name: "tokio".to_string(),
1007 version_req: "1.0".to_string(),
1008 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1009 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1010 }],
1011 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1012 };
1013
1014 let cached_versions = HashMap::new();
1015 let resolved_versions = HashMap::new();
1016
1017 let hints = generate_inlay_hints(
1018 &parse_result,
1019 &cached_versions,
1020 &resolved_versions,
1021 crate::LoadingState::Loading,
1022 &config,
1023 &formatter,
1024 );
1025
1026 assert_eq!(
1027 hints.len(),
1028 0,
1029 "Expected no hints when loading hints disabled"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_caret_version_0x_edge_cases() {
1035 let formatter = MockFormatter;
1036
1037 assert!(formatter.version_satisfies_requirement("0.2.0", "^0.2"));
1039 assert!(formatter.version_satisfies_requirement("0.2.5", "^0.2"));
1040 assert!(formatter.version_satisfies_requirement("0.2.99", "^0.2"));
1041
1042 assert!(!formatter.version_satisfies_requirement("0.3.0", "^0.2"));
1044 assert!(!formatter.version_satisfies_requirement("0.1.0", "^0.2"));
1045 assert!(!formatter.version_satisfies_requirement("1.0.0", "^0.2"));
1046
1047 assert!(formatter.version_satisfies_requirement("0.0.3", "^0.0.3"));
1049 assert!(formatter.version_satisfies_requirement("0.0.3", "^0.0"));
1050
1051 assert!(formatter.version_satisfies_requirement("0.0.0", "^0"));
1053 assert!(formatter.version_satisfies_requirement("0.5.0", "^0"));
1054 assert!(!formatter.version_satisfies_requirement("1.0.0", "^0"));
1055 }
1056
1057 #[test]
1058 fn test_caret_version_non_zero_major() {
1059 let formatter = MockFormatter;
1060
1061 assert!(formatter.version_satisfies_requirement("1.0.0", "^1.2"));
1063 assert!(formatter.version_satisfies_requirement("1.2.0", "^1.2"));
1064 assert!(formatter.version_satisfies_requirement("1.9.9", "^1.2"));
1065
1066 assert!(!formatter.version_satisfies_requirement("2.0.0", "^1.2"));
1068 assert!(!formatter.version_satisfies_requirement("0.9.0", "^1.2"));
1069 }
1070
1071 #[test]
1072 fn test_loading_hint_not_shown_when_cached_version_exists() {
1073 use std::any::Any;
1074 use std::collections::HashMap;
1075 use tower_lsp_server::ls_types::{Position, Range, Uri};
1076
1077 let formatter = MockFormatter;
1078 let config = EcosystemConfig {
1079 show_up_to_date_hints: true,
1080 up_to_date_text: "✅".to_string(),
1081 needs_update_text: "❌ {}".to_string(),
1082 loading_text: "⏳".to_string(),
1083 show_loading_hints: true,
1084 };
1085
1086 struct MockParseResult {
1087 deps: Vec<MockDep>,
1088 uri: Uri,
1089 }
1090
1091 impl ParseResult for MockParseResult {
1092 fn dependencies(&self) -> Vec<&dyn Dependency> {
1093 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1094 }
1095 fn workspace_root(&self) -> Option<&std::path::Path> {
1096 None
1097 }
1098 fn uri(&self) -> &Uri {
1099 &self.uri
1100 }
1101 fn as_any(&self) -> &dyn Any {
1102 self
1103 }
1104 }
1105
1106 struct MockDep {
1107 name: String,
1108 version_req: String,
1109 version_range: Range,
1110 name_range: Range,
1111 }
1112
1113 impl Dependency for MockDep {
1114 fn name(&self) -> &str {
1115 &self.name
1116 }
1117 fn name_range(&self) -> Range {
1118 self.name_range
1119 }
1120 fn version_requirement(&self) -> Option<&str> {
1121 Some(&self.version_req)
1122 }
1123 fn version_range(&self) -> Option<Range> {
1124 Some(self.version_range)
1125 }
1126 fn source(&self) -> crate::parser::DependencySource {
1127 crate::parser::DependencySource::Registry
1128 }
1129 fn as_any(&self) -> &dyn Any {
1130 self
1131 }
1132 }
1133
1134 let parse_result = MockParseResult {
1135 deps: vec![MockDep {
1136 name: "serde".to_string(),
1137 version_req: "1.0".to_string(),
1138 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1139 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1140 }],
1141 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1142 };
1143
1144 let mut cached_versions = HashMap::new();
1145 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
1146
1147 let mut resolved_versions = HashMap::new();
1149 resolved_versions.insert("serde".to_string(), "1.0.214".to_string());
1150
1151 let hints = generate_inlay_hints(
1152 &parse_result,
1153 &cached_versions,
1154 &resolved_versions,
1155 crate::LoadingState::Loading,
1156 &config,
1157 &formatter,
1158 );
1159
1160 assert_eq!(hints.len(), 1);
1161 match &hints[0].label {
1162 InlayHintLabel::String(text) => {
1163 assert_eq!(
1164 text, "✅ 1.0.214",
1165 "Expected up-to-date hint, not loading hint, got: {}",
1166 text
1167 );
1168 }
1169 _ => panic!("Expected string label"),
1170 }
1171 }
1172
1173 #[test]
1174 fn test_generate_diagnostics_from_cache_unknown_package() {
1175 use std::any::Any;
1176 use std::collections::HashMap;
1177 use tower_lsp_server::ls_types::{Position, Range, Uri};
1178
1179 let formatter = MockFormatter;
1180
1181 struct MockParseResult {
1182 deps: Vec<MockDep>,
1183 uri: Uri,
1184 }
1185
1186 impl ParseResult for MockParseResult {
1187 fn dependencies(&self) -> Vec<&dyn Dependency> {
1188 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1189 }
1190 fn workspace_root(&self) -> Option<&std::path::Path> {
1191 None
1192 }
1193 fn uri(&self) -> &Uri {
1194 &self.uri
1195 }
1196 fn as_any(&self) -> &dyn Any {
1197 self
1198 }
1199 }
1200
1201 struct MockDep {
1202 name: String,
1203 version_req: String,
1204 version_range: Range,
1205 name_range: Range,
1206 }
1207
1208 impl Dependency for MockDep {
1209 fn name(&self) -> &str {
1210 &self.name
1211 }
1212 fn name_range(&self) -> Range {
1213 self.name_range
1214 }
1215 fn version_requirement(&self) -> Option<&str> {
1216 Some(&self.version_req)
1217 }
1218 fn version_range(&self) -> Option<Range> {
1219 Some(self.version_range)
1220 }
1221 fn source(&self) -> crate::parser::DependencySource {
1222 crate::parser::DependencySource::Registry
1223 }
1224 fn as_any(&self) -> &dyn Any {
1225 self
1226 }
1227 }
1228
1229 let parse_result = MockParseResult {
1230 deps: vec![MockDep {
1231 name: "unknown-pkg".to_string(),
1232 version_req: "1.0.0".to_string(),
1233 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1234 name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
1235 }],
1236 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1237 };
1238
1239 let cached_versions = HashMap::new();
1240 let resolved_versions = HashMap::new();
1241
1242 let diagnostics = generate_diagnostics_from_cache(
1243 &parse_result,
1244 &cached_versions,
1245 &resolved_versions,
1246 &formatter,
1247 );
1248
1249 assert_eq!(diagnostics.len(), 1);
1250 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1251 assert!(diagnostics[0].message.contains("Unknown package"));
1252 assert!(diagnostics[0].message.contains("unknown-pkg"));
1253 }
1254
1255 #[test]
1256 fn test_generate_diagnostics_from_cache_outdated_version() {
1257 use std::any::Any;
1258 use std::collections::HashMap;
1259 use tower_lsp_server::ls_types::{Position, Range, Uri};
1260
1261 let formatter = MockFormatter;
1262
1263 struct MockParseResult {
1264 deps: Vec<MockDep>,
1265 uri: Uri,
1266 }
1267
1268 impl ParseResult for MockParseResult {
1269 fn dependencies(&self) -> Vec<&dyn Dependency> {
1270 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1271 }
1272 fn workspace_root(&self) -> Option<&std::path::Path> {
1273 None
1274 }
1275 fn uri(&self) -> &Uri {
1276 &self.uri
1277 }
1278 fn as_any(&self) -> &dyn Any {
1279 self
1280 }
1281 }
1282
1283 struct MockDep {
1284 name: String,
1285 version_req: String,
1286 version_range: Range,
1287 name_range: Range,
1288 }
1289
1290 impl Dependency for MockDep {
1291 fn name(&self) -> &str {
1292 &self.name
1293 }
1294 fn name_range(&self) -> Range {
1295 self.name_range
1296 }
1297 fn version_requirement(&self) -> Option<&str> {
1298 Some(&self.version_req)
1299 }
1300 fn version_range(&self) -> Option<Range> {
1301 Some(self.version_range)
1302 }
1303 fn source(&self) -> crate::parser::DependencySource {
1304 crate::parser::DependencySource::Registry
1305 }
1306 fn as_any(&self) -> &dyn Any {
1307 self
1308 }
1309 }
1310
1311 let parse_result = MockParseResult {
1312 deps: vec![MockDep {
1313 name: "serde".to_string(),
1314 version_req: "1.0".to_string(),
1315 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1316 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1317 }],
1318 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1319 };
1320
1321 let mut cached_versions = HashMap::new();
1322 cached_versions.insert("serde".to_string(), "2.0.0".to_string());
1323
1324 let resolved_versions = HashMap::new();
1325
1326 let diagnostics = generate_diagnostics_from_cache(
1327 &parse_result,
1328 &cached_versions,
1329 &resolved_versions,
1330 &formatter,
1331 );
1332
1333 assert_eq!(diagnostics.len(), 1);
1334 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1335 assert!(diagnostics[0].message.contains("Newer version available"));
1336 assert!(diagnostics[0].message.contains("2.0.0"));
1337 }
1338
1339 #[test]
1340 fn test_generate_diagnostics_from_cache_up_to_date() {
1341 use std::any::Any;
1342 use std::collections::HashMap;
1343 use tower_lsp_server::ls_types::{Position, Range, Uri};
1344
1345 let formatter = MockFormatter;
1346
1347 struct MockParseResult {
1348 deps: Vec<MockDep>,
1349 uri: Uri,
1350 }
1351
1352 impl ParseResult for MockParseResult {
1353 fn dependencies(&self) -> Vec<&dyn Dependency> {
1354 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1355 }
1356 fn workspace_root(&self) -> Option<&std::path::Path> {
1357 None
1358 }
1359 fn uri(&self) -> &Uri {
1360 &self.uri
1361 }
1362 fn as_any(&self) -> &dyn Any {
1363 self
1364 }
1365 }
1366
1367 struct MockDep {
1368 name: String,
1369 version_req: String,
1370 version_range: Range,
1371 name_range: Range,
1372 }
1373
1374 impl Dependency for MockDep {
1375 fn name(&self) -> &str {
1376 &self.name
1377 }
1378 fn name_range(&self) -> Range {
1379 self.name_range
1380 }
1381 fn version_requirement(&self) -> Option<&str> {
1382 Some(&self.version_req)
1383 }
1384 fn version_range(&self) -> Option<Range> {
1385 Some(self.version_range)
1386 }
1387 fn source(&self) -> crate::parser::DependencySource {
1388 crate::parser::DependencySource::Registry
1389 }
1390 fn as_any(&self) -> &dyn Any {
1391 self
1392 }
1393 }
1394
1395 let parse_result = MockParseResult {
1396 deps: vec![MockDep {
1397 name: "serde".to_string(),
1398 version_req: "^1.0".to_string(),
1399 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1400 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1401 }],
1402 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1403 };
1404
1405 let mut cached_versions = HashMap::new();
1406 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
1407
1408 let resolved_versions = HashMap::new();
1409
1410 let diagnostics = generate_diagnostics_from_cache(
1411 &parse_result,
1412 &cached_versions,
1413 &resolved_versions,
1414 &formatter,
1415 );
1416
1417 assert!(
1418 diagnostics.is_empty(),
1419 "Expected no diagnostics for up-to-date dependency"
1420 );
1421 }
1422
1423 #[test]
1424 fn test_generate_diagnostics_from_cache_multiple_deps() {
1425 use std::any::Any;
1426 use std::collections::HashMap;
1427 use tower_lsp_server::ls_types::{Position, Range, Uri};
1428
1429 let formatter = MockFormatter;
1430
1431 struct MockParseResult {
1432 deps: Vec<MockDep>,
1433 uri: Uri,
1434 }
1435
1436 impl ParseResult for MockParseResult {
1437 fn dependencies(&self) -> Vec<&dyn Dependency> {
1438 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1439 }
1440 fn workspace_root(&self) -> Option<&std::path::Path> {
1441 None
1442 }
1443 fn uri(&self) -> &Uri {
1444 &self.uri
1445 }
1446 fn as_any(&self) -> &dyn Any {
1447 self
1448 }
1449 }
1450
1451 struct MockDep {
1452 name: String,
1453 version_req: String,
1454 version_range: Range,
1455 name_range: Range,
1456 }
1457
1458 impl Dependency for MockDep {
1459 fn name(&self) -> &str {
1460 &self.name
1461 }
1462 fn name_range(&self) -> Range {
1463 self.name_range
1464 }
1465 fn version_requirement(&self) -> Option<&str> {
1466 Some(&self.version_req)
1467 }
1468 fn version_range(&self) -> Option<Range> {
1469 Some(self.version_range)
1470 }
1471 fn source(&self) -> crate::parser::DependencySource {
1472 crate::parser::DependencySource::Registry
1473 }
1474 fn as_any(&self) -> &dyn Any {
1475 self
1476 }
1477 }
1478
1479 let parse_result = MockParseResult {
1480 deps: vec![
1481 MockDep {
1482 name: "serde".to_string(),
1483 version_req: "^1.0".to_string(),
1484 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1485 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1486 },
1487 MockDep {
1488 name: "tokio".to_string(),
1489 version_req: "1.0".to_string(),
1490 version_range: Range::new(Position::new(1, 10), Position::new(1, 20)),
1491 name_range: Range::new(Position::new(1, 0), Position::new(1, 5)),
1492 },
1493 MockDep {
1494 name: "unknown".to_string(),
1495 version_req: "1.0".to_string(),
1496 version_range: Range::new(Position::new(2, 10), Position::new(2, 20)),
1497 name_range: Range::new(Position::new(2, 0), Position::new(2, 7)),
1498 },
1499 ],
1500 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1501 };
1502
1503 let mut cached_versions = HashMap::new();
1504 cached_versions.insert("serde".to_string(), "1.0.214".to_string());
1505 cached_versions.insert("tokio".to_string(), "2.0.0".to_string());
1506
1507 let resolved_versions = HashMap::new();
1508
1509 let diagnostics = generate_diagnostics_from_cache(
1510 &parse_result,
1511 &cached_versions,
1512 &resolved_versions,
1513 &formatter,
1514 );
1515
1516 assert_eq!(diagnostics.len(), 2);
1517
1518 let has_outdated = diagnostics
1519 .iter()
1520 .any(|d| d.message.contains("Newer version"));
1521 let has_unknown = diagnostics
1522 .iter()
1523 .any(|d| d.message.contains("Unknown package"));
1524
1525 assert!(has_outdated, "Expected outdated version diagnostic");
1526 assert!(has_unknown, "Expected unknown package diagnostic");
1527 }
1528
1529 #[test]
1530 fn test_inlay_hint_not_in_lockfile_but_satisfies_requirement() {
1531 use std::any::Any;
1532 use std::collections::HashMap;
1533 use tower_lsp_server::ls_types::{Position, Range, Uri};
1534
1535 let formatter = MockFormatter;
1536 let config = EcosystemConfig {
1537 show_up_to_date_hints: true,
1538 up_to_date_text: "✅".to_string(),
1539 needs_update_text: "❌ {}".to_string(),
1540 loading_text: "⏳".to_string(),
1541 show_loading_hints: true,
1542 };
1543
1544 struct MockParseResult {
1545 deps: Vec<MockDep>,
1546 uri: Uri,
1547 }
1548
1549 impl ParseResult for MockParseResult {
1550 fn dependencies(&self) -> Vec<&dyn Dependency> {
1551 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1552 }
1553 fn workspace_root(&self) -> Option<&std::path::Path> {
1554 None
1555 }
1556 fn uri(&self) -> &Uri {
1557 &self.uri
1558 }
1559 fn as_any(&self) -> &dyn Any {
1560 self
1561 }
1562 }
1563
1564 struct MockDep {
1565 name: String,
1566 version_req: String,
1567 version_range: Range,
1568 name_range: Range,
1569 }
1570
1571 impl Dependency for MockDep {
1572 fn name(&self) -> &str {
1573 &self.name
1574 }
1575 fn name_range(&self) -> Range {
1576 self.name_range
1577 }
1578 fn version_requirement(&self) -> Option<&str> {
1579 Some(&self.version_req)
1580 }
1581 fn version_range(&self) -> Option<Range> {
1582 Some(self.version_range)
1583 }
1584 fn source(&self) -> crate::parser::DependencySource {
1585 crate::parser::DependencySource::Registry
1586 }
1587 fn as_any(&self) -> &dyn Any {
1588 self
1589 }
1590 }
1591
1592 let parse_result = MockParseResult {
1593 deps: vec![MockDep {
1594 name: "criterion".to_string(),
1595 version_req: "0.5".to_string(),
1596 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1597 name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
1598 }],
1599 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1600 };
1601
1602 let mut cached_versions = HashMap::new();
1603 cached_versions.insert("criterion".to_string(), "0.5.1".to_string());
1604
1605 let resolved_versions = HashMap::new();
1607
1608 let hints = generate_inlay_hints(
1609 &parse_result,
1610 &cached_versions,
1611 &resolved_versions,
1612 crate::LoadingState::Loaded,
1613 &config,
1614 &formatter,
1615 );
1616
1617 assert_eq!(hints.len(), 1);
1618 match &hints[0].label {
1619 InlayHintLabel::String(text) => {
1620 assert!(
1621 text.starts_with("✅"),
1622 "Expected up-to-date hint for satisfied requirement, got: {}",
1623 text
1624 );
1625 }
1626 _ => panic!("Expected string label"),
1627 }
1628 }
1629
1630 #[test]
1631 fn test_inlay_hint_not_in_lockfile_and_outdated() {
1632 use std::any::Any;
1633 use std::collections::HashMap;
1634 use tower_lsp_server::ls_types::{Position, Range, Uri};
1635
1636 let formatter = MockFormatter;
1637 let config = EcosystemConfig {
1638 show_up_to_date_hints: true,
1639 up_to_date_text: "✅".to_string(),
1640 needs_update_text: "❌ {}".to_string(),
1641 loading_text: "⏳".to_string(),
1642 show_loading_hints: true,
1643 };
1644
1645 struct MockParseResult {
1646 deps: Vec<MockDep>,
1647 uri: Uri,
1648 }
1649
1650 impl ParseResult for MockParseResult {
1651 fn dependencies(&self) -> Vec<&dyn Dependency> {
1652 self.deps.iter().map(|d| d as &dyn Dependency).collect()
1653 }
1654 fn workspace_root(&self) -> Option<&std::path::Path> {
1655 None
1656 }
1657 fn uri(&self) -> &Uri {
1658 &self.uri
1659 }
1660 fn as_any(&self) -> &dyn Any {
1661 self
1662 }
1663 }
1664
1665 struct MockDep {
1666 name: String,
1667 version_req: String,
1668 version_range: Range,
1669 name_range: Range,
1670 }
1671
1672 impl Dependency for MockDep {
1673 fn name(&self) -> &str {
1674 &self.name
1675 }
1676 fn name_range(&self) -> Range {
1677 self.name_range
1678 }
1679 fn version_requirement(&self) -> Option<&str> {
1680 Some(&self.version_req)
1681 }
1682 fn version_range(&self) -> Option<Range> {
1683 Some(self.version_range)
1684 }
1685 fn source(&self) -> crate::parser::DependencySource {
1686 crate::parser::DependencySource::Registry
1687 }
1688 fn as_any(&self) -> &dyn Any {
1689 self
1690 }
1691 }
1692
1693 let parse_result = MockParseResult {
1694 deps: vec![MockDep {
1695 name: "criterion".to_string(),
1696 version_req: "0.4".to_string(),
1697 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
1698 name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
1699 }],
1700 uri: Uri::from_file_path("/test/Cargo.toml").unwrap(),
1701 };
1702
1703 let mut cached_versions = HashMap::new();
1704 cached_versions.insert("criterion".to_string(), "0.5.1".to_string());
1705
1706 let resolved_versions = HashMap::new();
1708
1709 let hints = generate_inlay_hints(
1710 &parse_result,
1711 &cached_versions,
1712 &resolved_versions,
1713 crate::LoadingState::Loaded,
1714 &config,
1715 &formatter,
1716 );
1717
1718 assert_eq!(hints.len(), 1);
1719 match &hints[0].label {
1720 InlayHintLabel::String(text) => {
1721 assert!(
1722 text.starts_with("❌"),
1723 "Expected needs-update hint for unsatisfied requirement, got: {}",
1724 text
1725 );
1726 assert!(text.contains("0.5.1"), "Expected latest version in hint");
1727 }
1728 _ => panic!("Expected string label"),
1729 }
1730 }
1731}