1use crate::HttpCache;
13use crate::parser::DependencyInfo;
14use crate::registry::{PackageRegistry, VersionInfo};
15use async_trait::async_trait;
16use futures::future::join_all;
17use std::collections::HashMap;
18use std::sync::Arc;
19use tower_lsp::lsp_types::{
20 InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart, MarkupContent, MarkupKind, Range,
21};
22
23const MAX_VERSIONS_IN_HOVER: usize = 8;
25
26const MAX_FEATURES_IN_HOVER: usize = 10;
28
29const MAX_CODE_ACTION_VERSIONS: usize = 5;
31
32#[async_trait]
134pub trait EcosystemHandler: Send + Sync + Sized {
135 type Registry: PackageRegistry + Clone;
137
138 type Dependency: DependencyInfo;
140
141 type UnifiedDep;
146
147 fn new(cache: Arc<HttpCache>) -> Self;
149
150 fn registry(&self) -> &Self::Registry;
152
153 fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency>;
158
159 fn package_url(name: &str) -> String;
163
164 fn ecosystem_display_name() -> &'static str;
168
169 fn is_version_latest(version_req: &str, latest: &str) -> bool;
174
175 fn format_version_for_edit(dep: &Self::Dependency, version: &str) -> String;
183
184 fn is_deprecated(version: &<Self::Registry as PackageRegistry>::Version) -> bool;
188
189 fn is_valid_version_syntax(version_req: &str) -> bool;
194
195 fn parse_version_req(
199 version_req: &str,
200 ) -> Option<<Self::Registry as PackageRegistry>::VersionReq>;
201
202 fn lockfile_provider(&self) -> Option<Arc<dyn crate::lockfile::LockFileProvider>> {
216 None
217 }
218}
219
220pub struct InlayHintsConfig {
225 pub enabled: bool,
226 pub up_to_date_text: String,
227 pub needs_update_text: String,
228}
229
230impl Default for InlayHintsConfig {
231 fn default() -> Self {
232 Self {
233 enabled: true,
234 up_to_date_text: "✅".to_string(),
235 needs_update_text: "❌ {}".to_string(),
236 }
237 }
238}
239
240pub trait VersionStringGetter {
244 fn version_string(&self) -> &str;
245}
246
247pub trait YankedChecker {
251 fn is_yanked(&self) -> bool;
252}
253
254pub async fn generate_inlay_hints<H, UnifiedVer>(
277 handler: &H,
278 dependencies: &[H::UnifiedDep],
279 cached_versions: &HashMap<String, UnifiedVer>,
280 resolved_versions: &HashMap<String, String>,
281 config: &InlayHintsConfig,
282) -> Vec<InlayHint>
283where
284 H: EcosystemHandler,
285 UnifiedVer: VersionStringGetter + YankedChecker,
286{
287 let mut cached_deps = Vec::with_capacity(dependencies.len());
289 let mut fetch_deps = Vec::with_capacity(dependencies.len());
290
291 for dep in dependencies {
292 let Some(typed_dep) = H::extract_dependency(dep) else {
293 continue;
294 };
295
296 let Some(version_req) = typed_dep.version_requirement() else {
297 continue;
298 };
299 let Some(version_range) = typed_dep.version_range() else {
300 continue;
301 };
302
303 let name = typed_dep.name();
304 if let Some(cached) = cached_versions.get(name) {
305 cached_deps.push((
306 name.to_string(),
307 version_req.to_string(),
308 version_range,
309 cached.version_string().to_string(),
310 cached.is_yanked(),
311 ));
312 } else {
313 fetch_deps.push((name.to_string(), version_req.to_string(), version_range));
314 }
315 }
316
317 let registry = handler.registry().clone();
318 let futures: Vec<_> = fetch_deps
319 .into_iter()
320 .map(|(name, version_req, version_range)| {
321 let registry = registry.clone();
322 async move {
323 let result = registry.get_versions(&name).await;
324 (name, version_req, version_range, result)
325 }
326 })
327 .collect();
328
329 let fetch_results = join_all(futures).await;
330
331 let mut hints = Vec::new();
332
333 for (name, version_req, version_range, latest_version, is_yanked) in cached_deps {
334 if is_yanked {
335 continue;
336 }
337 let version_to_compare = resolved_versions
339 .get(&name)
340 .map(String::as_str)
341 .unwrap_or(&version_req);
342 let is_latest = H::is_version_latest(version_to_compare, &latest_version);
343 hints.push(create_hint::<H>(
344 &name,
345 version_range,
346 &latest_version,
347 is_latest,
348 config,
349 ));
350 }
351
352 for (name, version_req, version_range, result) in fetch_results {
353 let Ok(versions): std::result::Result<Vec<<H::Registry as PackageRegistry>::Version>, _> =
354 result
355 else {
356 tracing::warn!("Failed to fetch versions for {}", name);
357 continue;
358 };
359
360 let Some(latest) = versions
361 .iter()
362 .find(|v: &&<H::Registry as PackageRegistry>::Version| !v.is_yanked())
363 else {
364 tracing::warn!("No non-yanked versions found for '{}'", name);
365 continue;
366 };
367
368 let version_to_compare = resolved_versions
370 .get(&name)
371 .map(String::as_str)
372 .unwrap_or(&version_req);
373 let is_latest = H::is_version_latest(version_to_compare, latest.version_string());
374 hints.push(create_hint::<H>(
375 &name,
376 version_range,
377 latest.version_string(),
378 is_latest,
379 config,
380 ));
381 }
382
383 hints
384}
385
386#[inline]
387fn create_hint<H: EcosystemHandler>(
388 name: &str,
389 version_range: Range,
390 latest_version: &str,
391 is_latest: bool,
392 config: &InlayHintsConfig,
393) -> InlayHint {
394 let label_text = if is_latest {
395 config.up_to_date_text.clone()
396 } else {
397 config.needs_update_text.replace("{}", latest_version)
398 };
399
400 let url = H::package_url(name);
401 let tooltip_content = format!(
402 "[{}]({}) - {}\n\nLatest: **{}**",
403 name, url, url, latest_version
404 );
405
406 InlayHint {
407 position: version_range.end,
408 label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
409 value: label_text,
410 tooltip: Some(
411 tower_lsp::lsp_types::InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
412 kind: MarkupKind::Markdown,
413 value: tooltip_content,
414 }),
415 ),
416 location: None,
417 command: Some(tower_lsp::lsp_types::Command {
418 title: format!("Open on {}", H::ecosystem_display_name()),
419 command: "vscode.open".into(),
420 arguments: Some(vec![serde_json::json!(url)]),
421 }),
422 }]),
423 kind: Some(InlayHintKind::TYPE),
424 text_edits: None,
425 tooltip: None,
426 padding_left: Some(true),
427 padding_right: None,
428 data: None,
429 }
430}
431
432pub async fn generate_hover<H>(
447 handler: &H,
448 dep: &H::UnifiedDep,
449 resolved_version: Option<&str>,
450) -> Option<tower_lsp::lsp_types::Hover>
451where
452 H: EcosystemHandler,
453{
454 use tower_lsp::lsp_types::{Hover, HoverContents};
455
456 let typed_dep = H::extract_dependency(dep)?;
457 let registry = handler.registry();
458 let versions: Vec<<H::Registry as PackageRegistry>::Version> =
459 registry.get_versions(typed_dep.name()).await.ok()?;
460 let latest: &<H::Registry as PackageRegistry>::Version = versions.first()?;
461
462 let url = H::package_url(typed_dep.name());
463 let mut markdown = format!("# [{}]({})\n\n", typed_dep.name(), url);
464
465 if let Some(version) = resolved_version.or(typed_dep.version_requirement()) {
466 markdown.push_str(&format!("**Current**: `{}`\n\n", version));
467 }
468
469 if latest.is_yanked() {
470 markdown.push_str("⚠️ **Warning**: This version has been yanked\n\n");
471 }
472
473 markdown.push_str("**Versions** *(use Cmd+. to update)*:\n");
474 for (i, version) in versions.iter().take(MAX_VERSIONS_IN_HOVER).enumerate() {
475 if i == 0 {
476 markdown.push_str(&format!("- {} *(latest)*\n", version.version_string()));
477 } else {
478 markdown.push_str(&format!("- {}\n", version.version_string()));
479 }
480 }
481 if versions.len() > MAX_VERSIONS_IN_HOVER {
482 markdown.push_str(&format!(
483 "- *...and {} more*\n",
484 versions.len() - MAX_VERSIONS_IN_HOVER
485 ));
486 }
487
488 let features = latest.features();
489 if !features.is_empty() {
490 markdown.push_str("\n**Features**:\n");
491 for feature in features.iter().take(MAX_FEATURES_IN_HOVER) {
492 markdown.push_str(&format!("- `{}`\n", feature));
493 }
494 if features.len() > MAX_FEATURES_IN_HOVER {
495 markdown.push_str(&format!(
496 "- *...and {} more*\n",
497 features.len() - MAX_FEATURES_IN_HOVER
498 ));
499 }
500 }
501
502 Some(Hover {
503 contents: HoverContents::Markup(MarkupContent {
504 kind: MarkupKind::Markdown,
505 value: markdown,
506 }),
507 range: Some(typed_dep.name_range()),
508 })
509}
510
511pub struct DiagnosticsConfig {
515 pub unknown_severity: tower_lsp::lsp_types::DiagnosticSeverity,
516 pub yanked_severity: tower_lsp::lsp_types::DiagnosticSeverity,
517 pub outdated_severity: tower_lsp::lsp_types::DiagnosticSeverity,
518}
519
520impl Default for DiagnosticsConfig {
521 fn default() -> Self {
522 use tower_lsp::lsp_types::DiagnosticSeverity;
523 Self {
524 unknown_severity: DiagnosticSeverity::WARNING,
525 yanked_severity: DiagnosticSeverity::WARNING,
526 outdated_severity: DiagnosticSeverity::HINT,
527 }
528 }
529}
530
531pub async fn generate_code_actions<H>(
550 handler: &H,
551 dependencies: &[H::UnifiedDep],
552 uri: &tower_lsp::lsp_types::Url,
553 selected_range: Range,
554) -> Vec<tower_lsp::lsp_types::CodeActionOrCommand>
555where
556 H: EcosystemHandler,
557{
558 use tower_lsp::lsp_types::{
559 CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit, WorkspaceEdit,
560 };
561
562 let mut deps_to_check = Vec::new();
563 for dep in dependencies {
564 let Some(typed_dep) = H::extract_dependency(dep) else {
565 continue;
566 };
567
568 let Some(version_range) = typed_dep.version_range() else {
569 continue;
570 };
571
572 if !ranges_overlap(version_range, selected_range) {
574 continue;
575 }
576
577 deps_to_check.push((typed_dep, version_range));
578 }
579
580 if deps_to_check.is_empty() {
581 return vec![];
582 }
583
584 let registry = handler.registry().clone();
585 let futures: Vec<_> = deps_to_check
586 .iter()
587 .map(|(dep, version_range)| {
588 let name = dep.name().to_string();
589 let version_range = *version_range;
590 let registry = registry.clone();
591 async move {
592 let versions = registry.get_versions(&name).await;
593 (name, dep, version_range, versions)
594 }
595 })
596 .collect();
597
598 let results = join_all(futures).await;
599
600 let mut actions = Vec::new();
601 for (name, dep, version_range, versions_result) in results {
602 let Ok(versions) = versions_result else {
603 tracing::warn!("Failed to fetch versions for {}", name);
604 continue;
605 };
606
607 for (i, version) in versions
608 .iter()
609 .filter(|v| !H::is_deprecated(v))
610 .take(MAX_CODE_ACTION_VERSIONS)
611 .enumerate()
612 {
613 let new_text = H::format_version_for_edit(dep, version.version_string());
614
615 let mut edits = std::collections::HashMap::new();
616 edits.insert(
617 uri.clone(),
618 vec![TextEdit {
619 range: version_range,
620 new_text,
621 }],
622 );
623
624 let title = if i == 0 {
625 format!("Update {} to {} (latest)", name, version.version_string())
626 } else {
627 format!("Update {} to {}", name, version.version_string())
628 };
629
630 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
631 title,
632 kind: Some(CodeActionKind::REFACTOR),
633 edit: Some(WorkspaceEdit {
634 changes: Some(edits),
635 ..Default::default()
636 }),
637 is_preferred: Some(i == 0),
638 ..Default::default()
639 }));
640 }
641 }
642
643 actions
644}
645
646fn ranges_overlap(a: Range, b: Range) -> bool {
647 !(a.end.line < b.start.line
648 || (a.end.line == b.start.line && a.end.character < b.start.character)
649 || b.end.line < a.start.line
650 || (b.end.line == a.start.line && b.end.character < a.start.character))
651}
652
653pub async fn generate_diagnostics<H>(
675 handler: &H,
676 dependencies: &[H::UnifiedDep],
677 config: &DiagnosticsConfig,
678) -> Vec<tower_lsp::lsp_types::Diagnostic>
679where
680 H: EcosystemHandler,
681{
682 use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
683
684 let mut deps_to_check = Vec::new();
685 for dep in dependencies {
686 let Some(typed_dep) = H::extract_dependency(dep) else {
687 continue;
688 };
689 deps_to_check.push(typed_dep);
690 }
691
692 if deps_to_check.is_empty() {
693 return vec![];
694 }
695
696 let registry = handler.registry().clone();
697 let futures: Vec<_> = deps_to_check
698 .iter()
699 .map(|dep| {
700 let name = dep.name().to_string();
701 let registry = registry.clone();
702 async move {
703 let versions = registry.get_versions(&name).await;
704 (name, versions)
705 }
706 })
707 .collect();
708
709 let version_results = join_all(futures).await;
710
711 let mut diagnostics = Vec::new();
712
713 for (i, dep) in deps_to_check.iter().enumerate() {
714 let (name, version_result) = &version_results[i];
715
716 let versions = match version_result {
717 Ok(v) => v,
718 Err(_) => {
719 diagnostics.push(Diagnostic {
720 range: dep.name_range(),
721 severity: Some(config.unknown_severity),
722 message: format!("Unknown package '{}'", name),
723 source: Some("deps-lsp".into()),
724 ..Default::default()
725 });
726 continue;
727 }
728 };
729
730 if let Some(version_req) = dep.version_requirement()
731 && let Some(version_range) = dep.version_range()
732 {
733 let Some(parsed_version_req) = H::parse_version_req(version_req) else {
734 diagnostics.push(Diagnostic {
735 range: version_range,
736 severity: Some(DiagnosticSeverity::ERROR),
737 message: format!("Invalid version requirement '{}'", version_req),
738 source: Some("deps-lsp".into()),
739 ..Default::default()
740 });
741 continue;
742 };
743
744 let matching = handler
745 .registry()
746 .get_latest_matching(name, &parsed_version_req)
747 .await
748 .ok()
749 .flatten();
750
751 if let Some(current) = &matching
752 && H::is_deprecated(current)
753 {
754 diagnostics.push(Diagnostic {
755 range: version_range,
756 severity: Some(config.yanked_severity),
757 message: "This version has been yanked".into(),
758 source: Some("deps-lsp".into()),
759 ..Default::default()
760 });
761 }
762
763 let latest = versions.iter().find(|v| !H::is_deprecated(v));
764 if let (Some(latest), Some(current)) = (latest, &matching)
765 && latest.version_string() != current.version_string()
766 {
767 diagnostics.push(Diagnostic {
768 range: version_range,
769 severity: Some(config.outdated_severity),
770 message: format!("Newer version available: {}", latest.version_string()),
771 source: Some("deps-lsp".into()),
772 ..Default::default()
773 });
774 }
775 }
776 }
777
778 diagnostics
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784 use crate::registry::PackageMetadata;
785 use tower_lsp::lsp_types::{Position, Range};
786
787 #[derive(Clone)]
788 struct MockVersion {
789 version: String,
790 yanked: bool,
791 features: Vec<String>,
792 }
793
794 impl VersionInfo for MockVersion {
795 fn version_string(&self) -> &str {
796 &self.version
797 }
798
799 fn is_yanked(&self) -> bool {
800 self.yanked
801 }
802
803 fn features(&self) -> Vec<String> {
804 self.features.clone()
805 }
806 }
807
808 #[derive(Clone)]
809 struct MockMetadata {
810 name: String,
811 description: Option<String>,
812 latest: String,
813 }
814
815 impl PackageMetadata for MockMetadata {
816 fn name(&self) -> &str {
817 &self.name
818 }
819
820 fn description(&self) -> Option<&str> {
821 self.description.as_deref()
822 }
823
824 fn repository(&self) -> Option<&str> {
825 None
826 }
827
828 fn documentation(&self) -> Option<&str> {
829 None
830 }
831
832 fn latest_version(&self) -> &str {
833 &self.latest
834 }
835 }
836
837 #[derive(Clone)]
838 struct MockDependency {
839 name: String,
840 version_req: Option<String>,
841 version_range: Option<Range>,
842 name_range: Range,
843 }
844
845 impl crate::parser::DependencyInfo for MockDependency {
846 fn name(&self) -> &str {
847 &self.name
848 }
849
850 fn name_range(&self) -> Range {
851 self.name_range
852 }
853
854 fn version_requirement(&self) -> Option<&str> {
855 self.version_req.as_deref()
856 }
857
858 fn version_range(&self) -> Option<Range> {
859 self.version_range
860 }
861
862 fn source(&self) -> crate::parser::DependencySource {
863 crate::parser::DependencySource::Registry
864 }
865 }
866
867 struct MockRegistry {
868 versions: std::collections::HashMap<String, Vec<MockVersion>>,
869 }
870
871 impl Clone for MockRegistry {
872 fn clone(&self) -> Self {
873 Self {
874 versions: self.versions.clone(),
875 }
876 }
877 }
878
879 #[async_trait]
880 impl crate::registry::PackageRegistry for MockRegistry {
881 type Version = MockVersion;
882 type Metadata = MockMetadata;
883 type VersionReq = String;
884
885 async fn get_versions(&self, name: &str) -> crate::error::Result<Vec<Self::Version>> {
886 self.versions.get(name).cloned().ok_or_else(|| {
887 use std::io::{Error as IoError, ErrorKind};
888 crate::DepsError::Io(IoError::new(ErrorKind::NotFound, "package not found"))
889 })
890 }
891
892 async fn get_latest_matching(
893 &self,
894 name: &str,
895 req: &Self::VersionReq,
896 ) -> crate::error::Result<Option<Self::Version>> {
897 Ok(self
898 .versions
899 .get(name)
900 .and_then(|versions| versions.iter().find(|v| v.version == *req).cloned()))
901 }
902
903 async fn search(
904 &self,
905 _query: &str,
906 _limit: usize,
907 ) -> crate::error::Result<Vec<Self::Metadata>> {
908 Ok(vec![])
909 }
910 }
911
912 struct MockHandler {
913 registry: MockRegistry,
914 }
915
916 #[async_trait]
917 impl EcosystemHandler for MockHandler {
918 type Registry = MockRegistry;
919 type Dependency = MockDependency;
920 type UnifiedDep = MockDependency;
921
922 fn new(_cache: Arc<HttpCache>) -> Self {
923 let mut versions = std::collections::HashMap::new();
924 versions.insert(
925 "serde".to_string(),
926 vec![
927 MockVersion {
928 version: "1.0.195".to_string(),
929 yanked: false,
930 features: vec!["derive".to_string(), "alloc".to_string()],
931 },
932 MockVersion {
933 version: "1.0.194".to_string(),
934 yanked: false,
935 features: vec![],
936 },
937 ],
938 );
939 versions.insert(
940 "yanked-pkg".to_string(),
941 vec![MockVersion {
942 version: "1.0.0".to_string(),
943 yanked: true,
944 features: vec![],
945 }],
946 );
947
948 Self {
949 registry: MockRegistry { versions },
950 }
951 }
952
953 fn registry(&self) -> &Self::Registry {
954 &self.registry
955 }
956
957 fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
958 Some(dep)
959 }
960
961 fn package_url(name: &str) -> String {
962 format!("https://test.io/pkg/{}", name)
963 }
964
965 fn ecosystem_display_name() -> &'static str {
966 "Test Registry"
967 }
968
969 fn is_version_latest(version_req: &str, latest: &str) -> bool {
970 version_req == latest
971 }
972
973 fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
974 format!("\"{}\"", version)
975 }
976
977 fn is_deprecated(version: &MockVersion) -> bool {
978 version.yanked
979 }
980
981 fn is_valid_version_syntax(_version_req: &str) -> bool {
982 true
983 }
984
985 fn parse_version_req(version_req: &str) -> Option<String> {
986 Some(version_req.to_string())
987 }
988 }
989
990 impl VersionStringGetter for MockVersion {
991 fn version_string(&self) -> &str {
992 &self.version
993 }
994 }
995
996 impl YankedChecker for MockVersion {
997 fn is_yanked(&self) -> bool {
998 self.yanked
999 }
1000 }
1001
1002 #[test]
1003 fn test_inlay_hints_config_default() {
1004 let config = InlayHintsConfig::default();
1005 assert!(config.enabled);
1006 assert_eq!(config.up_to_date_text, "✅");
1007 assert_eq!(config.needs_update_text, "❌ {}");
1008 }
1009
1010 #[tokio::test]
1011 async fn test_generate_inlay_hints_cached() {
1012 let cache = Arc::new(HttpCache::new());
1013 let handler = MockHandler::new(cache);
1014
1015 let deps = vec![MockDependency {
1016 name: "serde".to_string(),
1017 version_req: Some("1.0.195".to_string()),
1018 version_range: Some(Range {
1019 start: Position {
1020 line: 0,
1021 character: 10,
1022 },
1023 end: Position {
1024 line: 0,
1025 character: 20,
1026 },
1027 }),
1028 name_range: Range::default(),
1029 }];
1030
1031 let mut cached_versions = HashMap::new();
1032 cached_versions.insert(
1033 "serde".to_string(),
1034 MockVersion {
1035 version: "1.0.195".to_string(),
1036 yanked: false,
1037 features: vec![],
1038 },
1039 );
1040
1041 let config = InlayHintsConfig::default();
1042 let resolved_versions: HashMap<String, String> = HashMap::new();
1043 let hints = generate_inlay_hints(
1044 &handler,
1045 &deps,
1046 &cached_versions,
1047 &resolved_versions,
1048 &config,
1049 )
1050 .await;
1051
1052 assert_eq!(hints.len(), 1);
1053 assert_eq!(hints[0].position.line, 0);
1054 assert_eq!(hints[0].position.character, 20);
1055 }
1056
1057 #[tokio::test]
1058 async fn test_generate_inlay_hints_fetch() {
1059 let cache = Arc::new(HttpCache::new());
1060 let handler = MockHandler::new(cache);
1061
1062 let deps = vec![MockDependency {
1063 name: "serde".to_string(),
1064 version_req: Some("1.0.0".to_string()),
1065 version_range: Some(Range {
1066 start: Position {
1067 line: 0,
1068 character: 10,
1069 },
1070 end: Position {
1071 line: 0,
1072 character: 20,
1073 },
1074 }),
1075 name_range: Range::default(),
1076 }];
1077
1078 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1079 let config = InlayHintsConfig::default();
1080 let resolved_versions: HashMap<String, String> = HashMap::new();
1081 let hints = generate_inlay_hints(
1082 &handler,
1083 &deps,
1084 &cached_versions,
1085 &resolved_versions,
1086 &config,
1087 )
1088 .await;
1089
1090 assert_eq!(hints.len(), 1);
1091 }
1092
1093 #[tokio::test]
1094 async fn test_generate_inlay_hints_skips_yanked() {
1095 let cache = Arc::new(HttpCache::new());
1096 let handler = MockHandler::new(cache);
1097
1098 let deps = vec![MockDependency {
1099 name: "serde".to_string(),
1100 version_req: Some("1.0.195".to_string()),
1101 version_range: Some(Range {
1102 start: Position {
1103 line: 0,
1104 character: 10,
1105 },
1106 end: Position {
1107 line: 0,
1108 character: 20,
1109 },
1110 }),
1111 name_range: Range::default(),
1112 }];
1113
1114 let mut cached_versions = HashMap::new();
1115 cached_versions.insert(
1116 "serde".to_string(),
1117 MockVersion {
1118 version: "1.0.195".to_string(),
1119 yanked: true,
1120 features: vec![],
1121 },
1122 );
1123
1124 let config = InlayHintsConfig::default();
1125 let resolved_versions: HashMap<String, String> = HashMap::new();
1126 let hints = generate_inlay_hints(
1127 &handler,
1128 &deps,
1129 &cached_versions,
1130 &resolved_versions,
1131 &config,
1132 )
1133 .await;
1134
1135 assert_eq!(hints.len(), 0);
1136 }
1137
1138 #[tokio::test]
1139 async fn test_generate_inlay_hints_no_version_range() {
1140 let cache = Arc::new(HttpCache::new());
1141 let handler = MockHandler::new(cache);
1142
1143 let deps = vec![MockDependency {
1144 name: "serde".to_string(),
1145 version_req: Some("1.0.195".to_string()),
1146 version_range: None,
1147 name_range: Range::default(),
1148 }];
1149
1150 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1151 let config = InlayHintsConfig::default();
1152 let resolved_versions: HashMap<String, String> = HashMap::new();
1153 let hints = generate_inlay_hints(
1154 &handler,
1155 &deps,
1156 &cached_versions,
1157 &resolved_versions,
1158 &config,
1159 )
1160 .await;
1161
1162 assert_eq!(hints.len(), 0);
1163 }
1164
1165 #[tokio::test]
1166 async fn test_generate_inlay_hints_no_version_req() {
1167 let cache = Arc::new(HttpCache::new());
1168 let handler = MockHandler::new(cache);
1169
1170 let deps = vec![MockDependency {
1171 name: "serde".to_string(),
1172 version_req: None,
1173 version_range: Some(Range {
1174 start: Position {
1175 line: 0,
1176 character: 10,
1177 },
1178 end: Position {
1179 line: 0,
1180 character: 20,
1181 },
1182 }),
1183 name_range: Range::default(),
1184 }];
1185
1186 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1187 let config = InlayHintsConfig::default();
1188 let resolved_versions: HashMap<String, String> = HashMap::new();
1189 let hints = generate_inlay_hints(
1190 &handler,
1191 &deps,
1192 &cached_versions,
1193 &resolved_versions,
1194 &config,
1195 )
1196 .await;
1197
1198 assert_eq!(hints.len(), 0);
1199 }
1200
1201 #[test]
1202 fn test_create_hint_up_to_date() {
1203 let config = InlayHintsConfig::default();
1204 let range = Range {
1205 start: Position {
1206 line: 5,
1207 character: 10,
1208 },
1209 end: Position {
1210 line: 5,
1211 character: 20,
1212 },
1213 };
1214
1215 let hint = create_hint::<MockHandler>("serde", range, "1.0.195", true, &config);
1216
1217 assert_eq!(hint.position, range.end);
1218 if let InlayHintLabel::LabelParts(parts) = hint.label {
1219 assert_eq!(parts[0].value, "✅");
1220 } else {
1221 panic!("Expected LabelParts");
1222 }
1223 }
1224
1225 #[test]
1226 fn test_create_hint_needs_update() {
1227 let config = InlayHintsConfig::default();
1228 let range = Range {
1229 start: Position {
1230 line: 5,
1231 character: 10,
1232 },
1233 end: Position {
1234 line: 5,
1235 character: 20,
1236 },
1237 };
1238
1239 let hint = create_hint::<MockHandler>("serde", range, "1.0.200", false, &config);
1240
1241 assert_eq!(hint.position, range.end);
1242 if let InlayHintLabel::LabelParts(parts) = hint.label {
1243 assert_eq!(parts[0].value, "❌ 1.0.200");
1244 } else {
1245 panic!("Expected LabelParts");
1246 }
1247 }
1248
1249 #[test]
1250 fn test_create_hint_custom_config() {
1251 let config = InlayHintsConfig {
1252 enabled: true,
1253 up_to_date_text: "OK".to_string(),
1254 needs_update_text: "UPDATE: {}".to_string(),
1255 };
1256 let range = Range {
1257 start: Position {
1258 line: 0,
1259 character: 0,
1260 },
1261 end: Position {
1262 line: 0,
1263 character: 10,
1264 },
1265 };
1266
1267 let hint = create_hint::<MockHandler>("test", range, "2.0.0", false, &config);
1268
1269 if let InlayHintLabel::LabelParts(parts) = hint.label {
1270 assert_eq!(parts[0].value, "UPDATE: 2.0.0");
1271 } else {
1272 panic!("Expected LabelParts");
1273 }
1274 }
1275
1276 #[tokio::test]
1277 async fn test_generate_hover() {
1278 let cache = Arc::new(HttpCache::new());
1279 let handler = MockHandler::new(cache);
1280
1281 let dep = MockDependency {
1282 name: "serde".to_string(),
1283 version_req: Some("1.0.0".to_string()),
1284 version_range: Some(Range::default()),
1285 name_range: Range {
1286 start: Position {
1287 line: 0,
1288 character: 0,
1289 },
1290 end: Position {
1291 line: 0,
1292 character: 5,
1293 },
1294 },
1295 };
1296
1297 let hover = generate_hover(&handler, &dep, None).await;
1298
1299 assert!(hover.is_some());
1300 let hover = hover.unwrap();
1301
1302 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1303 assert!(content.value.contains("serde"));
1304 assert!(content.value.contains("1.0.195"));
1305 assert!(content.value.contains("Current"));
1306 assert!(content.value.contains("Features"));
1307 assert!(content.value.contains("derive"));
1308 } else {
1309 panic!("Expected Markup content");
1310 }
1311 }
1312
1313 #[tokio::test]
1314 async fn test_generate_hover_yanked_version() {
1315 let cache = Arc::new(HttpCache::new());
1316 let handler = MockHandler::new(cache);
1317
1318 let dep = MockDependency {
1319 name: "yanked-pkg".to_string(),
1320 version_req: Some("1.0.0".to_string()),
1321 version_range: Some(Range::default()),
1322 name_range: Range::default(),
1323 };
1324
1325 let hover = generate_hover(&handler, &dep, None).await;
1326
1327 assert!(hover.is_some());
1328 let hover = hover.unwrap();
1329
1330 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1331 assert!(content.value.contains("Warning"));
1332 assert!(content.value.contains("yanked"));
1333 } else {
1334 panic!("Expected Markup content");
1335 }
1336 }
1337
1338 #[tokio::test]
1339 async fn test_generate_hover_no_versions() {
1340 let cache = Arc::new(HttpCache::new());
1341 let handler = MockHandler::new(cache);
1342
1343 let dep = MockDependency {
1344 name: "nonexistent".to_string(),
1345 version_req: Some("1.0.0".to_string()),
1346 version_range: Some(Range::default()),
1347 name_range: Range::default(),
1348 };
1349
1350 let hover = generate_hover(&handler, &dep, None).await;
1351 assert!(hover.is_none());
1352 }
1353
1354 #[tokio::test]
1355 async fn test_generate_hover_no_version_req() {
1356 let cache = Arc::new(HttpCache::new());
1357 let handler = MockHandler::new(cache);
1358
1359 let dep = MockDependency {
1360 name: "serde".to_string(),
1361 version_req: None,
1362 version_range: Some(Range::default()),
1363 name_range: Range::default(),
1364 };
1365
1366 let hover = generate_hover(&handler, &dep, None).await;
1367
1368 assert!(hover.is_some());
1369 let hover = hover.unwrap();
1370
1371 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1372 assert!(!content.value.contains("Current"));
1373 } else {
1374 panic!("Expected Markup content");
1375 }
1376 }
1377
1378 #[tokio::test]
1379 async fn test_generate_hover_with_resolved_version() {
1380 let cache = Arc::new(HttpCache::new());
1381 let handler = MockHandler::new(cache);
1382
1383 let dep = MockDependency {
1384 name: "serde".to_string(),
1385 version_req: Some("1.0".to_string()), version_range: Some(Range::default()),
1387 name_range: Range {
1388 start: Position {
1389 line: 0,
1390 character: 0,
1391 },
1392 end: Position {
1393 line: 0,
1394 character: 5,
1395 },
1396 },
1397 };
1398
1399 let hover = generate_hover(&handler, &dep, Some("1.0.195")).await;
1401
1402 assert!(hover.is_some());
1403 let hover = hover.unwrap();
1404
1405 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1406 assert!(content.value.contains("**Current**: `1.0.195`"));
1408 assert!(!content.value.contains("**Current**: `1.0`"));
1409 } else {
1410 panic!("Expected Markup content");
1411 }
1412 }
1413
1414 #[tokio::test]
1415 async fn test_generate_code_actions_empty_when_up_to_date() {
1416 use tower_lsp::lsp_types::Url;
1417
1418 let cache = Arc::new(HttpCache::new());
1419 let handler = MockHandler::new(cache);
1420
1421 let deps = vec![MockDependency {
1422 name: "serde".to_string(),
1423 version_req: Some("1.0.195".to_string()),
1424 version_range: Some(Range {
1425 start: Position {
1426 line: 0,
1427 character: 10,
1428 },
1429 end: Position {
1430 line: 0,
1431 character: 20,
1432 },
1433 }),
1434 name_range: Range::default(),
1435 }];
1436
1437 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1438 let selected_range = Range {
1439 start: Position {
1440 line: 0,
1441 character: 15,
1442 },
1443 end: Position {
1444 line: 0,
1445 character: 15,
1446 },
1447 };
1448
1449 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1450
1451 assert!(!actions.is_empty());
1452 }
1453
1454 #[tokio::test]
1455 async fn test_generate_code_actions_update_outdated() {
1456 use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1457
1458 let cache = Arc::new(HttpCache::new());
1459 let handler = MockHandler::new(cache);
1460
1461 let deps = vec![MockDependency {
1462 name: "serde".to_string(),
1463 version_req: Some("1.0.0".to_string()),
1464 version_range: Some(Range {
1465 start: Position {
1466 line: 0,
1467 character: 10,
1468 },
1469 end: Position {
1470 line: 0,
1471 character: 20,
1472 },
1473 }),
1474 name_range: Range::default(),
1475 }];
1476
1477 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1478 let selected_range = Range {
1479 start: Position {
1480 line: 0,
1481 character: 15,
1482 },
1483 end: Position {
1484 line: 0,
1485 character: 15,
1486 },
1487 };
1488
1489 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1490
1491 assert!(!actions.is_empty());
1492 assert!(actions.len() <= 5);
1493
1494 if let CodeActionOrCommand::CodeAction(action) = &actions[0] {
1495 assert!(action.title.contains("1.0.195"));
1496 assert!(action.title.contains("latest"));
1497 assert_eq!(action.is_preferred, Some(true));
1498 } else {
1499 panic!("Expected CodeAction");
1500 }
1501 }
1502
1503 #[tokio::test]
1504 async fn test_generate_code_actions_missing_version_range() {
1505 use tower_lsp::lsp_types::Url;
1506
1507 let cache = Arc::new(HttpCache::new());
1508 let handler = MockHandler::new(cache);
1509
1510 let deps = vec![MockDependency {
1511 name: "serde".to_string(),
1512 version_req: Some("1.0.0".to_string()),
1513 version_range: None,
1514 name_range: Range::default(),
1515 }];
1516
1517 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1518 let selected_range = Range {
1519 start: Position {
1520 line: 0,
1521 character: 15,
1522 },
1523 end: Position {
1524 line: 0,
1525 character: 15,
1526 },
1527 };
1528
1529 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1530
1531 assert_eq!(actions.len(), 0);
1532 }
1533
1534 #[tokio::test]
1535 async fn test_generate_code_actions_no_overlap() {
1536 use tower_lsp::lsp_types::Url;
1537
1538 let cache = Arc::new(HttpCache::new());
1539 let handler = MockHandler::new(cache);
1540
1541 let deps = vec![MockDependency {
1542 name: "serde".to_string(),
1543 version_req: Some("1.0.0".to_string()),
1544 version_range: Some(Range {
1545 start: Position {
1546 line: 0,
1547 character: 10,
1548 },
1549 end: Position {
1550 line: 0,
1551 character: 20,
1552 },
1553 }),
1554 name_range: Range::default(),
1555 }];
1556
1557 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1558 let selected_range = Range {
1559 start: Position {
1560 line: 5,
1561 character: 0,
1562 },
1563 end: Position {
1564 line: 5,
1565 character: 10,
1566 },
1567 };
1568
1569 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1570
1571 assert_eq!(actions.len(), 0);
1572 }
1573
1574 #[tokio::test]
1575 async fn test_generate_code_actions_filters_deprecated() {
1576 use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1577
1578 let cache = Arc::new(HttpCache::new());
1579 let handler = MockHandler::new(cache);
1580
1581 let deps = vec![MockDependency {
1582 name: "yanked-pkg".to_string(),
1583 version_req: Some("1.0.0".to_string()),
1584 version_range: Some(Range {
1585 start: Position {
1586 line: 0,
1587 character: 10,
1588 },
1589 end: Position {
1590 line: 0,
1591 character: 20,
1592 },
1593 }),
1594 name_range: Range::default(),
1595 }];
1596
1597 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1598 let selected_range = Range {
1599 start: Position {
1600 line: 0,
1601 character: 15,
1602 },
1603 end: Position {
1604 line: 0,
1605 character: 15,
1606 },
1607 };
1608
1609 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1610
1611 assert_eq!(actions.len(), 0);
1612
1613 for action in actions {
1614 if let CodeActionOrCommand::CodeAction(a) = action {
1615 assert!(!a.title.contains("1.0.0"));
1616 }
1617 }
1618 }
1619
1620 #[test]
1621 fn test_ranges_overlap_basic() {
1622 let range_a = Range {
1623 start: Position {
1624 line: 0,
1625 character: 10,
1626 },
1627 end: Position {
1628 line: 0,
1629 character: 20,
1630 },
1631 };
1632
1633 let range_b = Range {
1634 start: Position {
1635 line: 0,
1636 character: 15,
1637 },
1638 end: Position {
1639 line: 0,
1640 character: 25,
1641 },
1642 };
1643
1644 assert!(ranges_overlap(range_a, range_b));
1645 }
1646
1647 #[test]
1648 fn test_ranges_no_overlap() {
1649 let range_a = Range {
1650 start: Position {
1651 line: 0,
1652 character: 10,
1653 },
1654 end: Position {
1655 line: 0,
1656 character: 20,
1657 },
1658 };
1659
1660 let range_b = Range {
1661 start: Position {
1662 line: 0,
1663 character: 25,
1664 },
1665 end: Position {
1666 line: 0,
1667 character: 30,
1668 },
1669 };
1670
1671 assert!(!ranges_overlap(range_a, range_b));
1672 }
1673
1674 #[tokio::test]
1675 async fn test_generate_diagnostics_valid_version() {
1676 let cache = Arc::new(HttpCache::new());
1677 let handler = MockHandler::new(cache);
1678
1679 let deps = vec![MockDependency {
1680 name: "serde".to_string(),
1681 version_req: Some("1.0.195".to_string()),
1682 version_range: Some(Range {
1683 start: Position {
1684 line: 0,
1685 character: 10,
1686 },
1687 end: Position {
1688 line: 0,
1689 character: 20,
1690 },
1691 }),
1692 name_range: Range::default(),
1693 }];
1694
1695 let config = DiagnosticsConfig::default();
1696 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1697
1698 assert_eq!(diagnostics.len(), 0);
1699 }
1700
1701 #[tokio::test]
1702 async fn test_generate_diagnostics_deprecated_version() {
1703 use tower_lsp::lsp_types::DiagnosticSeverity;
1704
1705 let cache = Arc::new(HttpCache::new());
1706 let handler = MockHandler::new(cache);
1707
1708 let deps = vec![MockDependency {
1709 name: "yanked-pkg".to_string(),
1710 version_req: Some("1.0.0".to_string()),
1711 version_range: Some(Range {
1712 start: Position {
1713 line: 0,
1714 character: 10,
1715 },
1716 end: Position {
1717 line: 0,
1718 character: 20,
1719 },
1720 }),
1721 name_range: Range::default(),
1722 }];
1723
1724 let config = DiagnosticsConfig::default();
1725 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1726
1727 assert_eq!(diagnostics.len(), 1);
1728 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1729 assert!(diagnostics[0].message.contains("yanked"));
1730 }
1731
1732 #[tokio::test]
1733 async fn test_generate_diagnostics_unknown_package() {
1734 use tower_lsp::lsp_types::DiagnosticSeverity;
1735
1736 let cache = Arc::new(HttpCache::new());
1737 let handler = MockHandler::new(cache);
1738
1739 let deps = vec![MockDependency {
1740 name: "nonexistent".to_string(),
1741 version_req: Some("1.0.0".to_string()),
1742 version_range: Some(Range {
1743 start: Position {
1744 line: 0,
1745 character: 10,
1746 },
1747 end: Position {
1748 line: 0,
1749 character: 20,
1750 },
1751 }),
1752 name_range: Range {
1753 start: Position {
1754 line: 0,
1755 character: 0,
1756 },
1757 end: Position {
1758 line: 0,
1759 character: 10,
1760 },
1761 },
1762 }];
1763
1764 let config = DiagnosticsConfig::default();
1765 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1766
1767 assert_eq!(diagnostics.len(), 1);
1768 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1769 assert!(diagnostics[0].message.contains("Unknown package"));
1770 assert!(diagnostics[0].message.contains("nonexistent"));
1771 }
1772
1773 #[tokio::test]
1774 async fn test_generate_diagnostics_missing_version() {
1775 let cache = Arc::new(HttpCache::new());
1776 let handler = MockHandler::new(cache);
1777
1778 let deps = vec![MockDependency {
1779 name: "serde".to_string(),
1780 version_req: None,
1781 version_range: None,
1782 name_range: Range::default(),
1783 }];
1784
1785 let config = DiagnosticsConfig::default();
1786 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1787
1788 assert_eq!(diagnostics.len(), 0);
1789 }
1790
1791 #[tokio::test]
1792 async fn test_generate_diagnostics_outdated_version() {
1793 use tower_lsp::lsp_types::DiagnosticSeverity;
1794
1795 let cache = Arc::new(HttpCache::new());
1796 let mut handler = MockHandler::new(cache);
1797
1798 handler.registry.versions.insert(
1799 "outdated-pkg".to_string(),
1800 vec![
1801 MockVersion {
1802 version: "2.0.0".to_string(),
1803 yanked: false,
1804 features: vec![],
1805 },
1806 MockVersion {
1807 version: "1.0.0".to_string(),
1808 yanked: false,
1809 features: vec![],
1810 },
1811 ],
1812 );
1813
1814 let deps = vec![MockDependency {
1815 name: "outdated-pkg".to_string(),
1816 version_req: Some("1.0.0".to_string()),
1817 version_range: Some(Range {
1818 start: Position {
1819 line: 0,
1820 character: 10,
1821 },
1822 end: Position {
1823 line: 0,
1824 character: 20,
1825 },
1826 }),
1827 name_range: Range::default(),
1828 }];
1829
1830 let config = DiagnosticsConfig::default();
1831 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1832
1833 assert_eq!(diagnostics.len(), 1);
1834 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1835 assert!(diagnostics[0].message.contains("Newer version available"));
1836 assert!(diagnostics[0].message.contains("2.0.0"));
1837 }
1838
1839 #[test]
1840 fn test_diagnostics_config_default() {
1841 use tower_lsp::lsp_types::DiagnosticSeverity;
1842
1843 let config = DiagnosticsConfig::default();
1844 assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1845 assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1846 assert_eq!(config.outdated_severity, DiagnosticSeverity::HINT);
1847 }
1848}