1use tower_lsp_server::ls_types::{InlayHint, InlayHintKind, InlayHintLabel, InlayHintTooltip};
2
3use crate::{ConcreteVersion, EcosystemConfig, ParseResult};
4
5use super::{EcosystemFormatter, RequirementStatus, VersionData};
6
7pub fn generate_inlay_hints(
8 parse_result: &dyn ParseResult,
9 versions: VersionData<'_>,
10 loading_state: crate::LoadingState,
11 config: &EcosystemConfig,
12 formatter: &dyn EcosystemFormatter,
13) -> Vec<InlayHint> {
14 let deps = parse_result.dependencies();
15 let mut hints = Vec::with_capacity(deps.len());
16
17 for dep in deps {
18 let Some(version_range) = dep.version_range() else {
19 continue;
20 };
21
22 let normalized_name = formatter.normalize_package_name(dep.name());
23 let latest_version = versions
24 .cached
25 .get(normalized_name.as_str())
26 .or_else(|| versions.cached.get(dep.name()))
27 .map(|v| &v.latest);
28 let resolved_version: Option<ConcreteVersion> =
29 if formatter.manifest_requirement_is_resolved_version(dep) {
30 dep.version_requirement()
31 .map(|r| ConcreteVersion::new(r.as_str()))
32 } else {
33 versions
34 .resolved
35 .get(normalized_name.as_str())
36 .or_else(|| versions.resolved.get(dep.name()))
37 .cloned()
38 };
39
40 if loading_state == crate::LoadingState::Loading
42 && config.show_loading_hints
43 && latest_version.is_none()
44 {
45 hints.push(InlayHint {
46 position: version_range.end,
47 label: InlayHintLabel::String(config.loading_text.clone()),
48 kind: Some(InlayHintKind::TYPE),
49 tooltip: Some(InlayHintTooltip::String(
50 "Fetching latest version...".to_string(),
51 )),
52 padding_left: Some(true),
53 padding_right: None,
54 text_edits: None,
55 data: None,
56 });
57 continue;
58 }
59
60 let Some(latest) = latest_version else {
61 if config.offline {
65 let label = resolved_version
66 .as_ref()
67 .map_or_else(|| "📴".to_string(), |resolved| format!("📴 {resolved}"));
68 hints.push(InlayHint {
69 position: version_range.end,
70 label: InlayHintLabel::String(label),
71 kind: Some(InlayHintKind::TYPE),
72 tooltip: Some(InlayHintTooltip::String(
73 "Offline: registry not checked".to_string(),
74 )),
75 padding_left: Some(true),
76 padding_right: None,
77 text_edits: None,
78 data: None,
79 });
80 continue;
81 }
82
83 if let Some(resolved) = &resolved_version
84 && config.show_up_to_date_hints
85 {
86 hints.push(InlayHint {
87 position: version_range.end,
88 label: InlayHintLabel::String(format!(
89 "{} {}",
90 config.up_to_date_text, resolved
91 )),
92 kind: Some(InlayHintKind::TYPE),
93 padding_left: Some(true),
94 padding_right: None,
95 text_edits: None,
96 tooltip: None,
97 data: None,
98 });
99 }
100 continue;
101 };
102
103 let status = if let Some(resolved) = &resolved_version {
107 if resolved == latest {
108 RequirementStatus::UpToDate
109 } else {
110 RequirementStatus::Outdated
111 }
112 } else {
113 match dep.version_requirement() {
114 Some(version_req) => formatter.requirement_status(version_req, latest),
115 None => RequirementStatus::Unresolved,
118 }
119 };
120
121 let label_text = match status {
122 RequirementStatus::UpToDate => {
123 if config.show_up_to_date_hints {
124 if let Some(resolved) = &resolved_version {
125 format!("{} {}", config.up_to_date_text, resolved)
126 } else {
127 config.up_to_date_text.clone()
128 }
129 } else {
130 continue;
131 }
132 }
133 RequirementStatus::Outdated => config.needs_update_text.replace("{}", latest.as_str()),
134 RequirementStatus::Unresolved => continue,
137 };
138
139 let label_text = if config.offline {
143 format!("{label_text} 📴")
144 } else {
145 label_text
146 };
147
148 hints.push(InlayHint {
149 position: version_range.end,
150 label: InlayHintLabel::String(label_text),
151 kind: Some(InlayHintKind::TYPE),
152 padding_left: Some(true),
153 padding_right: None,
154 text_edits: None,
155 tooltip: None,
156 data: None,
157 });
158 }
159
160 hints
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::lsp_helpers::test_support::*;
167 use crate::lsp_helpers::*;
168
169 #[test]
170 fn test_inlay_hint_exact_version_shows_update_needed() {
171 use std::collections::HashMap;
172 use tower_lsp_server::ls_types::{Position, Range};
173
174 let formatter = MockFormatter;
175 let config = EcosystemConfig {
176 show_up_to_date_hints: true,
177 up_to_date_text: "✅".to_string(),
178 needs_update_text: "❌ {}".to_string(),
179 loading_text: "⏳".to_string(),
180 show_loading_hints: true,
181 offline: false,
182 };
183
184 let parse_result = MockParseResult {
185 deps: vec![MockDep {
186 name: "serde".into(),
187 version_req: "=2.0.12".into(),
188 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
189 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
190 }],
191 uri: crate::test_util::test_uri("/test/Cargo.toml"),
192 };
193
194 let mut cached_versions = HashMap::new();
195 cached_versions.insert("serde".into(), PackageVersions::latest_only("2.1.1"));
196
197 let mut resolved_versions = HashMap::new();
198 resolved_versions.insert("serde".into(), "2.0.12".into());
199
200 let hints = generate_inlay_hints(
201 &parse_result,
202 VersionData::new(&cached_versions, &resolved_versions),
203 crate::LoadingState::Loaded,
204 &config,
205 &formatter,
206 );
207
208 assert_eq!(hints.len(), 1);
209 match &hints[0].label {
210 InlayHintLabel::String(text) => {
211 assert_eq!(text, "❌ 2.1.1");
212 }
213 _ => panic!("Expected string label"),
214 }
215 }
216
217 #[test]
218 fn test_inlay_hint_caret_version_up_to_date() {
219 use std::collections::HashMap;
220 use tower_lsp_server::ls_types::{Position, Range};
221
222 let formatter = MockFormatter;
223 let config = EcosystemConfig {
224 show_up_to_date_hints: true,
225 up_to_date_text: "✅".to_string(),
226 needs_update_text: "❌ {}".to_string(),
227 loading_text: "⏳".to_string(),
228 show_loading_hints: true,
229 offline: false,
230 };
231
232 let parse_result = MockParseResult {
233 deps: vec![MockDep {
234 name: "serde".into(),
235 version_req: "^2.0".into(),
236 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
237 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
238 }],
239 uri: crate::test_util::test_uri("/test/Cargo.toml"),
240 };
241
242 let mut cached_versions = HashMap::new();
243 cached_versions.insert("serde".into(), PackageVersions::latest_only("2.1.1"));
244
245 let mut resolved_versions = HashMap::new();
246 resolved_versions.insert("serde".into(), "2.1.1".into());
247
248 let hints = generate_inlay_hints(
249 &parse_result,
250 VersionData::new(&cached_versions, &resolved_versions),
251 crate::LoadingState::Loaded,
252 &config,
253 &formatter,
254 );
255
256 assert_eq!(hints.len(), 1);
257 match &hints[0].label {
258 InlayHintLabel::String(text) => {
259 assert!(
260 text.starts_with("✅"),
261 "Expected up-to-date hint, got: {}",
262 text
263 );
264 }
265 _ => panic!("Expected string label"),
266 }
267 }
268
269 #[test]
270 fn test_inlay_hint_go_prefers_manifest_requirement_over_stale_resolved_version() {
271 use std::collections::HashMap;
272 use tower_lsp_server::ls_types::{Position, Range};
273
274 let formatter = MockGoFormatter;
275 let config = EcosystemConfig {
276 show_up_to_date_hints: true,
277 up_to_date_text: "✅".to_string(),
278 needs_update_text: "❌ {}".to_string(),
279 loading_text: "⏳".to_string(),
280 show_loading_hints: true,
281 offline: false,
282 };
283
284 let parse_result = MockParseResult {
285 deps: vec![MockDep {
286 name: "example.com/mod".into(),
287 version_req: "v0.8.1".into(),
288 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
289 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
290 }],
291 uri: crate::test_util::test_uri("/test/go.mod"),
292 };
293
294 let mut cached_versions = HashMap::new();
295 cached_versions.insert(
296 "example.com/mod".into(),
297 PackageVersions::latest_only("v0.9.1"),
298 );
299
300 let mut resolved_versions = HashMap::new();
304 resolved_versions.insert("example.com/mod".into(), "v0.9.1".into());
305
306 let hints = generate_inlay_hints(
307 &parse_result,
308 VersionData::new(&cached_versions, &resolved_versions),
309 crate::LoadingState::Loaded,
310 &config,
311 &formatter,
312 );
313
314 assert_eq!(hints.len(), 1);
315 match &hints[0].label {
316 InlayHintLabel::String(text) => {
317 assert!(
321 text.starts_with("❌"),
322 "expected outdated hint driven by go.mod pin, got: {text}"
323 );
324 }
325 _ => panic!("Expected string label"),
326 }
327 }
328
329 #[test]
330 fn test_inlay_hint_non_go_formatter_uses_resolved_lockfile_version() {
331 use std::collections::HashMap;
332 use tower_lsp_server::ls_types::{Position, Range};
333
334 let formatter = MockFormatter;
335 let config = EcosystemConfig {
336 show_up_to_date_hints: true,
337 up_to_date_text: "✅".to_string(),
338 needs_update_text: "❌ {}".to_string(),
339 loading_text: "⏳".to_string(),
340 show_loading_hints: true,
341 offline: false,
342 };
343
344 let parse_result = MockParseResult {
345 deps: vec![MockDep {
346 name: "serde".into(),
347 version_req: "1.0.0".into(),
348 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
349 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
350 }],
351 uri: crate::test_util::test_uri("/test/Cargo.toml"),
352 };
353
354 let mut cached_versions = HashMap::new();
355 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.2.0"));
356
357 let mut resolved_versions = HashMap::new();
358 resolved_versions.insert("serde".into(), "1.2.0".into());
359
360 let hints = generate_inlay_hints(
361 &parse_result,
362 VersionData::new(&cached_versions, &resolved_versions),
363 crate::LoadingState::Loaded,
364 &config,
365 &formatter,
366 );
367
368 assert_eq!(hints.len(), 1);
369 match &hints[0].label {
370 InlayHintLabel::String(text) => {
371 assert!(
376 text.starts_with("✅"),
377 "expected up-to-date hint from resolved lockfile version, got: {text}"
378 );
379 }
380 _ => panic!("Expected string label"),
381 }
382 }
383
384 #[test]
385 fn test_loading_hint_shows_when_no_cached_version() {
386 use std::collections::HashMap;
387 use tower_lsp_server::ls_types::{Position, Range};
388
389 let formatter = MockFormatter;
390 let config = EcosystemConfig {
391 show_up_to_date_hints: true,
392 up_to_date_text: "✅".to_string(),
393 needs_update_text: "❌ {}".to_string(),
394 loading_text: "⏳".to_string(),
395 show_loading_hints: true,
396 offline: false,
397 };
398
399 let parse_result = MockParseResult {
400 deps: vec![MockDep {
401 name: "tokio".into(),
402 version_req: "1.0".into(),
403 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
404 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
405 }],
406 uri: crate::test_util::test_uri("/test/Cargo.toml"),
407 };
408
409 let cached_versions = HashMap::new();
410 let resolved_versions = HashMap::new();
411
412 let hints = generate_inlay_hints(
413 &parse_result,
414 VersionData::new(&cached_versions, &resolved_versions),
415 crate::LoadingState::Loading,
416 &config,
417 &formatter,
418 );
419
420 assert_eq!(hints.len(), 1);
421 match &hints[0].label {
422 InlayHintLabel::String(text) => {
423 assert_eq!(text, "⏳", "Expected loading hint");
424 }
425 _ => panic!("Expected string label"),
426 }
427
428 if let Some(InlayHintTooltip::String(tooltip)) = &hints[0].tooltip {
429 assert_eq!(tooltip, "Fetching latest version...");
430 } else {
431 panic!("Expected tooltip");
432 }
433 }
434
435 #[test]
436 fn test_loading_hint_disabled_when_config_false() {
437 use std::collections::HashMap;
438 use tower_lsp_server::ls_types::{Position, Range};
439
440 let formatter = MockFormatter;
441 let config = EcosystemConfig {
442 show_up_to_date_hints: true,
443 up_to_date_text: "✅".to_string(),
444 needs_update_text: "❌ {}".to_string(),
445 loading_text: "⏳".to_string(),
446 show_loading_hints: false,
447 offline: false,
448 };
449
450 let parse_result = MockParseResult {
451 deps: vec![MockDep {
452 name: "tokio".into(),
453 version_req: "1.0".into(),
454 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
455 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
456 }],
457 uri: crate::test_util::test_uri("/test/Cargo.toml"),
458 };
459
460 let cached_versions = HashMap::new();
461 let resolved_versions = HashMap::new();
462
463 let hints = generate_inlay_hints(
464 &parse_result,
465 VersionData::new(&cached_versions, &resolved_versions),
466 crate::LoadingState::Loading,
467 &config,
468 &formatter,
469 );
470
471 assert_eq!(
472 hints.len(),
473 0,
474 "Expected no hints when loading hints disabled"
475 );
476 }
477
478 #[test]
479 fn test_caret_version_0x_edge_cases() {
480 let formatter = MockFormatter;
481
482 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.2.0"), "^0.2"));
484 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.2.5"), "^0.2"));
485 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.2.99"), "^0.2"));
486
487 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("0.3.0"), "^0.2"));
489 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("0.1.0"), "^0.2"));
490 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("1.0.0"), "^0.2"));
491
492 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.0.3"), "^0.0.3"));
494 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.0.3"), "^0.0"));
495
496 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.0.0"), "^0"));
498 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("0.5.0"), "^0"));
499 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("1.0.0"), "^0"));
500 }
501
502 #[test]
503 fn test_caret_version_non_zero_major() {
504 let formatter = MockFormatter;
505
506 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.0.0"), "^1.2"));
508 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.2.0"), "^1.2"));
509 assert!(formatter.version_satisfies_requirement(&ConcreteVersion::new("1.9.9"), "^1.2"));
510
511 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("2.0.0"), "^1.2"));
513 assert!(!formatter.version_satisfies_requirement(&ConcreteVersion::new("0.9.0"), "^1.2"));
514 }
515
516 #[test]
517 fn test_loading_hint_not_shown_when_cached_version_exists() {
518 use std::collections::HashMap;
519 use tower_lsp_server::ls_types::{Position, Range};
520
521 let formatter = MockFormatter;
522 let config = EcosystemConfig {
523 show_up_to_date_hints: true,
524 up_to_date_text: "✅".to_string(),
525 needs_update_text: "❌ {}".to_string(),
526 loading_text: "⏳".to_string(),
527 show_loading_hints: true,
528 offline: false,
529 };
530
531 let parse_result = MockParseResult {
532 deps: vec![MockDep {
533 name: "serde".into(),
534 version_req: "1.0".into(),
535 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
536 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
537 }],
538 uri: crate::test_util::test_uri("/test/Cargo.toml"),
539 };
540
541 let mut cached_versions = HashMap::new();
542 cached_versions.insert("serde".into(), PackageVersions::latest_only("1.0.214"));
543
544 let mut resolved_versions = HashMap::new();
546 resolved_versions.insert("serde".into(), "1.0.214".into());
547
548 let hints = generate_inlay_hints(
549 &parse_result,
550 VersionData::new(&cached_versions, &resolved_versions),
551 crate::LoadingState::Loading,
552 &config,
553 &formatter,
554 );
555
556 assert_eq!(hints.len(), 1);
557 match &hints[0].label {
558 InlayHintLabel::String(text) => {
559 assert_eq!(
560 text, "✅ 1.0.214",
561 "Expected up-to-date hint, not loading hint, got: {}",
562 text
563 );
564 }
565 _ => panic!("Expected string label"),
566 }
567 }
568
569 #[test]
570 fn test_inlay_hint_not_in_lockfile_but_satisfies_requirement() {
571 use std::collections::HashMap;
572 use tower_lsp_server::ls_types::{Position, Range};
573
574 let formatter = MockFormatter;
575 let config = EcosystemConfig {
576 show_up_to_date_hints: true,
577 up_to_date_text: "✅".to_string(),
578 needs_update_text: "❌ {}".to_string(),
579 loading_text: "⏳".to_string(),
580 show_loading_hints: true,
581 offline: false,
582 };
583
584 let parse_result = MockParseResult {
585 deps: vec![MockDep {
586 name: "criterion".into(),
587 version_req: "0.5".into(),
588 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
589 name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
590 }],
591 uri: crate::test_util::test_uri("/test/Cargo.toml"),
592 };
593
594 let mut cached_versions = HashMap::new();
595 cached_versions.insert("criterion".into(), PackageVersions::latest_only("0.5.1"));
596
597 let resolved_versions = HashMap::new();
599
600 let hints = generate_inlay_hints(
601 &parse_result,
602 VersionData::new(&cached_versions, &resolved_versions),
603 crate::LoadingState::Loaded,
604 &config,
605 &formatter,
606 );
607
608 assert_eq!(hints.len(), 1);
609 match &hints[0].label {
610 InlayHintLabel::String(text) => {
611 assert!(
612 text.starts_with("✅"),
613 "Expected up-to-date hint for satisfied requirement, got: {}",
614 text
615 );
616 }
617 _ => panic!("Expected string label"),
618 }
619 }
620
621 #[test]
622 fn test_inlay_hint_not_in_lockfile_and_outdated() {
623 use std::collections::HashMap;
624 use tower_lsp_server::ls_types::{Position, Range};
625
626 let formatter = MockFormatter;
627 let config = EcosystemConfig {
628 show_up_to_date_hints: true,
629 up_to_date_text: "✅".to_string(),
630 needs_update_text: "❌ {}".to_string(),
631 loading_text: "⏳".to_string(),
632 show_loading_hints: true,
633 offline: false,
634 };
635
636 let parse_result = MockParseResult {
637 deps: vec![MockDep {
638 name: "criterion".into(),
639 version_req: "0.4".into(),
640 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
641 name_range: Range::new(Position::new(0, 0), Position::new(0, 9)),
642 }],
643 uri: crate::test_util::test_uri("/test/Cargo.toml"),
644 };
645
646 let mut cached_versions = HashMap::new();
647 cached_versions.insert("criterion".into(), PackageVersions::latest_only("0.5.1"));
648
649 let resolved_versions = HashMap::new();
651
652 let hints = generate_inlay_hints(
653 &parse_result,
654 VersionData::new(&cached_versions, &resolved_versions),
655 crate::LoadingState::Loaded,
656 &config,
657 &formatter,
658 );
659
660 assert_eq!(hints.len(), 1);
661 match &hints[0].label {
662 InlayHintLabel::String(text) => {
663 assert!(
664 text.starts_with("❌"),
665 "Expected needs-update hint for unsatisfied requirement, got: {}",
666 text
667 );
668 assert!(text.contains("0.5.1"), "Expected latest version in hint");
669 }
670 _ => panic!("Expected string label"),
671 }
672 }
673
674 #[test]
675 fn test_inlay_hint_unresolved_requirement_emits_no_hint() {
676 use std::collections::HashMap;
677 use tower_lsp_server::ls_types::{Position, Range};
678
679 let config = EcosystemConfig {
680 show_up_to_date_hints: true,
681 up_to_date_text: "✅".to_string(),
682 needs_update_text: "❌ {}".to_string(),
683 loading_text: "⏳".to_string(),
684 show_loading_hints: true,
685 offline: false,
686 };
687
688 let parse_result = MockParseResult {
689 deps: vec![MockDep {
690 name: "spring-boot-starter".into(),
691 version_req: "$missing".into(),
692 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
693 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
694 }],
695 uri: crate::test_util::test_uri("/test/libs.versions.toml"),
696 };
697
698 let mut cached_versions = HashMap::new();
699 cached_versions.insert(
700 "spring-boot-starter".into(),
701 PackageVersions::latest_only("3.2.0"),
702 );
703
704 let resolved_versions = HashMap::new();
708
709 let hints = generate_inlay_hints(
710 &parse_result,
711 VersionData::new(&cached_versions, &resolved_versions),
712 crate::LoadingState::Loaded,
713 &config,
714 &MockUnresolvedFormatter,
715 );
716
717 assert!(
718 hints.is_empty(),
719 "Expected no inlay hint at all for an unresolved requirement (not even 'up to date'), got: {hints:?}"
720 );
721 }
722
723 #[test]
726 fn test_inlay_hint_offline_cold_no_resolved_shows_bare_marker() {
727 use std::collections::HashMap;
728 use tower_lsp_server::ls_types::{Position, Range};
729
730 let formatter = MockFormatter;
731 let config = EcosystemConfig {
732 show_up_to_date_hints: true,
733 up_to_date_text: "✅".to_string(),
734 needs_update_text: "❌ {}".to_string(),
735 loading_text: "⏳".to_string(),
736 show_loading_hints: true,
737 offline: true,
738 };
739
740 let parse_result = MockParseResult {
741 deps: vec![MockDep {
742 name: "serde".into(),
743 version_req: "^2.0".into(),
744 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
745 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
746 }],
747 uri: crate::test_util::test_uri("/test/Cargo.toml"),
748 };
749
750 let cached_versions = HashMap::new();
751 let resolved_versions = HashMap::new();
752
753 let hints = generate_inlay_hints(
754 &parse_result,
755 VersionData::new(&cached_versions, &resolved_versions),
756 crate::LoadingState::Loaded,
757 &config,
758 &formatter,
759 );
760
761 assert_eq!(hints.len(), 1);
762 match &hints[0].label {
763 InlayHintLabel::String(text) => assert_eq!(text, "📴"),
764 _ => panic!("Expected string label"),
765 }
766 }
767
768 #[test]
772 fn test_inlay_hint_offline_cold_with_resolved_shows_marker_and_version() {
773 use std::collections::HashMap;
774 use tower_lsp_server::ls_types::{Position, Range};
775
776 let formatter = MockFormatter;
777 let config = EcosystemConfig {
778 show_up_to_date_hints: true,
779 up_to_date_text: "✅".to_string(),
780 needs_update_text: "❌ {}".to_string(),
781 loading_text: "⏳".to_string(),
782 show_loading_hints: true,
783 offline: true,
784 };
785
786 let parse_result = MockParseResult {
787 deps: vec![MockDep {
788 name: "serde".into(),
789 version_req: "^2.0".into(),
790 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
791 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
792 }],
793 uri: crate::test_util::test_uri("/test/Cargo.toml"),
794 };
795
796 let cached_versions = HashMap::new();
797 let mut resolved_versions = HashMap::new();
798 resolved_versions.insert("serde".into(), "2.0.12".into());
799
800 let hints = generate_inlay_hints(
801 &parse_result,
802 VersionData::new(&cached_versions, &resolved_versions),
803 crate::LoadingState::Loaded,
804 &config,
805 &formatter,
806 );
807
808 assert_eq!(hints.len(), 1);
809 match &hints[0].label {
810 InlayHintLabel::String(text) => assert_eq!(text, "📴 2.0.12"),
811 _ => panic!("Expected string label"),
812 }
813 }
814
815 #[test]
820 fn test_inlay_hint_offline_warm_cache_appends_marker_to_outdated_badge() {
821 use std::collections::HashMap;
822 use tower_lsp_server::ls_types::{Position, Range};
823
824 let formatter = MockFormatter;
825 let config = EcosystemConfig {
826 show_up_to_date_hints: true,
827 up_to_date_text: "✅".to_string(),
828 needs_update_text: "❌ {}".to_string(),
829 loading_text: "⏳".to_string(),
830 show_loading_hints: true,
831 offline: true,
832 };
833
834 let parse_result = MockParseResult {
835 deps: vec![MockDep {
836 name: "serde".into(),
837 version_req: "=2.0.12".into(),
838 version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
839 name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
840 }],
841 uri: crate::test_util::test_uri("/test/Cargo.toml"),
842 };
843
844 let mut cached_versions = HashMap::new();
845 cached_versions.insert("serde".into(), PackageVersions::latest_only("2.1.1"));
846
847 let mut resolved_versions = HashMap::new();
848 resolved_versions.insert("serde".into(), "2.0.12".into());
849
850 let hints = generate_inlay_hints(
851 &parse_result,
852 VersionData::new(&cached_versions, &resolved_versions),
853 crate::LoadingState::Loaded,
854 &config,
855 &formatter,
856 );
857
858 assert_eq!(hints.len(), 1);
859 match &hints[0].label {
860 InlayHintLabel::String(text) => assert_eq!(text, "❌ 2.1.1 📴"),
861 _ => panic!("Expected string label"),
862 }
863 }
864}