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