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