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
764 .iter()
765 .find(|v| !H::is_deprecated(v) && !v.is_prerelease());
766 if let (Some(latest), Some(current)) = (latest, &matching)
767 && latest.version_string() != current.version_string()
768 {
769 diagnostics.push(Diagnostic {
770 range: version_range,
771 severity: Some(config.outdated_severity),
772 message: format!("Newer version available: {}", latest.version_string()),
773 source: Some("deps-lsp".into()),
774 ..Default::default()
775 });
776 }
777 }
778 }
779
780 diagnostics
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786 use crate::registry::PackageMetadata;
787 use tower_lsp::lsp_types::{Position, Range};
788
789 #[derive(Clone)]
790 struct MockVersion {
791 version: String,
792 yanked: bool,
793 features: Vec<String>,
794 }
795
796 impl VersionInfo for MockVersion {
797 fn version_string(&self) -> &str {
798 &self.version
799 }
800
801 fn is_yanked(&self) -> bool {
802 self.yanked
803 }
804
805 fn features(&self) -> Vec<String> {
806 self.features.clone()
807 }
808 }
809
810 #[derive(Clone)]
811 struct MockMetadata {
812 name: String,
813 description: Option<String>,
814 latest: String,
815 }
816
817 impl PackageMetadata for MockMetadata {
818 fn name(&self) -> &str {
819 &self.name
820 }
821
822 fn description(&self) -> Option<&str> {
823 self.description.as_deref()
824 }
825
826 fn repository(&self) -> Option<&str> {
827 None
828 }
829
830 fn documentation(&self) -> Option<&str> {
831 None
832 }
833
834 fn latest_version(&self) -> &str {
835 &self.latest
836 }
837 }
838
839 #[derive(Clone)]
840 struct MockDependency {
841 name: String,
842 version_req: Option<String>,
843 version_range: Option<Range>,
844 name_range: Range,
845 }
846
847 impl crate::parser::DependencyInfo for MockDependency {
848 fn name(&self) -> &str {
849 &self.name
850 }
851
852 fn name_range(&self) -> Range {
853 self.name_range
854 }
855
856 fn version_requirement(&self) -> Option<&str> {
857 self.version_req.as_deref()
858 }
859
860 fn version_range(&self) -> Option<Range> {
861 self.version_range
862 }
863
864 fn source(&self) -> crate::parser::DependencySource {
865 crate::parser::DependencySource::Registry
866 }
867 }
868
869 struct MockRegistry {
870 versions: std::collections::HashMap<String, Vec<MockVersion>>,
871 }
872
873 impl Clone for MockRegistry {
874 fn clone(&self) -> Self {
875 Self {
876 versions: self.versions.clone(),
877 }
878 }
879 }
880
881 #[async_trait]
882 impl crate::registry::PackageRegistry for MockRegistry {
883 type Version = MockVersion;
884 type Metadata = MockMetadata;
885 type VersionReq = String;
886
887 async fn get_versions(&self, name: &str) -> crate::error::Result<Vec<Self::Version>> {
888 self.versions.get(name).cloned().ok_or_else(|| {
889 use std::io::{Error as IoError, ErrorKind};
890 crate::DepsError::Io(IoError::new(ErrorKind::NotFound, "package not found"))
891 })
892 }
893
894 async fn get_latest_matching(
895 &self,
896 name: &str,
897 req: &Self::VersionReq,
898 ) -> crate::error::Result<Option<Self::Version>> {
899 Ok(self
900 .versions
901 .get(name)
902 .and_then(|versions| versions.iter().find(|v| v.version == *req).cloned()))
903 }
904
905 async fn search(
906 &self,
907 _query: &str,
908 _limit: usize,
909 ) -> crate::error::Result<Vec<Self::Metadata>> {
910 Ok(vec![])
911 }
912 }
913
914 struct MockHandler {
915 registry: MockRegistry,
916 }
917
918 #[async_trait]
919 impl EcosystemHandler for MockHandler {
920 type Registry = MockRegistry;
921 type Dependency = MockDependency;
922 type UnifiedDep = MockDependency;
923
924 fn new(_cache: Arc<HttpCache>) -> Self {
925 let mut versions = std::collections::HashMap::new();
926 versions.insert(
927 "serde".to_string(),
928 vec![
929 MockVersion {
930 version: "1.0.195".to_string(),
931 yanked: false,
932 features: vec!["derive".to_string(), "alloc".to_string()],
933 },
934 MockVersion {
935 version: "1.0.194".to_string(),
936 yanked: false,
937 features: vec![],
938 },
939 ],
940 );
941 versions.insert(
942 "yanked-pkg".to_string(),
943 vec![MockVersion {
944 version: "1.0.0".to_string(),
945 yanked: true,
946 features: vec![],
947 }],
948 );
949
950 Self {
951 registry: MockRegistry { versions },
952 }
953 }
954
955 fn registry(&self) -> &Self::Registry {
956 &self.registry
957 }
958
959 fn extract_dependency(dep: &Self::UnifiedDep) -> Option<&Self::Dependency> {
960 Some(dep)
961 }
962
963 fn package_url(name: &str) -> String {
964 format!("https://test.io/pkg/{}", name)
965 }
966
967 fn ecosystem_display_name() -> &'static str {
968 "Test Registry"
969 }
970
971 fn is_version_latest(version_req: &str, latest: &str) -> bool {
972 version_req == latest
973 }
974
975 fn format_version_for_edit(_dep: &Self::Dependency, version: &str) -> String {
976 format!("\"{}\"", version)
977 }
978
979 fn is_deprecated(version: &MockVersion) -> bool {
980 version.yanked
981 }
982
983 fn is_valid_version_syntax(_version_req: &str) -> bool {
984 true
985 }
986
987 fn parse_version_req(version_req: &str) -> Option<String> {
988 Some(version_req.to_string())
989 }
990 }
991
992 impl VersionStringGetter for MockVersion {
993 fn version_string(&self) -> &str {
994 &self.version
995 }
996 }
997
998 impl YankedChecker for MockVersion {
999 fn is_yanked(&self) -> bool {
1000 self.yanked
1001 }
1002 }
1003
1004 #[test]
1005 fn test_inlay_hints_config_default() {
1006 let config = InlayHintsConfig::default();
1007 assert!(config.enabled);
1008 assert_eq!(config.up_to_date_text, "✅");
1009 assert_eq!(config.needs_update_text, "❌ {}");
1010 }
1011
1012 #[tokio::test]
1013 async fn test_generate_inlay_hints_cached() {
1014 let cache = Arc::new(HttpCache::new());
1015 let handler = MockHandler::new(cache);
1016
1017 let deps = vec![MockDependency {
1018 name: "serde".to_string(),
1019 version_req: Some("1.0.195".to_string()),
1020 version_range: Some(Range {
1021 start: Position {
1022 line: 0,
1023 character: 10,
1024 },
1025 end: Position {
1026 line: 0,
1027 character: 20,
1028 },
1029 }),
1030 name_range: Range::default(),
1031 }];
1032
1033 let mut cached_versions = HashMap::new();
1034 cached_versions.insert(
1035 "serde".to_string(),
1036 MockVersion {
1037 version: "1.0.195".to_string(),
1038 yanked: false,
1039 features: vec![],
1040 },
1041 );
1042
1043 let config = InlayHintsConfig::default();
1044 let resolved_versions: HashMap<String, String> = HashMap::new();
1045 let hints = generate_inlay_hints(
1046 &handler,
1047 &deps,
1048 &cached_versions,
1049 &resolved_versions,
1050 &config,
1051 )
1052 .await;
1053
1054 assert_eq!(hints.len(), 1);
1055 assert_eq!(hints[0].position.line, 0);
1056 assert_eq!(hints[0].position.character, 20);
1057 }
1058
1059 #[tokio::test]
1060 async fn test_generate_inlay_hints_fetch() {
1061 let cache = Arc::new(HttpCache::new());
1062 let handler = MockHandler::new(cache);
1063
1064 let deps = vec![MockDependency {
1065 name: "serde".to_string(),
1066 version_req: Some("1.0.0".to_string()),
1067 version_range: Some(Range {
1068 start: Position {
1069 line: 0,
1070 character: 10,
1071 },
1072 end: Position {
1073 line: 0,
1074 character: 20,
1075 },
1076 }),
1077 name_range: Range::default(),
1078 }];
1079
1080 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1081 let config = InlayHintsConfig::default();
1082 let resolved_versions: HashMap<String, String> = HashMap::new();
1083 let hints = generate_inlay_hints(
1084 &handler,
1085 &deps,
1086 &cached_versions,
1087 &resolved_versions,
1088 &config,
1089 )
1090 .await;
1091
1092 assert_eq!(hints.len(), 1);
1093 }
1094
1095 #[tokio::test]
1096 async fn test_generate_inlay_hints_skips_yanked() {
1097 let cache = Arc::new(HttpCache::new());
1098 let handler = MockHandler::new(cache);
1099
1100 let deps = vec![MockDependency {
1101 name: "serde".to_string(),
1102 version_req: Some("1.0.195".to_string()),
1103 version_range: Some(Range {
1104 start: Position {
1105 line: 0,
1106 character: 10,
1107 },
1108 end: Position {
1109 line: 0,
1110 character: 20,
1111 },
1112 }),
1113 name_range: Range::default(),
1114 }];
1115
1116 let mut cached_versions = HashMap::new();
1117 cached_versions.insert(
1118 "serde".to_string(),
1119 MockVersion {
1120 version: "1.0.195".to_string(),
1121 yanked: true,
1122 features: vec![],
1123 },
1124 );
1125
1126 let config = InlayHintsConfig::default();
1127 let resolved_versions: HashMap<String, String> = HashMap::new();
1128 let hints = generate_inlay_hints(
1129 &handler,
1130 &deps,
1131 &cached_versions,
1132 &resolved_versions,
1133 &config,
1134 )
1135 .await;
1136
1137 assert_eq!(hints.len(), 0);
1138 }
1139
1140 #[tokio::test]
1141 async fn test_generate_inlay_hints_no_version_range() {
1142 let cache = Arc::new(HttpCache::new());
1143 let handler = MockHandler::new(cache);
1144
1145 let deps = vec![MockDependency {
1146 name: "serde".to_string(),
1147 version_req: Some("1.0.195".to_string()),
1148 version_range: None,
1149 name_range: Range::default(),
1150 }];
1151
1152 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1153 let config = InlayHintsConfig::default();
1154 let resolved_versions: HashMap<String, String> = HashMap::new();
1155 let hints = generate_inlay_hints(
1156 &handler,
1157 &deps,
1158 &cached_versions,
1159 &resolved_versions,
1160 &config,
1161 )
1162 .await;
1163
1164 assert_eq!(hints.len(), 0);
1165 }
1166
1167 #[tokio::test]
1168 async fn test_generate_inlay_hints_no_version_req() {
1169 let cache = Arc::new(HttpCache::new());
1170 let handler = MockHandler::new(cache);
1171
1172 let deps = vec![MockDependency {
1173 name: "serde".to_string(),
1174 version_req: None,
1175 version_range: Some(Range {
1176 start: Position {
1177 line: 0,
1178 character: 10,
1179 },
1180 end: Position {
1181 line: 0,
1182 character: 20,
1183 },
1184 }),
1185 name_range: Range::default(),
1186 }];
1187
1188 let cached_versions: HashMap<String, MockVersion> = HashMap::new();
1189 let config = InlayHintsConfig::default();
1190 let resolved_versions: HashMap<String, String> = HashMap::new();
1191 let hints = generate_inlay_hints(
1192 &handler,
1193 &deps,
1194 &cached_versions,
1195 &resolved_versions,
1196 &config,
1197 )
1198 .await;
1199
1200 assert_eq!(hints.len(), 0);
1201 }
1202
1203 #[test]
1204 fn test_create_hint_up_to_date() {
1205 let config = InlayHintsConfig::default();
1206 let range = Range {
1207 start: Position {
1208 line: 5,
1209 character: 10,
1210 },
1211 end: Position {
1212 line: 5,
1213 character: 20,
1214 },
1215 };
1216
1217 let hint = create_hint::<MockHandler>("serde", range, "1.0.195", true, &config);
1218
1219 assert_eq!(hint.position, range.end);
1220 if let InlayHintLabel::LabelParts(parts) = hint.label {
1221 assert_eq!(parts[0].value, "✅");
1222 } else {
1223 panic!("Expected LabelParts");
1224 }
1225 }
1226
1227 #[test]
1228 fn test_create_hint_needs_update() {
1229 let config = InlayHintsConfig::default();
1230 let range = Range {
1231 start: Position {
1232 line: 5,
1233 character: 10,
1234 },
1235 end: Position {
1236 line: 5,
1237 character: 20,
1238 },
1239 };
1240
1241 let hint = create_hint::<MockHandler>("serde", range, "1.0.200", false, &config);
1242
1243 assert_eq!(hint.position, range.end);
1244 if let InlayHintLabel::LabelParts(parts) = hint.label {
1245 assert_eq!(parts[0].value, "❌ 1.0.200");
1246 } else {
1247 panic!("Expected LabelParts");
1248 }
1249 }
1250
1251 #[test]
1252 fn test_create_hint_custom_config() {
1253 let config = InlayHintsConfig {
1254 enabled: true,
1255 up_to_date_text: "OK".to_string(),
1256 needs_update_text: "UPDATE: {}".to_string(),
1257 };
1258 let range = Range {
1259 start: Position {
1260 line: 0,
1261 character: 0,
1262 },
1263 end: Position {
1264 line: 0,
1265 character: 10,
1266 },
1267 };
1268
1269 let hint = create_hint::<MockHandler>("test", range, "2.0.0", false, &config);
1270
1271 if let InlayHintLabel::LabelParts(parts) = hint.label {
1272 assert_eq!(parts[0].value, "UPDATE: 2.0.0");
1273 } else {
1274 panic!("Expected LabelParts");
1275 }
1276 }
1277
1278 #[tokio::test]
1279 async fn test_generate_hover() {
1280 let cache = Arc::new(HttpCache::new());
1281 let handler = MockHandler::new(cache);
1282
1283 let dep = MockDependency {
1284 name: "serde".to_string(),
1285 version_req: Some("1.0.0".to_string()),
1286 version_range: Some(Range::default()),
1287 name_range: Range {
1288 start: Position {
1289 line: 0,
1290 character: 0,
1291 },
1292 end: Position {
1293 line: 0,
1294 character: 5,
1295 },
1296 },
1297 };
1298
1299 let hover = generate_hover(&handler, &dep, None).await;
1300
1301 assert!(hover.is_some());
1302 let hover = hover.unwrap();
1303
1304 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1305 assert!(content.value.contains("serde"));
1306 assert!(content.value.contains("1.0.195"));
1307 assert!(content.value.contains("Current"));
1308 assert!(content.value.contains("Features"));
1309 assert!(content.value.contains("derive"));
1310 } else {
1311 panic!("Expected Markup content");
1312 }
1313 }
1314
1315 #[tokio::test]
1316 async fn test_generate_hover_yanked_version() {
1317 let cache = Arc::new(HttpCache::new());
1318 let handler = MockHandler::new(cache);
1319
1320 let dep = MockDependency {
1321 name: "yanked-pkg".to_string(),
1322 version_req: Some("1.0.0".to_string()),
1323 version_range: Some(Range::default()),
1324 name_range: Range::default(),
1325 };
1326
1327 let hover = generate_hover(&handler, &dep, None).await;
1328
1329 assert!(hover.is_some());
1330 let hover = hover.unwrap();
1331
1332 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1333 assert!(content.value.contains("Warning"));
1334 assert!(content.value.contains("yanked"));
1335 } else {
1336 panic!("Expected Markup content");
1337 }
1338 }
1339
1340 #[tokio::test]
1341 async fn test_generate_hover_no_versions() {
1342 let cache = Arc::new(HttpCache::new());
1343 let handler = MockHandler::new(cache);
1344
1345 let dep = MockDependency {
1346 name: "nonexistent".to_string(),
1347 version_req: Some("1.0.0".to_string()),
1348 version_range: Some(Range::default()),
1349 name_range: Range::default(),
1350 };
1351
1352 let hover = generate_hover(&handler, &dep, None).await;
1353 assert!(hover.is_none());
1354 }
1355
1356 #[tokio::test]
1357 async fn test_generate_hover_no_version_req() {
1358 let cache = Arc::new(HttpCache::new());
1359 let handler = MockHandler::new(cache);
1360
1361 let dep = MockDependency {
1362 name: "serde".to_string(),
1363 version_req: None,
1364 version_range: Some(Range::default()),
1365 name_range: Range::default(),
1366 };
1367
1368 let hover = generate_hover(&handler, &dep, None).await;
1369
1370 assert!(hover.is_some());
1371 let hover = hover.unwrap();
1372
1373 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1374 assert!(!content.value.contains("Current"));
1375 } else {
1376 panic!("Expected Markup content");
1377 }
1378 }
1379
1380 #[tokio::test]
1381 async fn test_generate_hover_with_resolved_version() {
1382 let cache = Arc::new(HttpCache::new());
1383 let handler = MockHandler::new(cache);
1384
1385 let dep = MockDependency {
1386 name: "serde".to_string(),
1387 version_req: Some("1.0".to_string()), version_range: Some(Range::default()),
1389 name_range: Range {
1390 start: Position {
1391 line: 0,
1392 character: 0,
1393 },
1394 end: Position {
1395 line: 0,
1396 character: 5,
1397 },
1398 },
1399 };
1400
1401 let hover = generate_hover(&handler, &dep, Some("1.0.195")).await;
1403
1404 assert!(hover.is_some());
1405 let hover = hover.unwrap();
1406
1407 if let tower_lsp::lsp_types::HoverContents::Markup(content) = hover.contents {
1408 assert!(content.value.contains("**Current**: `1.0.195`"));
1410 assert!(!content.value.contains("**Current**: `1.0`"));
1411 } else {
1412 panic!("Expected Markup content");
1413 }
1414 }
1415
1416 #[tokio::test]
1417 async fn test_generate_code_actions_empty_when_up_to_date() {
1418 use tower_lsp::lsp_types::Url;
1419
1420 let cache = Arc::new(HttpCache::new());
1421 let handler = MockHandler::new(cache);
1422
1423 let deps = vec![MockDependency {
1424 name: "serde".to_string(),
1425 version_req: Some("1.0.195".to_string()),
1426 version_range: Some(Range {
1427 start: Position {
1428 line: 0,
1429 character: 10,
1430 },
1431 end: Position {
1432 line: 0,
1433 character: 20,
1434 },
1435 }),
1436 name_range: Range::default(),
1437 }];
1438
1439 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1440 let selected_range = Range {
1441 start: Position {
1442 line: 0,
1443 character: 15,
1444 },
1445 end: Position {
1446 line: 0,
1447 character: 15,
1448 },
1449 };
1450
1451 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1452
1453 assert!(!actions.is_empty());
1454 }
1455
1456 #[tokio::test]
1457 async fn test_generate_code_actions_update_outdated() {
1458 use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1459
1460 let cache = Arc::new(HttpCache::new());
1461 let handler = MockHandler::new(cache);
1462
1463 let deps = vec![MockDependency {
1464 name: "serde".to_string(),
1465 version_req: Some("1.0.0".to_string()),
1466 version_range: Some(Range {
1467 start: Position {
1468 line: 0,
1469 character: 10,
1470 },
1471 end: Position {
1472 line: 0,
1473 character: 20,
1474 },
1475 }),
1476 name_range: Range::default(),
1477 }];
1478
1479 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1480 let selected_range = Range {
1481 start: Position {
1482 line: 0,
1483 character: 15,
1484 },
1485 end: Position {
1486 line: 0,
1487 character: 15,
1488 },
1489 };
1490
1491 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1492
1493 assert!(!actions.is_empty());
1494 assert!(actions.len() <= 5);
1495
1496 if let CodeActionOrCommand::CodeAction(action) = &actions[0] {
1497 assert!(action.title.contains("1.0.195"));
1498 assert!(action.title.contains("latest"));
1499 assert_eq!(action.is_preferred, Some(true));
1500 } else {
1501 panic!("Expected CodeAction");
1502 }
1503 }
1504
1505 #[tokio::test]
1506 async fn test_generate_code_actions_missing_version_range() {
1507 use tower_lsp::lsp_types::Url;
1508
1509 let cache = Arc::new(HttpCache::new());
1510 let handler = MockHandler::new(cache);
1511
1512 let deps = vec![MockDependency {
1513 name: "serde".to_string(),
1514 version_req: Some("1.0.0".to_string()),
1515 version_range: None,
1516 name_range: Range::default(),
1517 }];
1518
1519 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1520 let selected_range = Range {
1521 start: Position {
1522 line: 0,
1523 character: 15,
1524 },
1525 end: Position {
1526 line: 0,
1527 character: 15,
1528 },
1529 };
1530
1531 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1532
1533 assert_eq!(actions.len(), 0);
1534 }
1535
1536 #[tokio::test]
1537 async fn test_generate_code_actions_no_overlap() {
1538 use tower_lsp::lsp_types::Url;
1539
1540 let cache = Arc::new(HttpCache::new());
1541 let handler = MockHandler::new(cache);
1542
1543 let deps = vec![MockDependency {
1544 name: "serde".to_string(),
1545 version_req: Some("1.0.0".to_string()),
1546 version_range: Some(Range {
1547 start: Position {
1548 line: 0,
1549 character: 10,
1550 },
1551 end: Position {
1552 line: 0,
1553 character: 20,
1554 },
1555 }),
1556 name_range: Range::default(),
1557 }];
1558
1559 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1560 let selected_range = Range {
1561 start: Position {
1562 line: 5,
1563 character: 0,
1564 },
1565 end: Position {
1566 line: 5,
1567 character: 10,
1568 },
1569 };
1570
1571 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1572
1573 assert_eq!(actions.len(), 0);
1574 }
1575
1576 #[tokio::test]
1577 async fn test_generate_code_actions_filters_deprecated() {
1578 use tower_lsp::lsp_types::{CodeActionOrCommand, Url};
1579
1580 let cache = Arc::new(HttpCache::new());
1581 let handler = MockHandler::new(cache);
1582
1583 let deps = vec![MockDependency {
1584 name: "yanked-pkg".to_string(),
1585 version_req: Some("1.0.0".to_string()),
1586 version_range: Some(Range {
1587 start: Position {
1588 line: 0,
1589 character: 10,
1590 },
1591 end: Position {
1592 line: 0,
1593 character: 20,
1594 },
1595 }),
1596 name_range: Range::default(),
1597 }];
1598
1599 let uri = Url::parse("file:///test/Cargo.toml").unwrap();
1600 let selected_range = Range {
1601 start: Position {
1602 line: 0,
1603 character: 15,
1604 },
1605 end: Position {
1606 line: 0,
1607 character: 15,
1608 },
1609 };
1610
1611 let actions = generate_code_actions(&handler, &deps, &uri, selected_range).await;
1612
1613 assert_eq!(actions.len(), 0);
1614
1615 for action in actions {
1616 if let CodeActionOrCommand::CodeAction(a) = action {
1617 assert!(!a.title.contains("1.0.0"));
1618 }
1619 }
1620 }
1621
1622 #[test]
1623 fn test_ranges_overlap_basic() {
1624 let range_a = Range {
1625 start: Position {
1626 line: 0,
1627 character: 10,
1628 },
1629 end: Position {
1630 line: 0,
1631 character: 20,
1632 },
1633 };
1634
1635 let range_b = Range {
1636 start: Position {
1637 line: 0,
1638 character: 15,
1639 },
1640 end: Position {
1641 line: 0,
1642 character: 25,
1643 },
1644 };
1645
1646 assert!(ranges_overlap(range_a, range_b));
1647 }
1648
1649 #[test]
1650 fn test_ranges_no_overlap() {
1651 let range_a = Range {
1652 start: Position {
1653 line: 0,
1654 character: 10,
1655 },
1656 end: Position {
1657 line: 0,
1658 character: 20,
1659 },
1660 };
1661
1662 let range_b = Range {
1663 start: Position {
1664 line: 0,
1665 character: 25,
1666 },
1667 end: Position {
1668 line: 0,
1669 character: 30,
1670 },
1671 };
1672
1673 assert!(!ranges_overlap(range_a, range_b));
1674 }
1675
1676 #[tokio::test]
1677 async fn test_generate_diagnostics_valid_version() {
1678 let cache = Arc::new(HttpCache::new());
1679 let handler = MockHandler::new(cache);
1680
1681 let deps = vec![MockDependency {
1682 name: "serde".to_string(),
1683 version_req: Some("1.0.195".to_string()),
1684 version_range: Some(Range {
1685 start: Position {
1686 line: 0,
1687 character: 10,
1688 },
1689 end: Position {
1690 line: 0,
1691 character: 20,
1692 },
1693 }),
1694 name_range: Range::default(),
1695 }];
1696
1697 let config = DiagnosticsConfig::default();
1698 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1699
1700 assert_eq!(diagnostics.len(), 0);
1701 }
1702
1703 #[tokio::test]
1704 async fn test_generate_diagnostics_deprecated_version() {
1705 use tower_lsp::lsp_types::DiagnosticSeverity;
1706
1707 let cache = Arc::new(HttpCache::new());
1708 let handler = MockHandler::new(cache);
1709
1710 let deps = vec![MockDependency {
1711 name: "yanked-pkg".to_string(),
1712 version_req: Some("1.0.0".to_string()),
1713 version_range: Some(Range {
1714 start: Position {
1715 line: 0,
1716 character: 10,
1717 },
1718 end: Position {
1719 line: 0,
1720 character: 20,
1721 },
1722 }),
1723 name_range: Range::default(),
1724 }];
1725
1726 let config = DiagnosticsConfig::default();
1727 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1728
1729 assert_eq!(diagnostics.len(), 1);
1730 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1731 assert!(diagnostics[0].message.contains("yanked"));
1732 }
1733
1734 #[tokio::test]
1735 async fn test_generate_diagnostics_unknown_package() {
1736 use tower_lsp::lsp_types::DiagnosticSeverity;
1737
1738 let cache = Arc::new(HttpCache::new());
1739 let handler = MockHandler::new(cache);
1740
1741 let deps = vec![MockDependency {
1742 name: "nonexistent".to_string(),
1743 version_req: Some("1.0.0".to_string()),
1744 version_range: Some(Range {
1745 start: Position {
1746 line: 0,
1747 character: 10,
1748 },
1749 end: Position {
1750 line: 0,
1751 character: 20,
1752 },
1753 }),
1754 name_range: Range {
1755 start: Position {
1756 line: 0,
1757 character: 0,
1758 },
1759 end: Position {
1760 line: 0,
1761 character: 10,
1762 },
1763 },
1764 }];
1765
1766 let config = DiagnosticsConfig::default();
1767 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1768
1769 assert_eq!(diagnostics.len(), 1);
1770 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
1771 assert!(diagnostics[0].message.contains("Unknown package"));
1772 assert!(diagnostics[0].message.contains("nonexistent"));
1773 }
1774
1775 #[tokio::test]
1776 async fn test_generate_diagnostics_missing_version() {
1777 let cache = Arc::new(HttpCache::new());
1778 let handler = MockHandler::new(cache);
1779
1780 let deps = vec![MockDependency {
1781 name: "serde".to_string(),
1782 version_req: None,
1783 version_range: None,
1784 name_range: Range::default(),
1785 }];
1786
1787 let config = DiagnosticsConfig::default();
1788 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1789
1790 assert_eq!(diagnostics.len(), 0);
1791 }
1792
1793 #[tokio::test]
1794 async fn test_generate_diagnostics_outdated_version() {
1795 use tower_lsp::lsp_types::DiagnosticSeverity;
1796
1797 let cache = Arc::new(HttpCache::new());
1798 let mut handler = MockHandler::new(cache);
1799
1800 handler.registry.versions.insert(
1801 "outdated-pkg".to_string(),
1802 vec![
1803 MockVersion {
1804 version: "2.0.0".to_string(),
1805 yanked: false,
1806 features: vec![],
1807 },
1808 MockVersion {
1809 version: "1.0.0".to_string(),
1810 yanked: false,
1811 features: vec![],
1812 },
1813 ],
1814 );
1815
1816 let deps = vec![MockDependency {
1817 name: "outdated-pkg".to_string(),
1818 version_req: Some("1.0.0".to_string()),
1819 version_range: Some(Range {
1820 start: Position {
1821 line: 0,
1822 character: 10,
1823 },
1824 end: Position {
1825 line: 0,
1826 character: 20,
1827 },
1828 }),
1829 name_range: Range::default(),
1830 }];
1831
1832 let config = DiagnosticsConfig::default();
1833 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1834
1835 assert_eq!(diagnostics.len(), 1);
1836 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::HINT));
1837 assert!(diagnostics[0].message.contains("Newer version available"));
1838 assert!(diagnostics[0].message.contains("2.0.0"));
1839 }
1840
1841 #[test]
1842 fn test_diagnostics_config_default() {
1843 use tower_lsp::lsp_types::DiagnosticSeverity;
1844
1845 let config = DiagnosticsConfig::default();
1846 assert_eq!(config.unknown_severity, DiagnosticSeverity::WARNING);
1847 assert_eq!(config.yanked_severity, DiagnosticSeverity::WARNING);
1848 assert_eq!(config.outdated_severity, DiagnosticSeverity::HINT);
1849 }
1850
1851 #[tokio::test]
1852 async fn test_generate_diagnostics_ignores_prerelease() {
1853 let cache = Arc::new(HttpCache::new());
1854 let mut handler = MockHandler::new(cache);
1855
1856 handler.registry.versions.insert(
1857 "test-pkg".to_string(),
1858 vec![
1859 MockVersion {
1860 version: "4.0.0-alpha.13".to_string(),
1861 yanked: false,
1862 features: vec![],
1863 },
1864 MockVersion {
1865 version: "3.7.4".to_string(),
1866 yanked: false,
1867 features: vec![],
1868 },
1869 MockVersion {
1870 version: "3.4.2".to_string(),
1871 yanked: false,
1872 features: vec![],
1873 },
1874 ],
1875 );
1876
1877 let deps = vec![MockDependency {
1878 name: "test-pkg".to_string(),
1879 version_req: Some("3.4.2".to_string()),
1880 version_range: Some(Range {
1881 start: Position {
1882 line: 0,
1883 character: 10,
1884 },
1885 end: Position {
1886 line: 0,
1887 character: 20,
1888 },
1889 }),
1890 name_range: Range::default(),
1891 }];
1892
1893 let config = DiagnosticsConfig::default();
1894 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1895
1896 assert_eq!(diagnostics.len(), 1);
1897 assert!(diagnostics[0].message.contains("Newer version available"));
1898 assert!(diagnostics[0].message.contains("3.7.4"));
1899 assert!(!diagnostics[0].message.contains("4.0.0-alpha"));
1900 }
1901
1902 #[tokio::test]
1903 async fn test_generate_diagnostics_no_warning_when_latest_is_prerelease() {
1904 let cache = Arc::new(HttpCache::new());
1905 let mut handler = MockHandler::new(cache);
1906
1907 handler.registry.versions.insert(
1908 "prerelease-pkg".to_string(),
1909 vec![
1910 MockVersion {
1911 version: "2.0.0-beta.1".to_string(),
1912 yanked: false,
1913 features: vec![],
1914 },
1915 MockVersion {
1916 version: "1.5.0".to_string(),
1917 yanked: false,
1918 features: vec![],
1919 },
1920 ],
1921 );
1922
1923 let deps = vec![MockDependency {
1924 name: "prerelease-pkg".to_string(),
1925 version_req: Some("1.5.0".to_string()),
1926 version_range: Some(Range {
1927 start: Position {
1928 line: 0,
1929 character: 10,
1930 },
1931 end: Position {
1932 line: 0,
1933 character: 20,
1934 },
1935 }),
1936 name_range: Range::default(),
1937 }];
1938
1939 let config = DiagnosticsConfig::default();
1940 let diagnostics = generate_diagnostics(&handler, &deps, &config).await;
1941
1942 assert_eq!(diagnostics.len(), 0);
1943 }
1944}