1use crate::{Metadata, ParseResult, Version};
44use tower_lsp_server::ls_types::{
45 CompletionItem, CompletionItemKind, CompletionItemTag, CompletionTextEdit, Documentation,
46 MarkupContent, MarkupKind, Position, Range, TextEdit,
47};
48
49#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum CompletionContext {
55 PackageName {
59 prefix: String,
61 },
62
63 Version {
67 package_name: String,
69 prefix: String,
71 },
72
73 Feature {
77 package_name: String,
79 prefix: String,
81 },
82
83 None,
85}
86
87pub fn detect_completion_context(
116 parse_result: &dyn ParseResult,
117 position: Position,
118 content: &str,
119) -> CompletionContext {
120 let dependencies = parse_result.dependencies();
121
122 for dep in dependencies {
123 let name_range = dep.name_range();
125 if position_in_range(position, name_range) {
126 let prefix = extract_prefix(content, position, name_range);
127 return CompletionContext::PackageName { prefix };
128 }
129
130 if let Some(version_range) = dep.version_range()
132 && position_in_range(position, version_range)
133 {
134 let prefix = extract_prefix(content, position, version_range);
135 return CompletionContext::Version {
136 package_name: dep.name().to_string(),
137 prefix,
138 };
139 }
140
141 }
143
144 CompletionContext::None
145}
146
147fn position_in_range(position: Position, range: Range) -> bool {
153 if position.line < range.start.line {
155 return false;
156 }
157
158 if position.line == range.start.line && position.character < range.start.character {
159 return false;
160 }
161
162 if position.line > range.end.line {
164 return false;
165 }
166
167 if position.line == range.end.line && position.character > range.end.character + 1 {
168 return false;
169 }
170
171 true
172}
173
174pub fn utf16_to_byte_offset(s: &str, utf16_offset: u32) -> Option<usize> {
205 let mut utf16_count = 0u32;
206 for (byte_idx, ch) in s.char_indices() {
207 if utf16_count >= utf16_offset {
208 return Some(byte_idx);
209 }
210 utf16_count += ch.len_utf16() as u32;
211 }
212 if utf16_count == utf16_offset {
213 return Some(s.len());
214 }
215 None
216}
217
218pub fn extract_prefix(content: &str, position: Position, range: Range) -> String {
250 let line = match content.lines().nth(position.line as usize) {
252 Some(l) => l,
253 None => return String::new(),
254 };
255
256 let start_byte = if position.line == range.start.line {
258 match utf16_to_byte_offset(line, range.start.character) {
259 Some(offset) => offset,
260 None => return String::new(),
261 }
262 } else {
263 0
264 };
265
266 let cursor_byte = match utf16_to_byte_offset(line, position.character) {
267 Some(offset) => offset,
268 None => return String::new(),
269 };
270
271 if start_byte > line.len() || cursor_byte > line.len() || start_byte > cursor_byte {
273 return String::new();
274 }
275
276 let prefix = &line[start_byte..cursor_byte];
278
279 prefix
281 .trim()
282 .trim_matches('"')
283 .trim_matches('\'')
284 .trim()
285 .to_string()
286}
287
288pub fn build_package_completion(metadata: &dyn Metadata, insert_range: Range) -> CompletionItem {
315 let name = metadata.name();
316 let latest = metadata.latest_version();
317
318 let mut doc_parts = vec![format!("**{}** v{}", name, latest)];
320
321 if let Some(desc) = metadata.description() {
322 doc_parts.push(String::new()); let truncated = if desc.len() > 200 {
324 let mut end = 200;
325 while end > 0 && !desc.is_char_boundary(end) {
326 end -= 1;
327 }
328 format!("{}...", &desc[..end])
329 } else {
330 desc.to_string()
331 };
332 doc_parts.push(truncated);
333 }
334
335 let mut links = Vec::new();
337 if let Some(repo) = metadata.repository() {
338 links.push(format!("[Repository]({})", repo));
339 }
340 if let Some(docs) = metadata.documentation() {
341 links.push(format!("[Documentation]({})", docs));
342 }
343
344 if !links.is_empty() {
345 doc_parts.push(String::new()); doc_parts.push(links.join(" | "));
347 }
348
349 CompletionItem {
350 label: name.to_string(),
351 kind: Some(CompletionItemKind::MODULE),
352 detail: Some(format!("v{}", latest)),
353 documentation: Some(Documentation::MarkupContent(MarkupContent {
354 kind: MarkupKind::Markdown,
355 value: doc_parts.join("\n"),
356 })),
357 insert_text: Some(name.to_string()),
358 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
359 range: insert_range,
360 new_text: name.to_string(),
361 })),
362 sort_text: Some(name.to_string()),
363 filter_text: Some(name.to_string()),
364 ..Default::default()
365 }
366}
367
368pub fn build_version_completion(
403 version: &dyn Version,
404 package_name: &str,
405 insert_range: Range,
406) -> CompletionItem {
407 let version_str = version.version_string();
408
409 let mut detail_parts = vec![format!("v{}", version_str)];
411
412 if version.is_yanked() {
413 detail_parts.push("(yanked)".to_string());
414 }
415
416 if version.is_prerelease() {
417 detail_parts.push("(pre-release)".to_string());
418 }
419
420 let detail = detail_parts.join(" ");
421
422 let tags = if version.is_yanked() {
424 Some(vec![CompletionItemTag::DEPRECATED])
425 } else {
426 None
427 };
428
429 let sort_prefix = if version.is_yanked() {
432 "3_"
433 } else if version.is_prerelease() {
434 "2_"
435 } else {
436 "1_"
437 };
438
439 let sort_text = format!("{}{}", sort_prefix, version_str);
440
441 CompletionItem {
442 label: version_str.to_string(),
443 kind: Some(CompletionItemKind::VALUE),
444 detail: Some(detail),
445 documentation: Some(Documentation::String(format!(
446 "Version {} of {}",
447 version_str, package_name
448 ))),
449 insert_text: Some(version_str.to_string()),
450 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
451 range: insert_range,
452 new_text: version_str.to_string(),
453 })),
454 sort_text: Some(sort_text),
455 deprecated: Some(version.is_yanked()),
456 tags,
457 ..Default::default()
458 }
459}
460
461pub fn build_feature_completion(
487 feature_name: &str,
488 package_name: &str,
489 insert_range: Range,
490) -> CompletionItem {
491 CompletionItem {
492 label: feature_name.to_string(),
493 kind: Some(CompletionItemKind::PROPERTY),
494 detail: Some(format!("Feature of {}", package_name)),
495 documentation: None,
496 insert_text: Some(feature_name.to_string()),
497 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
498 range: insert_range,
499 new_text: feature_name.to_string(),
500 })),
501 sort_text: Some(feature_name.to_string()),
502 ..Default::default()
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use std::any::Any;
510
511 struct MockDependency {
514 name: String,
515 name_range: Range,
516 version_range: Option<Range>,
517 }
518
519 impl crate::ecosystem::Dependency for MockDependency {
520 fn name(&self) -> &str {
521 &self.name
522 }
523
524 fn name_range(&self) -> Range {
525 self.name_range
526 }
527
528 fn version_requirement(&self) -> Option<&str> {
529 Some("1.0")
530 }
531
532 fn version_range(&self) -> Option<Range> {
533 self.version_range
534 }
535
536 fn source(&self) -> crate::parser::DependencySource {
537 crate::parser::DependencySource::Registry
538 }
539
540 fn as_any(&self) -> &dyn Any {
541 self
542 }
543 }
544
545 struct MockParseResult {
546 dependencies: Vec<MockDependency>,
547 }
548
549 impl ParseResult for MockParseResult {
550 fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
551 self.dependencies
552 .iter()
553 .map(|d| d as &dyn crate::ecosystem::Dependency)
554 .collect()
555 }
556
557 fn workspace_root(&self) -> Option<&std::path::Path> {
558 None
559 }
560
561 fn uri(&self) -> &tower_lsp_server::ls_types::Uri {
562 static URL_STR: &str = "file:///test/Cargo.toml";
564 static URL: once_cell::sync::Lazy<tower_lsp_server::ls_types::Uri> =
565 once_cell::sync::Lazy::new(|| URL_STR.parse().unwrap());
566 &URL
567 }
568
569 fn as_any(&self) -> &dyn Any {
570 self
571 }
572 }
573
574 struct MockVersion {
575 version: String,
576 yanked: bool,
577 prerelease: bool,
578 }
579
580 impl crate::registry::Version for MockVersion {
581 fn version_string(&self) -> &str {
582 &self.version
583 }
584
585 fn is_yanked(&self) -> bool {
586 self.yanked
587 }
588
589 fn is_prerelease(&self) -> bool {
590 self.prerelease
591 }
592
593 fn as_any(&self) -> &dyn Any {
594 self
595 }
596 }
597
598 struct MockMetadata {
599 name: String,
600 description: Option<String>,
601 repository: Option<String>,
602 documentation: Option<String>,
603 latest_version: String,
604 }
605
606 impl crate::registry::Metadata for MockMetadata {
607 fn name(&self) -> &str {
608 &self.name
609 }
610
611 fn description(&self) -> Option<&str> {
612 self.description.as_deref()
613 }
614
615 fn repository(&self) -> Option<&str> {
616 self.repository.as_deref()
617 }
618
619 fn documentation(&self) -> Option<&str> {
620 self.documentation.as_deref()
621 }
622
623 fn latest_version(&self) -> &str {
624 &self.latest_version
625 }
626
627 fn as_any(&self) -> &dyn Any {
628 self
629 }
630 }
631
632 #[test]
635 fn test_detect_package_name_context_at_start() {
636 let parse_result = MockParseResult {
637 dependencies: vec![MockDependency {
638 name: "serde".to_string(),
639 name_range: Range {
640 start: Position {
641 line: 0,
642 character: 0,
643 },
644 end: Position {
645 line: 0,
646 character: 5,
647 },
648 },
649 version_range: None,
650 }],
651 };
652
653 let content = "serde";
654 let position = Position {
655 line: 0,
656 character: 0,
657 };
658
659 let context = detect_completion_context(&parse_result, position, content);
660
661 match context {
662 CompletionContext::PackageName { prefix } => {
663 assert_eq!(prefix, "");
664 }
665 _ => panic!("Expected PackageName context, got {:?}", context),
666 }
667 }
668
669 #[test]
670 fn test_detect_package_name_context_partial() {
671 let parse_result = MockParseResult {
672 dependencies: vec![MockDependency {
673 name: "serde".to_string(),
674 name_range: Range {
675 start: Position {
676 line: 0,
677 character: 0,
678 },
679 end: Position {
680 line: 0,
681 character: 5,
682 },
683 },
684 version_range: None,
685 }],
686 };
687
688 let content = "serde";
689 let position = Position {
690 line: 0,
691 character: 3,
692 };
693
694 let context = detect_completion_context(&parse_result, position, content);
695
696 match context {
697 CompletionContext::PackageName { prefix } => {
698 assert_eq!(prefix, "ser");
699 }
700 _ => panic!("Expected PackageName context, got {:?}", context),
701 }
702 }
703
704 #[test]
705 fn test_detect_version_context() {
706 let parse_result = MockParseResult {
707 dependencies: vec![MockDependency {
708 name: "serde".to_string(),
709 name_range: Range {
710 start: Position {
711 line: 0,
712 character: 0,
713 },
714 end: Position {
715 line: 0,
716 character: 5,
717 },
718 },
719 version_range: Some(Range {
720 start: Position {
721 line: 0,
722 character: 9,
723 },
724 end: Position {
725 line: 0,
726 character: 14,
727 },
728 }),
729 }],
730 };
731
732 let content = r#"serde = "1.0.1""#;
733 let position = Position {
734 line: 0,
735 character: 11,
736 };
737
738 let context = detect_completion_context(&parse_result, position, content);
739
740 match context {
741 CompletionContext::Version {
742 package_name,
743 prefix,
744 } => {
745 assert_eq!(package_name, "serde");
746 assert_eq!(prefix, "1.");
747 }
748 _ => panic!("Expected Version context, got {:?}", context),
749 }
750 }
751
752 #[test]
753 fn test_detect_no_context_before_dependencies() {
754 let parse_result = MockParseResult {
755 dependencies: vec![MockDependency {
756 name: "serde".to_string(),
757 name_range: Range {
758 start: Position {
759 line: 5,
760 character: 0,
761 },
762 end: Position {
763 line: 5,
764 character: 5,
765 },
766 },
767 version_range: None,
768 }],
769 };
770
771 let content = "[dependencies]\nserde";
772 let position = Position {
773 line: 0,
774 character: 10,
775 };
776
777 let context = detect_completion_context(&parse_result, position, content);
778
779 assert_eq!(context, CompletionContext::None);
780 }
781
782 #[test]
783 fn test_detect_no_context_invalid_position() {
784 let parse_result = MockParseResult {
785 dependencies: vec![],
786 };
787
788 let content = "";
789 let position = Position {
790 line: 100,
791 character: 100,
792 };
793
794 let context = detect_completion_context(&parse_result, position, content);
795
796 assert_eq!(context, CompletionContext::None);
797 }
798
799 #[test]
802 fn test_extract_prefix_at_start() {
803 let content = "serde";
804 let position = Position {
805 line: 0,
806 character: 0,
807 };
808 let range = Range {
809 start: Position {
810 line: 0,
811 character: 0,
812 },
813 end: Position {
814 line: 0,
815 character: 5,
816 },
817 };
818
819 let prefix = extract_prefix(content, position, range);
820 assert_eq!(prefix, "");
821 }
822
823 #[test]
824 fn test_extract_prefix_partial() {
825 let content = "serde";
826 let position = Position {
827 line: 0,
828 character: 3,
829 };
830 let range = Range {
831 start: Position {
832 line: 0,
833 character: 0,
834 },
835 end: Position {
836 line: 0,
837 character: 5,
838 },
839 };
840
841 let prefix = extract_prefix(content, position, range);
842 assert_eq!(prefix, "ser");
843 }
844
845 #[test]
846 fn test_extract_prefix_with_quotes() {
847 let content = r#"serde = "1.0""#;
848 let position = Position {
849 line: 0,
850 character: 11,
851 };
852 let range = Range {
853 start: Position {
854 line: 0,
855 character: 9,
856 },
857 end: Position {
858 line: 0,
859 character: 13,
860 },
861 };
862
863 let prefix = extract_prefix(content, position, range);
864 assert_eq!(prefix, "1.");
865 }
866
867 #[test]
868 fn test_extract_prefix_empty() {
869 let content = r#"serde = """#;
870 let position = Position {
871 line: 0,
872 character: 9,
873 };
874 let range = Range {
875 start: Position {
876 line: 0,
877 character: 9,
878 },
879 end: Position {
880 line: 0,
881 character: 11,
882 },
883 };
884
885 let prefix = extract_prefix(content, position, range);
886 assert_eq!(prefix, "");
887 }
888
889 #[test]
890 fn test_extract_prefix_version_with_operator() {
891 let content = r#"serde = "^1.0""#;
892 let position = Position {
893 line: 0,
894 character: 12,
895 };
896 let range = Range {
897 start: Position {
898 line: 0,
899 character: 9,
900 },
901 end: Position {
902 line: 0,
903 character: 14,
904 },
905 };
906
907 let prefix = extract_prefix(content, position, range);
908 assert_eq!(prefix, "^1.");
909 }
910
911 #[test]
914 fn test_build_package_completion_full() {
915 let metadata = MockMetadata {
916 name: "serde".to_string(),
917 description: Some("Serialization framework".to_string()),
918 repository: Some("https://github.com/serde-rs/serde".to_string()),
919 documentation: Some("https://docs.rs/serde".to_string()),
920 latest_version: "1.0.214".to_string(),
921 };
922
923 let range = Range::default();
924 let item = build_package_completion(&metadata, range);
925
926 assert_eq!(item.label, "serde");
927 assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
928 assert_eq!(item.detail, Some("v1.0.214".to_string()));
929 assert!(matches!(
930 item.documentation,
931 Some(Documentation::MarkupContent(_))
932 ));
933
934 if let Some(Documentation::MarkupContent(content)) = item.documentation {
935 assert!(content.value.contains("**serde** v1.0.214"));
936 assert!(content.value.contains("Serialization framework"));
937 assert!(content.value.contains("Repository"));
938 assert!(content.value.contains("Documentation"));
939 }
940 }
941
942 #[test]
943 fn test_build_package_completion_minimal() {
944 let metadata = MockMetadata {
945 name: "test-pkg".to_string(),
946 description: None,
947 repository: None,
948 documentation: None,
949 latest_version: "0.1.0".to_string(),
950 };
951
952 let range = Range::default();
953 let item = build_package_completion(&metadata, range);
954
955 assert_eq!(item.label, "test-pkg");
956 assert_eq!(item.detail, Some("v0.1.0".to_string()));
957
958 if let Some(Documentation::MarkupContent(content)) = item.documentation {
959 assert!(content.value.contains("**test-pkg** v0.1.0"));
960 assert!(!content.value.contains("Repository"));
961 }
962 }
963
964 #[test]
965 fn test_build_version_completion_stable() {
966 let version = MockVersion {
967 version: "1.0.0".to_string(),
968 yanked: false,
969 prerelease: false,
970 };
971
972 let range = Range::default();
973 let item = build_version_completion(&version, "serde", range);
974
975 assert_eq!(item.label, "1.0.0");
976 assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
977 assert_eq!(item.detail, Some("v1.0.0".to_string()));
978 assert_eq!(item.deprecated, Some(false));
979 assert!(item.tags.is_none());
980 assert!(item.sort_text.as_ref().unwrap().starts_with("1_"));
981 }
982
983 #[test]
984 fn test_build_version_completion_yanked() {
985 let version = MockVersion {
986 version: "1.0.0".to_string(),
987 yanked: true,
988 prerelease: false,
989 };
990
991 let range = Range::default();
992 let item = build_version_completion(&version, "serde", range);
993
994 assert_eq!(item.detail, Some("v1.0.0 (yanked)".to_string()));
995 assert_eq!(item.deprecated, Some(true));
996 assert_eq!(item.tags, Some(vec![CompletionItemTag::DEPRECATED]));
997 assert!(item.sort_text.as_ref().unwrap().starts_with("3_"));
998 }
999
1000 #[test]
1001 fn test_build_version_completion_prerelease() {
1002 let version = MockVersion {
1003 version: "2.0.0-alpha.1".to_string(),
1004 yanked: false,
1005 prerelease: true,
1006 };
1007
1008 let range = Range::default();
1009 let item = build_version_completion(&version, "tokio", range);
1010
1011 assert_eq!(
1012 item.detail,
1013 Some("v2.0.0-alpha.1 (pre-release)".to_string())
1014 );
1015 assert_eq!(item.deprecated, Some(false));
1016 assert!(item.tags.is_none());
1017 assert!(item.sort_text.as_ref().unwrap().starts_with("2_"));
1018 }
1019
1020 #[test]
1021 fn test_build_version_completion_sort_order() {
1022 let stable = MockVersion {
1023 version: "1.0.0".to_string(),
1024 yanked: false,
1025 prerelease: false,
1026 };
1027 let prerelease = MockVersion {
1028 version: "2.0.0-beta".to_string(),
1029 yanked: false,
1030 prerelease: true,
1031 };
1032 let yanked = MockVersion {
1033 version: "0.9.0".to_string(),
1034 yanked: true,
1035 prerelease: false,
1036 };
1037
1038 let range = Range::default();
1039 let stable_item = build_version_completion(&stable, "test", range);
1040 let prerelease_item = build_version_completion(&prerelease, "test", range);
1041 let yanked_item = build_version_completion(&yanked, "test", range);
1042
1043 assert!(stable_item.sort_text.as_ref().unwrap().starts_with("1_"));
1045 assert!(
1047 prerelease_item
1048 .sort_text
1049 .as_ref()
1050 .unwrap()
1051 .starts_with("2_")
1052 );
1053 assert!(yanked_item.sort_text.as_ref().unwrap().starts_with("3_"));
1055 }
1056
1057 #[test]
1058 fn test_build_feature_completion() {
1059 let range = Range::default();
1060 let item = build_feature_completion("derive", "serde", range);
1061
1062 assert_eq!(item.label, "derive");
1063 assert_eq!(item.kind, Some(CompletionItemKind::PROPERTY));
1064 assert_eq!(item.detail, Some("Feature of serde".to_string()));
1065 assert!(item.documentation.is_none());
1066 assert_eq!(item.sort_text, Some("derive".to_string()));
1067 }
1068
1069 #[test]
1070 fn test_position_in_range_within() {
1071 let range = Range {
1072 start: Position {
1073 line: 0,
1074 character: 5,
1075 },
1076 end: Position {
1077 line: 0,
1078 character: 10,
1079 },
1080 };
1081
1082 let position = Position {
1083 line: 0,
1084 character: 7,
1085 };
1086
1087 assert!(position_in_range(position, range));
1088 }
1089
1090 #[test]
1091 fn test_position_in_range_at_start() {
1092 let range = Range {
1093 start: Position {
1094 line: 0,
1095 character: 5,
1096 },
1097 end: Position {
1098 line: 0,
1099 character: 10,
1100 },
1101 };
1102
1103 let position = Position {
1104 line: 0,
1105 character: 5,
1106 };
1107
1108 assert!(position_in_range(position, range));
1109 }
1110
1111 #[test]
1112 fn test_position_in_range_at_end() {
1113 let range = Range {
1114 start: Position {
1115 line: 0,
1116 character: 5,
1117 },
1118 end: Position {
1119 line: 0,
1120 character: 10,
1121 },
1122 };
1123
1124 let position = Position {
1125 line: 0,
1126 character: 10,
1127 };
1128
1129 assert!(position_in_range(position, range));
1130 }
1131
1132 #[test]
1133 fn test_position_in_range_one_past_end() {
1134 let range = Range {
1135 start: Position {
1136 line: 0,
1137 character: 5,
1138 },
1139 end: Position {
1140 line: 0,
1141 character: 10,
1142 },
1143 };
1144
1145 let position = Position {
1147 line: 0,
1148 character: 11,
1149 };
1150
1151 assert!(position_in_range(position, range));
1152 }
1153
1154 #[test]
1155 fn test_position_in_range_before() {
1156 let range = Range {
1157 start: Position {
1158 line: 0,
1159 character: 5,
1160 },
1161 end: Position {
1162 line: 0,
1163 character: 10,
1164 },
1165 };
1166
1167 let position = Position {
1168 line: 0,
1169 character: 4,
1170 };
1171
1172 assert!(!position_in_range(position, range));
1173 }
1174
1175 #[test]
1176 fn test_position_in_range_after() {
1177 let range = Range {
1178 start: Position {
1179 line: 0,
1180 character: 5,
1181 },
1182 end: Position {
1183 line: 0,
1184 character: 10,
1185 },
1186 };
1187
1188 let position = Position {
1189 line: 0,
1190 character: 12,
1191 };
1192
1193 assert!(!position_in_range(position, range));
1194 }
1195
1196 #[test]
1199 fn test_utf16_to_byte_offset_ascii() {
1200 let s = "hello";
1201 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1202 assert_eq!(utf16_to_byte_offset(s, 2), Some(2));
1203 assert_eq!(utf16_to_byte_offset(s, 5), Some(5));
1204 }
1205
1206 #[test]
1207 fn test_utf16_to_byte_offset_multibyte() {
1208 let s = "日本語";
1210 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1211 assert_eq!(utf16_to_byte_offset(s, 1), Some(3));
1212 assert_eq!(utf16_to_byte_offset(s, 2), Some(6));
1213 assert_eq!(utf16_to_byte_offset(s, 3), Some(9));
1214 }
1215
1216 #[test]
1217 fn test_utf16_to_byte_offset_emoji() {
1218 let s = "😀test";
1220 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1221 assert_eq!(utf16_to_byte_offset(s, 2), Some(4)); assert_eq!(utf16_to_byte_offset(s, 3), Some(5)); }
1224
1225 #[test]
1226 fn test_utf16_to_byte_offset_mixed() {
1227 let s = "hello 世界 😀!";
1229 assert_eq!(utf16_to_byte_offset(s, 0), Some(0)); assert_eq!(utf16_to_byte_offset(s, 6), Some(6)); assert_eq!(utf16_to_byte_offset(s, 7), Some(9)); assert_eq!(utf16_to_byte_offset(s, 9), Some(13)); assert_eq!(utf16_to_byte_offset(s, 11), Some(17)); }
1235
1236 #[test]
1237 fn test_utf16_to_byte_offset_out_of_bounds() {
1238 let s = "hello";
1239 assert_eq!(utf16_to_byte_offset(s, 100), None);
1240 }
1241
1242 #[test]
1243 fn test_utf16_to_byte_offset_empty() {
1244 let s = "";
1245 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
1246 assert_eq!(utf16_to_byte_offset(s, 1), None);
1247 }
1248
1249 #[test]
1252 fn test_build_package_completion_long_description_ascii() {
1253 let long_desc = "a".repeat(250);
1254 let metadata = MockMetadata {
1255 name: "test-pkg".to_string(),
1256 description: Some(long_desc),
1257 repository: None,
1258 documentation: None,
1259 latest_version: "1.0.0".to_string(),
1260 };
1261
1262 let range = Range::default();
1263 let item = build_package_completion(&metadata, range);
1264
1265 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1266 let lines: Vec<_> = content.value.lines().collect();
1268 assert!(lines[2].ends_with("..."));
1269 assert!(lines[2].len() <= 203); } else {
1271 panic!("Expected MarkupContent documentation");
1272 }
1273 }
1274
1275 #[test]
1276 fn test_build_package_completion_long_description_unicode() {
1277 let mut long_desc = String::new();
1280 for _ in 0..67 {
1281 long_desc.push('日');
1282 }
1283
1284 let metadata = MockMetadata {
1285 name: "test-pkg".to_string(),
1286 description: Some(long_desc),
1287 repository: None,
1288 documentation: None,
1289 latest_version: "1.0.0".to_string(),
1290 };
1291
1292 let range = Range::default();
1293 let item = build_package_completion(&metadata, range);
1294
1295 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1297 let lines: Vec<_> = content.value.lines().collect();
1298 assert!(lines[2].ends_with("..."));
1299 assert!(lines[2].is_char_boundary(lines[2].len()));
1301 } else {
1302 panic!("Expected MarkupContent documentation");
1303 }
1304 }
1305
1306 #[test]
1307 fn test_build_package_completion_long_description_emoji() {
1308 let long_desc = "😀".repeat(51);
1311
1312 let metadata = MockMetadata {
1313 name: "test-pkg".to_string(),
1314 description: Some(long_desc),
1315 repository: None,
1316 documentation: None,
1317 latest_version: "1.0.0".to_string(),
1318 };
1319
1320 let range = Range::default();
1321 let item = build_package_completion(&metadata, range);
1322
1323 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1325 let lines: Vec<_> = content.value.lines().collect();
1326 assert!(lines[2].ends_with("..."));
1327 assert!(lines[2].is_char_boundary(lines[2].len()));
1329 } else {
1330 panic!("Expected MarkupContent documentation");
1331 }
1332 }
1333
1334 #[test]
1335 fn test_extract_prefix_unicode_package_name() {
1336 let content = "日本語-crate = \"1.0\"";
1338 let position = Position {
1339 line: 0,
1340 character: 3, };
1342 let range = Range {
1343 start: Position {
1344 line: 0,
1345 character: 0,
1346 },
1347 end: Position {
1348 line: 0,
1349 character: 10,
1350 },
1351 };
1352
1353 let prefix = extract_prefix(content, position, range);
1354 assert_eq!(prefix, "日本語");
1355 }
1356
1357 #[test]
1358 fn test_extract_prefix_emoji_in_content() {
1359 let content = "emoji-😀-crate = \"1.0\"";
1361 let position = Position {
1362 line: 0,
1363 character: 8, };
1365 let range = Range {
1366 start: Position {
1367 line: 0,
1368 character: 0,
1369 },
1370 end: Position {
1371 line: 0,
1372 character: 14,
1373 },
1374 };
1375
1376 let prefix = extract_prefix(content, position, range);
1377 assert_eq!(prefix, "emoji-😀");
1378 }
1379}