1use crate::lsp_helpers::{escape_markdown, is_safe_version_string, warn_rejected_value};
44use crate::{
45 ConcreteVersion, FreshnessSettings, Metadata, PackageName, ParseResult, PublishTime, Version,
46 format_relative_age,
47};
48use std::time::Duration;
49use tower_lsp_server::ls_types::{
50 CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit,
51 Documentation, MarkupContent, MarkupKind, Position, Range, TextEdit,
52};
53
54pub const COMPLETION_SEARCH_TIMEOUT: Duration = Duration::from_secs(2);
68
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct Completions {
84 pub items: Vec<CompletionItem>,
86 pub is_incomplete: bool,
88}
89
90impl From<Vec<CompletionItem>> for Completions {
91 fn from(items: Vec<CompletionItem>) -> Self {
93 Self {
94 items,
95 is_incomplete: false,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum CompletionContext {
106 PackageName {
110 prefix: String,
112 range: Range,
115 },
116
117 Version {
121 package_name: PackageName,
123 prefix: String,
125 },
126
127 Feature {
131 package_name: PackageName,
133 prefix: String,
135 },
136
137 None,
139}
140
141pub fn detect_completion_context(
170 parse_result: &dyn ParseResult,
171 position: Position,
172 content: &str,
173) -> CompletionContext {
174 let dependencies = parse_result.dependencies();
175
176 for dep in dependencies {
177 let name_range = dep.name_range();
179 if position_in_range(position, name_range)
191 && (name_range.end.line != position.line
192 || position.character <= name_range.end.character)
193 {
194 let prefix = extract_prefix(content, position, name_range);
195 return CompletionContext::PackageName {
196 prefix,
197 range: name_range,
198 };
199 }
200
201 if let Some(version_range) = dep.version_range()
203 && position_in_range(position, version_range)
204 {
205 let prefix = extract_prefix(content, position, version_range);
206 return CompletionContext::Version {
207 package_name: dep.name().clone(),
208 prefix,
209 };
210 }
211
212 if let Some(features_range) = dep.features_range()
214 && position_in_range(position, features_range)
215 {
216 let prefix = extract_feature_prefix(content, position);
217 return CompletionContext::Feature {
218 package_name: dep.name().clone(),
219 prefix,
220 };
221 }
222 }
223
224 CompletionContext::None
225}
226
227const fn position_in_range(position: Position, range: Range) -> bool {
233 if position.line < range.start.line {
235 return false;
236 }
237
238 if position.line == range.start.line && position.character < range.start.character {
239 return false;
240 }
241
242 if position.line > range.end.line {
244 return false;
245 }
246
247 if position.line == range.end.line && position.character > range.end.character + 1 {
248 return false;
249 }
250
251 true
252}
253
254pub fn utf16_to_byte_offset(s: &str, utf16_offset: u32) -> Option<usize> {
285 let mut utf16_count = 0u32;
286 for (byte_idx, ch) in s.char_indices() {
287 if utf16_count >= utf16_offset {
288 return Some(byte_idx);
289 }
290 utf16_count += ch.len_utf16() as u32;
291 }
292 if utf16_count == utf16_offset {
293 return Some(s.len());
294 }
295 None
296}
297
298pub fn byte_to_utf16_offset(s: &str, byte_offset: usize) -> u32 {
323 s[..byte_offset].encode_utf16().count() as u32
324}
325
326pub fn extract_prefix(content: &str, position: Position, range: Range) -> String {
358 let line = match content.lines().nth(position.line as usize) {
360 Some(l) => l,
361 None => return String::new(),
362 };
363
364 let start_byte = if position.line == range.start.line {
366 match utf16_to_byte_offset(line, range.start.character) {
367 Some(offset) => offset,
368 None => return String::new(),
369 }
370 } else {
371 0
372 };
373
374 let cursor_byte = match utf16_to_byte_offset(line, position.character) {
375 Some(offset) => offset,
376 None => return String::new(),
377 };
378
379 if start_byte > line.len() || cursor_byte > line.len() || start_byte > cursor_byte {
381 return String::new();
382 }
383
384 let prefix = &line[start_byte..cursor_byte];
386
387 prefix
389 .trim()
390 .trim_matches('"')
391 .trim_matches('\'')
392 .trim()
393 .to_string()
394}
395
396pub fn extract_feature_prefix(content: &str, position: Position) -> String {
418 let line = match content.lines().nth(position.line as usize) {
419 Some(l) => l,
420 None => return String::new(),
421 };
422
423 let cursor_byte = match utf16_to_byte_offset(line, position.character) {
424 Some(offset) => offset.min(line.len()),
425 None => return String::new(),
426 };
427
428 let before_cursor = &line[..cursor_byte];
429
430 let segment_start = before_cursor.rfind('[').map_or(0, |i| i + 1);
434 let segment = &before_cursor[segment_start..];
435
436 let quote_count = segment.chars().filter(|&c| c == '"').count();
439 if quote_count % 2 == 0 {
440 return String::new();
441 }
442
443 match segment.rfind('"') {
445 Some(pos) => segment[pos + 1..].to_string(),
446 None => String::new(),
447 }
448}
449
450pub fn build_package_completion(
481 metadata: &dyn Metadata,
482 insert_range: Range,
483) -> Option<CompletionItem> {
484 let name = metadata.name();
485 if !crate::is_safe_package_name(name.as_str()) {
486 warn_rejected_value(
487 "is_safe_package_name",
488 "primary completion path package name",
489 name.as_str(),
490 );
491 return None;
492 }
493 let latest = metadata.latest_version().as_str();
494
495 let header = if latest.is_empty() {
497 format!("**{}**", escape_markdown(name.as_str()))
498 } else {
499 format!(
500 "**{}** v{}",
501 escape_markdown(name.as_str()),
502 escape_markdown(latest)
503 )
504 };
505 let mut doc_parts = vec![header];
506
507 if let Some(desc) = metadata.description() {
508 doc_parts.push(String::new()); let truncated = if desc.len() > 200 {
512 let end = desc.floor_char_boundary(200);
513 format!("{}...", escape_markdown(&desc[..end]))
514 } else {
515 escape_markdown(desc)
516 };
517 doc_parts.push(truncated);
518 }
519
520 let mut links = Vec::new();
522 if let Some(repo) = metadata.repository() {
523 links.push(format!("[Repository]({})", escape_markdown(repo)));
524 }
525 if let Some(docs) = metadata.documentation() {
526 links.push(format!("[Documentation]({})", escape_markdown(docs)));
527 }
528
529 if !links.is_empty() {
530 doc_parts.push(String::new()); doc_parts.push(links.join(" | "));
532 }
533
534 Some(CompletionItem {
535 label: name.to_string(),
536 kind: Some(CompletionItemKind::MODULE),
537 detail: if latest.is_empty() {
538 None
539 } else {
540 Some(format!("v{}", latest))
541 },
542 documentation: Some(Documentation::MarkupContent(MarkupContent {
543 kind: MarkupKind::Markdown,
544 value: doc_parts.join("\n"),
545 })),
546 insert_text: Some(name.to_string()),
547 text_edit: Some(CompletionTextEdit::Edit(TextEdit {
548 range: insert_range,
549 new_text: name.to_string(),
550 })),
551 sort_text: Some(name.to_string()),
552 filter_text: Some(name.to_string()),
553 ..Default::default()
554 })
555}
556
557pub fn build_version_completion(
606 display_item: &VersionDisplayItem,
607 insert_range: Option<Range>,
608 now: PublishTime,
609 freshness_enabled: bool,
610) -> CompletionItem {
611 let sort_text = format!("{:05}", display_item.index);
613
614 let label_details = freshness_enabled
617 .then_some(display_item.published_at)
618 .flatten()
619 .map(|published_at| CompletionItemLabelDetails {
620 detail: Some(format!(
621 " {}",
622 format_relative_age(published_at.age_secs_from(now))
623 )),
624 description: None,
625 });
626
627 CompletionItem {
628 label: display_item.label.clone(),
629 kind: Some(CompletionItemKind::VALUE),
630 detail: Some(display_item.description.clone()),
631 documentation: None,
632 insert_text: Some(display_item.version.to_string()),
633 text_edit: insert_range.map(|range| {
634 CompletionTextEdit::Edit(TextEdit {
635 range,
636 new_text: display_item.version.to_string(),
637 })
638 }),
639 sort_text: Some(sort_text),
640 preselect: Some(display_item.is_latest),
641 label_details,
642 ..Default::default()
643 }
644}
645
646#[derive(Debug, Clone)]
650pub struct VersionDisplayItem {
651 pub version: ConcreteVersion,
653 pub label: String,
655 pub description: String,
657 pub index: usize,
659 pub is_latest: bool,
661 pub published_at: Option<PublishTime>,
667}
668
669impl VersionDisplayItem {
670 pub fn new(
672 version: &dyn Version,
673 package_name: &PackageName,
674 index: usize,
675 is_latest: bool,
676 ) -> Self {
677 let version_str = version.version_string();
678 let label = if is_latest {
679 format!("{} (latest)", version_str)
680 } else {
681 version_str.to_string()
682 };
683 let description = format!("Update {} to {}", package_name, version_str);
684
685 Self {
686 version: version_str.clone(),
687 label,
688 description,
689 index,
690 is_latest,
691 published_at: version.published_at(),
692 }
693 }
694}
695
696pub fn prepare_version_display_items<V: AsRef<dyn Version>>(
704 versions: &[V],
705 package_name: &PackageName,
706) -> Vec<VersionDisplayItem> {
707 versions
708 .iter()
709 .map(|v| v.as_ref())
710 .filter(|v| !v.removal_status().blocks_resolution())
711 .take(MAX_COMPLETION_VERSIONS)
712 .enumerate()
713 .map(|(index, version)| VersionDisplayItem::new(version, package_name, index, index == 0))
714 .collect()
715}
716
717pub fn build_feature_completion(
742 feature_name: &str,
743 package_name: &PackageName,
744 insert_range: Option<Range>,
745) -> CompletionItem {
746 CompletionItem {
747 label: feature_name.to_string(),
748 kind: Some(CompletionItemKind::PROPERTY),
749 detail: Some(format!("Feature of {}", package_name)),
750 documentation: None,
751 insert_text: Some(feature_name.to_string()),
752 text_edit: insert_range.map(|range| {
753 CompletionTextEdit::Edit(TextEdit {
754 range,
755 new_text: feature_name.to_string(),
756 })
757 }),
758 sort_text: Some(feature_name.to_string()),
759 ..Default::default()
760 }
761}
762
763const MAX_COMPLETION_VERSIONS: usize = 5;
765
766#[must_use]
786pub fn is_valid_completion_prefix_len(prefix: &str) -> bool {
787 (2..=200).contains(&prefix.chars().count())
788}
789
790pub async fn complete_package_names_generic(
799 registry: &dyn crate::Registry,
800 prefix: &str,
801 limit: usize,
802 insert_range: Range,
803) -> Vec<CompletionItem> {
804 if !is_valid_completion_prefix_len(prefix) {
805 return vec![];
806 }
807
808 let results = match registry.search(prefix, limit).await {
809 Ok(r) => r,
810 Err(e) => {
811 tracing::warn!("Registry search failed for '{}': {}", prefix, e);
812 return vec![];
813 }
814 };
815
816 results
817 .into_iter()
818 .filter_map(|metadata| build_package_completion(metadata.as_ref(), insert_range))
819 .collect()
820}
821
822pub async fn complete_versions_generic(
869 registry: &dyn crate::Registry,
870 package_name: &PackageName,
871 prefix: &str,
872 operator_chars: &[char],
873 freshness: FreshnessSettings,
874) -> Vec<CompletionItem> {
875 complete_versions_generic_from(
876 registry,
877 package_name,
878 &crate::parser::DependencySource::Registry,
879 prefix,
880 operator_chars,
881 freshness,
882 )
883 .await
884}
885
886pub async fn complete_versions_generic_from(
920 registry: &dyn crate::Registry,
921 package_name: &PackageName,
922 source: &crate::parser::DependencySource,
923 prefix: &str,
924 operator_chars: &[char],
925 freshness: FreshnessSettings,
926) -> Vec<CompletionItem> {
927 let versions = match registry
928 .get_versions_from(package_name, source, freshness)
929 .await
930 {
931 Ok(v) => v,
932 Err(e) => {
933 tracing::warn!("Failed to fetch versions for '{}': {}", package_name, e);
934 return vec![];
935 }
936 };
937
938 let clean_prefix = prefix.trim_start_matches(operator_chars).trim();
939
940 let filtered_versions: Vec<_> = versions
942 .iter()
943 .filter(|v| v.version_string().as_str().starts_with(clean_prefix))
944 .collect();
945
946 let display_items = if filtered_versions.is_empty() {
948 prepare_version_display_items(&versions, package_name)
949 } else {
950 prepare_version_display_items(&filtered_versions, package_name)
951 };
952
953 let now = PublishTime::now();
955 display_items
956 .iter()
957 .filter(|item| {
963 let safe = is_safe_version_string(item.version.as_str());
964 if !safe {
965 warn_rejected_value(
966 "is_safe_version_string",
967 "version completion item",
968 item.version.as_str(),
969 );
970 }
971 safe
972 })
973 .map(|item| build_version_completion(item, None, now, freshness.enabled))
974 .collect()
975}
976
977pub async fn complete_versions_at_position(
1031 registry: &dyn crate::Registry,
1032 formatter: &dyn crate::lsp_helpers::SourcePolicy,
1033 parse_result: &dyn ParseResult,
1034 position: Position,
1035 prefix: &str,
1036 operator_chars: &[char],
1037 freshness: FreshnessSettings,
1038) -> Vec<CompletionItem> {
1039 let Some(dep) = parse_result.dependencies().into_iter().find(|d| {
1040 d.version_range()
1041 .is_some_and(|r| position_in_range(position, r))
1042 }) else {
1043 return vec![];
1044 };
1045
1046 let source = dep.source();
1047 if !formatter.can_resolve_source(&source) {
1048 return vec![];
1049 }
1050
1051 complete_versions_generic_from(
1052 registry,
1053 dep.name(),
1054 &source,
1055 prefix,
1056 operator_chars,
1057 freshness,
1058 )
1059 .await
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064 use super::*;
1065 use std::any::Any;
1066 use std::assert_matches;
1067
1068 fn pkg(s: &str) -> PackageName {
1069 PackageName::new(s)
1070 }
1071
1072 struct MockDependency {
1075 name: crate::PackageName,
1076 name_range: Range,
1077 version_range: Option<Range>,
1078 features_range: Option<Range>,
1079 }
1080
1081 impl crate::ecosystem::Dependency for MockDependency {
1082 fn name(&self) -> &crate::PackageName {
1083 &self.name
1084 }
1085
1086 fn name_range(&self) -> Range {
1087 self.name_range
1088 }
1089
1090 fn version_requirement(&self) -> Option<&crate::VersionReq> {
1091 static VERSION_REQ: std::sync::LazyLock<crate::VersionReq> =
1092 std::sync::LazyLock::new(|| crate::VersionReq::new("1.0"));
1093 Some(&VERSION_REQ)
1094 }
1095
1096 fn version_range(&self) -> Option<Range> {
1097 self.version_range
1098 }
1099
1100 fn features_range(&self) -> Option<Range> {
1101 self.features_range
1102 }
1103
1104 fn source(&self) -> crate::parser::DependencySource {
1105 crate::parser::DependencySource::Registry
1106 }
1107
1108 fn as_any(&self) -> &dyn Any {
1109 self
1110 }
1111 }
1112
1113 struct MockParseResult {
1114 dependencies: Vec<MockDependency>,
1115 }
1116
1117 impl ParseResult for MockParseResult {
1118 fn dependencies(&self) -> Vec<&dyn crate::ecosystem::Dependency> {
1119 self.dependencies
1120 .iter()
1121 .map(|d| d as &dyn crate::ecosystem::Dependency)
1122 .collect()
1123 }
1124
1125 fn workspace_root(&self) -> Option<&std::path::Path> {
1126 None
1127 }
1128
1129 fn uri(&self) -> &tower_lsp_server::ls_types::Uri {
1130 static URL: std::sync::LazyLock<tower_lsp_server::ls_types::Uri> =
1131 std::sync::LazyLock::new(|| "file:///test/Cargo.toml".parse().unwrap());
1132 &URL
1133 }
1134
1135 fn as_any(&self) -> &dyn Any {
1136 self
1137 }
1138 }
1139
1140 struct MockVersion {
1141 version: ConcreteVersion,
1142 yanked: bool,
1143 prerelease: bool,
1144 }
1145
1146 impl crate::registry::Version for MockVersion {
1147 fn version_string(&self) -> &ConcreteVersion {
1148 &self.version
1149 }
1150
1151 fn removal_status(&self) -> crate::RemovalStatus {
1152 crate::RemovalStatus::from_yanked(self.yanked)
1153 }
1154
1155 fn is_prerelease(&self) -> bool {
1156 self.prerelease
1157 }
1158
1159 fn as_any(&self) -> &dyn Any {
1160 self
1161 }
1162 }
1163
1164 struct MockVersionWithAge {
1168 version: ConcreteVersion,
1169 published_at: Option<PublishTime>,
1170 }
1171
1172 impl crate::registry::Version for MockVersionWithAge {
1173 fn version_string(&self) -> &ConcreteVersion {
1174 &self.version
1175 }
1176
1177 fn published_at(&self) -> Option<PublishTime> {
1178 self.published_at
1179 }
1180
1181 fn as_any(&self) -> &dyn Any {
1182 self
1183 }
1184 }
1185
1186 #[derive(Clone)]
1187 struct MockMetadata {
1188 name: crate::PackageName,
1189 description: Option<String>,
1190 repository: Option<String>,
1191 documentation: Option<String>,
1192 latest_version: ConcreteVersion,
1193 }
1194
1195 impl crate::registry::Metadata for MockMetadata {
1196 fn name(&self) -> &crate::PackageName {
1197 &self.name
1198 }
1199
1200 fn description(&self) -> Option<&str> {
1201 self.description.as_deref()
1202 }
1203
1204 fn repository(&self) -> Option<&str> {
1205 self.repository.as_deref()
1206 }
1207
1208 fn documentation(&self) -> Option<&str> {
1209 self.documentation.as_deref()
1210 }
1211
1212 fn latest_version(&self) -> &ConcreteVersion {
1213 &self.latest_version
1214 }
1215
1216 fn as_any(&self) -> &dyn Any {
1217 self
1218 }
1219 }
1220
1221 struct MockRegistry {
1222 versions: Vec<MockVersion>,
1223 }
1224
1225 impl crate::Registry for MockRegistry {
1226 fn get_versions<'a>(
1227 &'a self,
1228 _package_name: &'a crate::PackageName,
1229 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
1230 {
1231 let versions: Vec<Box<dyn crate::Version>> = self
1232 .versions
1233 .iter()
1234 .map(|v| {
1235 Box::new(MockVersion {
1236 version: v.version.clone(),
1237 yanked: v.yanked,
1238 prerelease: v.prerelease,
1239 }) as Box<dyn crate::Version>
1240 })
1241 .collect();
1242 Box::pin(async move { Ok(versions) })
1243 }
1244
1245 fn get_latest_matching<'a>(
1246 &'a self,
1247 _name: &'a crate::PackageName,
1248 _req: &'a crate::VersionReq,
1249 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
1250 {
1251 Box::pin(async move { Ok(None) })
1252 }
1253
1254 fn search<'a>(
1255 &'a self,
1256 _query: &'a str,
1257 _limit: usize,
1258 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
1259 {
1260 Box::pin(async move { Ok(vec![]) })
1261 }
1262
1263 fn as_any(&self) -> &dyn Any {
1264 self
1265 }
1266 }
1267
1268 struct MockSearchRegistry {
1272 results: Vec<MockMetadata>,
1273 }
1274
1275 impl crate::Registry for MockSearchRegistry {
1276 fn get_versions<'a>(
1277 &'a self,
1278 _package_name: &'a crate::PackageName,
1279 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
1280 {
1281 Box::pin(async move { Ok(vec![]) })
1282 }
1283
1284 fn get_latest_matching<'a>(
1285 &'a self,
1286 _name: &'a crate::PackageName,
1287 _req: &'a crate::VersionReq,
1288 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
1289 {
1290 Box::pin(async move { Ok(None) })
1291 }
1292
1293 fn search<'a>(
1294 &'a self,
1295 _query: &'a str,
1296 _limit: usize,
1297 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
1298 {
1299 let results: Vec<Box<dyn crate::Metadata>> = self
1300 .results
1301 .iter()
1302 .cloned()
1303 .map(|m| Box::new(m) as Box<dyn crate::Metadata>)
1304 .collect();
1305 Box::pin(async move { Ok(results) })
1306 }
1307
1308 fn as_any(&self) -> &dyn Any {
1309 self
1310 }
1311 }
1312
1313 #[tokio::test]
1314 async fn test_complete_package_names_generic_uses_insert_range() {
1315 let registry = MockSearchRegistry {
1316 results: vec![MockMetadata {
1317 name: pkg("serde"),
1318 description: None,
1319 repository: None,
1320 documentation: None,
1321 latest_version: "1.0.0".into(),
1322 }],
1323 };
1324
1325 let insert_range = Range {
1326 start: Position {
1327 line: 3,
1328 character: 4,
1329 },
1330 end: Position {
1331 line: 3,
1332 character: 7,
1333 },
1334 };
1335
1336 let items = complete_package_names_generic(®istry, "ser", 5, insert_range).await;
1337
1338 assert_eq!(items.len(), 1);
1339 assert_ne!(insert_range, Range::default());
1340 match &items[0].text_edit {
1341 Some(CompletionTextEdit::Edit(edit)) => {
1342 assert_eq!(edit.range, insert_range);
1343 assert_eq!(edit.new_text, "serde");
1344 }
1345 other => panic!("Expected a textEdit::Edit, got {other:?}"),
1346 }
1347 }
1348
1349 #[tokio::test]
1350 async fn test_complete_package_names_generic_short_prefix_empty() {
1351 let registry = MockSearchRegistry {
1352 results: vec![MockMetadata {
1353 name: pkg("serde"),
1354 description: None,
1355 repository: None,
1356 documentation: None,
1357 latest_version: "1.0.0".into(),
1358 }],
1359 };
1360
1361 let items = complete_package_names_generic(®istry, "s", 5, Range::default()).await;
1362 assert!(items.is_empty());
1363 }
1364
1365 #[tokio::test]
1366 async fn test_complete_package_names_generic_drops_unsafe_names() {
1367 let registry = MockSearchRegistry {
1371 results: vec![
1372 MockMetadata {
1373 name: pkg("serde"),
1374 description: None,
1375 repository: None,
1376 documentation: None,
1377 latest_version: "1.0.0".into(),
1378 },
1379 MockMetadata {
1380 name: pkg("guava'); System.exit(1); //"),
1381 description: None,
1382 repository: None,
1383 documentation: None,
1384 latest_version: "1.0.0".into(),
1385 },
1386 ],
1387 };
1388
1389 let items = complete_package_names_generic(®istry, "gua", 5, Range::default()).await;
1390
1391 assert_eq!(items.len(), 1);
1392 assert_eq!(items[0].label, "serde");
1393 }
1394
1395 #[test]
1396 fn test_is_valid_completion_prefix_len_ascii_short_rejected() {
1397 assert!(!is_valid_completion_prefix_len("a"));
1398 }
1399
1400 #[test]
1401 fn test_is_valid_completion_prefix_len_one_char_cjk_rejected() {
1402 assert!(!is_valid_completion_prefix_len("日"));
1404 }
1405
1406 #[test]
1407 fn test_is_valid_completion_prefix_len_two_char_cjk_accepted() {
1408 assert!(is_valid_completion_prefix_len("日本"));
1410 }
1411
1412 #[tokio::test]
1413 async fn test_complete_package_names_generic_one_char_cjk_prefix_empty() {
1414 let registry = MockSearchRegistry {
1416 results: vec![MockMetadata {
1417 name: pkg("serde"),
1418 description: None,
1419 repository: None,
1420 documentation: None,
1421 latest_version: "1.0.0".into(),
1422 }],
1423 };
1424
1425 let items = complete_package_names_generic(®istry, "日", 5, Range::default()).await;
1426 assert!(items.is_empty());
1427 }
1428
1429 #[tokio::test]
1430 async fn test_complete_package_names_generic_two_char_cjk_prefix_accepted() {
1431 let registry = MockSearchRegistry {
1433 results: vec![MockMetadata {
1434 name: pkg("serde"),
1435 description: None,
1436 repository: None,
1437 documentation: None,
1438 latest_version: "1.0.0".into(),
1439 }],
1440 };
1441
1442 let items = complete_package_names_generic(®istry, "日本", 5, Range::default()).await;
1443 assert_eq!(items.len(), 1);
1444 }
1445
1446 #[test]
1449 fn test_detect_package_name_context_at_start() {
1450 let parse_result = MockParseResult {
1451 dependencies: vec![MockDependency {
1452 name: "serde".into(),
1453 name_range: Range {
1454 start: Position {
1455 line: 0,
1456 character: 0,
1457 },
1458 end: Position {
1459 line: 0,
1460 character: 5,
1461 },
1462 },
1463 version_range: None,
1464 features_range: None,
1465 }],
1466 };
1467
1468 let content = "serde";
1469 let position = Position {
1470 line: 0,
1471 character: 0,
1472 };
1473
1474 let context = detect_completion_context(&parse_result, position, content);
1475
1476 match context {
1477 CompletionContext::PackageName { prefix, range } => {
1478 assert_eq!(prefix, "");
1479 assert_eq!(
1480 range,
1481 Range {
1482 start: Position {
1483 line: 0,
1484 character: 0
1485 },
1486 end: Position {
1487 line: 0,
1488 character: 5
1489 },
1490 }
1491 );
1492 }
1493 _ => panic!("Expected PackageName context, got {:?}", context),
1494 }
1495 }
1496
1497 #[test]
1498 fn test_detect_package_name_context_partial() {
1499 let parse_result = MockParseResult {
1500 dependencies: vec![MockDependency {
1501 name: "serde".into(),
1502 name_range: Range {
1503 start: Position {
1504 line: 0,
1505 character: 0,
1506 },
1507 end: Position {
1508 line: 0,
1509 character: 5,
1510 },
1511 },
1512 version_range: None,
1513 features_range: None,
1514 }],
1515 };
1516
1517 let content = "serde";
1518 let position = Position {
1519 line: 0,
1520 character: 3,
1521 };
1522
1523 let context = detect_completion_context(&parse_result, position, content);
1524
1525 match context {
1526 CompletionContext::PackageName { prefix, range } => {
1527 assert_eq!(prefix, "ser");
1528 assert_eq!(
1529 range,
1530 Range {
1531 start: Position {
1532 line: 0,
1533 character: 0
1534 },
1535 end: Position {
1536 line: 0,
1537 character: 5
1538 },
1539 }
1540 );
1541 }
1542 _ => panic!("Expected PackageName context, got {:?}", context),
1543 }
1544 }
1545
1546 #[test]
1547 fn test_detect_package_name_context_one_past_end_does_not_widen_range() {
1548 let parse_result = MockParseResult {
1556 dependencies: vec![MockDependency {
1557 name: "serde".into(),
1558 name_range: Range {
1559 start: Position {
1560 line: 0,
1561 character: 0,
1562 },
1563 end: Position {
1564 line: 0,
1565 character: 5,
1566 },
1567 },
1568 version_range: None,
1569 features_range: None,
1570 }],
1571 };
1572
1573 let content = "serde";
1574 let position = Position {
1575 line: 0,
1576 character: 6,
1577 };
1578
1579 let context = detect_completion_context(&parse_result, position, content);
1580
1581 assert_eq!(context, CompletionContext::None);
1582 }
1583
1584 #[test]
1585 fn test_detect_package_name_context_exactly_at_end_matches_unwidened_range() {
1586 let parse_result = MockParseResult {
1590 dependencies: vec![MockDependency {
1591 name: "serde".into(),
1592 name_range: Range {
1593 start: Position {
1594 line: 0,
1595 character: 0,
1596 },
1597 end: Position {
1598 line: 0,
1599 character: 5,
1600 },
1601 },
1602 version_range: None,
1603 features_range: None,
1604 }],
1605 };
1606
1607 let content = "serde";
1608 let position = Position {
1609 line: 0,
1610 character: 5,
1611 };
1612
1613 let context = detect_completion_context(&parse_result, position, content);
1614
1615 match context {
1616 CompletionContext::PackageName { prefix, range } => {
1617 assert_eq!(prefix, "serde");
1618 assert_eq!(
1619 range,
1620 Range {
1621 start: Position {
1622 line: 0,
1623 character: 0
1624 },
1625 end: Position {
1626 line: 0,
1627 character: 5
1628 },
1629 }
1630 );
1631 }
1632 _ => panic!("Expected PackageName context, got {:?}", context),
1633 }
1634 }
1635
1636 #[test]
1637 fn test_detect_package_name_context_fires_for_ecosystem_supplied_partial_name() {
1638 let parse_result = MockParseResult {
1648 dependencies: vec![MockDependency {
1649 name: "jsr:@std/".into(),
1650 name_range: Range {
1651 start: Position {
1652 line: 0,
1653 character: 0,
1654 },
1655 end: Position {
1656 line: 0,
1657 character: 9,
1658 },
1659 },
1660 version_range: None,
1661 features_range: None,
1662 }],
1663 };
1664
1665 let content = "jsr:@std/";
1666 let position = Position {
1667 line: 0,
1668 character: 9,
1669 };
1670
1671 let context = detect_completion_context(&parse_result, position, content);
1672
1673 match context {
1674 CompletionContext::PackageName { prefix, range } => {
1675 assert_eq!(prefix, "jsr:@std/");
1676 assert_eq!(range, parse_result.dependencies[0].name_range);
1677 }
1678 other => panic!("Expected PackageName context, got {other:?}"),
1679 }
1680 }
1681
1682 #[test]
1683 fn test_detect_version_context() {
1684 let parse_result = MockParseResult {
1685 dependencies: vec![MockDependency {
1686 name: "serde".into(),
1687 name_range: Range {
1688 start: Position {
1689 line: 0,
1690 character: 0,
1691 },
1692 end: Position {
1693 line: 0,
1694 character: 5,
1695 },
1696 },
1697 version_range: Some(Range {
1698 start: Position {
1699 line: 0,
1700 character: 9,
1701 },
1702 end: Position {
1703 line: 0,
1704 character: 14,
1705 },
1706 }),
1707 features_range: None,
1708 }],
1709 };
1710
1711 let content = r#"serde = "1.0.1""#;
1712 let position = Position {
1713 line: 0,
1714 character: 11,
1715 };
1716
1717 let context = detect_completion_context(&parse_result, position, content);
1718
1719 match context {
1720 CompletionContext::Version {
1721 package_name,
1722 prefix,
1723 } => {
1724 assert_eq!(package_name, "serde");
1725 assert_eq!(prefix, "1.");
1726 }
1727 _ => panic!("Expected Version context, got {:?}", context),
1728 }
1729 }
1730
1731 #[test]
1732 fn test_detect_no_context_before_dependencies() {
1733 let parse_result = MockParseResult {
1734 dependencies: vec![MockDependency {
1735 name: "serde".into(),
1736 name_range: Range {
1737 start: Position {
1738 line: 5,
1739 character: 0,
1740 },
1741 end: Position {
1742 line: 5,
1743 character: 5,
1744 },
1745 },
1746 version_range: None,
1747 features_range: None,
1748 }],
1749 };
1750
1751 let content = "[dependencies]\nserde";
1752 let position = Position {
1753 line: 0,
1754 character: 10,
1755 };
1756
1757 let context = detect_completion_context(&parse_result, position, content);
1758
1759 assert_eq!(context, CompletionContext::None);
1760 }
1761
1762 #[test]
1763 fn test_detect_no_context_invalid_position() {
1764 let parse_result = MockParseResult {
1765 dependencies: vec![],
1766 };
1767
1768 let content = "";
1769 let position = Position {
1770 line: 100,
1771 character: 100,
1772 };
1773
1774 let context = detect_completion_context(&parse_result, position, content);
1775
1776 assert_eq!(context, CompletionContext::None);
1777 }
1778
1779 #[test]
1782 fn test_extract_prefix_at_start() {
1783 let content = "serde";
1784 let position = Position {
1785 line: 0,
1786 character: 0,
1787 };
1788 let range = Range {
1789 start: Position {
1790 line: 0,
1791 character: 0,
1792 },
1793 end: Position {
1794 line: 0,
1795 character: 5,
1796 },
1797 };
1798
1799 let prefix = extract_prefix(content, position, range);
1800 assert_eq!(prefix, "");
1801 }
1802
1803 #[test]
1804 fn test_extract_prefix_partial() {
1805 let content = "serde";
1806 let position = Position {
1807 line: 0,
1808 character: 3,
1809 };
1810 let range = Range {
1811 start: Position {
1812 line: 0,
1813 character: 0,
1814 },
1815 end: Position {
1816 line: 0,
1817 character: 5,
1818 },
1819 };
1820
1821 let prefix = extract_prefix(content, position, range);
1822 assert_eq!(prefix, "ser");
1823 }
1824
1825 #[test]
1826 fn test_extract_prefix_with_quotes() {
1827 let content = r#"serde = "1.0""#;
1828 let position = Position {
1829 line: 0,
1830 character: 11,
1831 };
1832 let range = Range {
1833 start: Position {
1834 line: 0,
1835 character: 9,
1836 },
1837 end: Position {
1838 line: 0,
1839 character: 13,
1840 },
1841 };
1842
1843 let prefix = extract_prefix(content, position, range);
1844 assert_eq!(prefix, "1.");
1845 }
1846
1847 #[test]
1848 fn test_extract_prefix_empty() {
1849 let content = r#"serde = """#;
1850 let position = Position {
1851 line: 0,
1852 character: 9,
1853 };
1854 let range = Range {
1855 start: Position {
1856 line: 0,
1857 character: 9,
1858 },
1859 end: Position {
1860 line: 0,
1861 character: 11,
1862 },
1863 };
1864
1865 let prefix = extract_prefix(content, position, range);
1866 assert_eq!(prefix, "");
1867 }
1868
1869 #[test]
1870 fn test_extract_prefix_version_with_operator() {
1871 let content = r#"serde = "^1.0""#;
1872 let position = Position {
1873 line: 0,
1874 character: 12,
1875 };
1876 let range = Range {
1877 start: Position {
1878 line: 0,
1879 character: 9,
1880 },
1881 end: Position {
1882 line: 0,
1883 character: 14,
1884 },
1885 };
1886
1887 let prefix = extract_prefix(content, position, range);
1888 assert_eq!(prefix, "^1.");
1889 }
1890
1891 #[test]
1894 fn test_build_package_completion_full() {
1895 let metadata = MockMetadata {
1896 name: "serde".into(),
1897 description: Some("Serialization framework".to_string()),
1898 repository: Some("https://github.com/serde-rs/serde".to_string()),
1899 documentation: Some("https://docs.rs/serde".to_string()),
1900 latest_version: "1.0.214".into(),
1901 };
1902
1903 let range = Range::default();
1904 let item = build_package_completion(&metadata, range).unwrap();
1905
1906 assert_eq!(item.label, "serde");
1907 assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
1908 assert_eq!(item.detail, Some("v1.0.214".to_string()));
1909 assert_matches!(item.documentation, Some(Documentation::MarkupContent(_)));
1910
1911 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1912 assert!(content.value.contains("**serde** v1\\.0\\.214"));
1913 assert!(content.value.contains("Serialization framework"));
1914 assert!(content.value.contains("Repository"));
1915 assert!(content.value.contains("Documentation"));
1916 }
1917 }
1918
1919 #[test]
1920 fn test_build_package_completion_minimal() {
1921 let metadata = MockMetadata {
1922 name: "test-pkg".into(),
1923 description: None,
1924 repository: None,
1925 documentation: None,
1926 latest_version: "0.1.0".into(),
1927 };
1928
1929 let range = Range::default();
1930 let item = build_package_completion(&metadata, range).unwrap();
1931
1932 assert_eq!(item.label, "test-pkg");
1933 assert_eq!(item.detail, Some("v0.1.0".to_string()));
1934
1935 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1936 assert!(content.value.contains("**test\\-pkg** v0\\.1\\.0"));
1937 assert!(!content.value.contains("Repository"));
1938 }
1939 }
1940
1941 #[test]
1942 fn test_build_package_completion_empty_latest_version() {
1943 let metadata = MockMetadata {
1944 name: "swift-nio".into(),
1945 description: None,
1946 repository: None,
1947 documentation: None,
1948 latest_version: String::new().into(),
1949 };
1950
1951 let range = Range::default();
1952 let item = build_package_completion(&metadata, range).unwrap();
1953
1954 assert_eq!(item.detail, None);
1955
1956 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1957 assert!(content.value.contains("**swift\\-nio**"));
1958 assert!(!content.value.trim_end().ends_with('v'));
1959 }
1960 }
1961
1962 #[test]
1963 fn test_build_package_completion_escapes_description_markdown() {
1964 let metadata = MockMetadata {
1965 name: "test-pkg".into(),
1966 description: Some("Fast *bold* _italic_ [link](evil) `code`".to_string()),
1967 repository: None,
1968 documentation: None,
1969 latest_version: "1.0.0".into(),
1970 };
1971
1972 let range = Range::default();
1973 let item = build_package_completion(&metadata, range).unwrap();
1974
1975 if let Some(Documentation::MarkupContent(content)) = item.documentation {
1976 assert!(!content.value.contains("*bold*"));
1977 assert!(!content.value.contains("_italic_"));
1978 assert!(!content.value.contains("[link](evil)"));
1979 assert!(content.value.contains(r"\*bold\*"));
1980 assert!(content.value.contains(r"\[link\]\(evil\)"));
1981 } else {
1982 panic!("Expected MarkupContent documentation");
1983 }
1984 }
1985
1986 #[test]
1987 fn test_build_package_completion_escapes_repository_link_breakout() {
1988 let malicious_repo = "https://legit.example)[Click here](https://evil.example";
1991 let metadata = MockMetadata {
1992 name: "test-pkg".into(),
1993 description: None,
1994 repository: Some(malicious_repo.to_string()),
1995 documentation: None,
1996 latest_version: "1.0.0".into(),
1997 };
1998
1999 let range = Range::default();
2000 let item = build_package_completion(&metadata, range).unwrap();
2001
2002 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2003 assert!(!content.value.contains(")[Click here]("));
2004 assert!(content.value.contains(r"\)\[Click here\]\("));
2005 } else {
2006 panic!("Expected MarkupContent documentation");
2007 }
2008 }
2009
2010 #[test]
2011 fn test_build_package_completion_escapes_documentation_link_breakout() {
2012 let malicious_docs = "https://legit.example)[Click here](https://evil.example";
2013 let metadata = MockMetadata {
2014 name: "test-pkg".into(),
2015 description: None,
2016 repository: None,
2017 documentation: Some(malicious_docs.to_string()),
2018 latest_version: "1.0.0".into(),
2019 };
2020
2021 let range = Range::default();
2022 let item = build_package_completion(&metadata, range).unwrap();
2023
2024 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2025 assert!(!content.value.contains(")[Click here]("));
2026 assert!(content.value.contains(r"\)\[Click here\]\("));
2027 } else {
2028 panic!("Expected MarkupContent documentation");
2029 }
2030 }
2031
2032 #[test]
2033 fn test_build_package_completion_truncate_then_escape_no_dangling_backslash() {
2034 let mut desc = "a".repeat(199);
2039 desc.push('*');
2040 desc.push_str(&"b".repeat(50));
2041
2042 let metadata = MockMetadata {
2043 name: "test-pkg".into(),
2044 description: Some(desc),
2045 repository: None,
2046 documentation: None,
2047 latest_version: "1.0.0".into(),
2048 };
2049
2050 let range = Range::default();
2051 let item = build_package_completion(&metadata, range).unwrap();
2052
2053 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2054 let lines: Vec<_> = content.value.lines().collect();
2055 let desc_line = lines[2];
2056 assert!(desc_line.ends_with(r"\*..."), "got: {desc_line}");
2057 assert!(
2058 !desc_line.ends_with(r"\..."),
2059 "dangling backslash: {desc_line}"
2060 );
2061 } else {
2062 panic!("Expected MarkupContent documentation");
2063 }
2064 }
2065
2066 #[test]
2067 fn test_build_package_completion_escapes_malicious_version_link_breakout() {
2068 let malicious_latest = "1.0.0)[click](https://evil.example";
2074 let metadata = MockMetadata {
2075 name: "test-pkg".into(),
2076 description: None,
2077 repository: None,
2078 documentation: None,
2079 latest_version: malicious_latest.into(),
2080 };
2081
2082 let range = Range::default();
2083 let item = build_package_completion(&metadata, range).unwrap();
2084
2085 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2086 assert!(!content.value.contains(")[click]("));
2087 assert!(
2088 content
2089 .value
2090 .contains(r"1\.0\.0\)\[click\]\(https\:\/\/evil\.example")
2091 );
2092 } else {
2093 panic!("Expected MarkupContent documentation");
2094 }
2095 }
2096
2097 #[test]
2098 fn test_build_package_completion_rejects_unsafe_name() {
2099 let malicious_name = "a** [Official Download](https://evil.example) **b";
2106 let metadata = MockMetadata {
2107 name: malicious_name.into(),
2108 description: None,
2109 repository: None,
2110 documentation: None,
2111 latest_version: "1.0.0".into(),
2112 };
2113
2114 let range = Range::default();
2115 assert!(build_package_completion(&metadata, range).is_none());
2116 }
2117
2118 #[test]
2119 fn test_build_package_completion_benign_repository_url_round_trips() {
2120 let metadata = MockMetadata {
2124 name: "test-pkg".into(),
2125 description: None,
2126 repository: Some("https://github.com/owner/repo".to_string()),
2127 documentation: None,
2128 latest_version: "1.0.0".into(),
2129 };
2130
2131 let range = Range::default();
2132 let item = build_package_completion(&metadata, range).unwrap();
2133
2134 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2135 let unescaped: String = content.value.chars().filter(|&c| c != '\\').collect();
2136 assert!(unescaped.contains("[Repository](https://github.com/owner/repo)"));
2137 } else {
2138 panic!("Expected MarkupContent documentation");
2139 }
2140 }
2141
2142 #[test]
2143 fn test_build_package_completion_escapes_html_in_description() {
2144 let metadata = MockMetadata {
2145 name: "test-pkg".into(),
2146 description: Some("<img src=x onerror=alert(1)>".to_string()),
2147 repository: None,
2148 documentation: None,
2149 latest_version: "1.0.0".into(),
2150 };
2151
2152 let range = Range::default();
2153 let item = build_package_completion(&metadata, range).unwrap();
2154
2155 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2156 assert!(!content.value.contains("<img src=x onerror=alert(1)>"));
2157 assert!(
2158 content
2159 .value
2160 .contains(r"\<img src\=x onerror\=alert\(1\)\>")
2161 );
2162 } else {
2163 panic!("Expected MarkupContent documentation");
2164 }
2165 }
2166
2167 #[test]
2168 fn test_build_package_completion_empty_description() {
2169 let metadata = MockMetadata {
2170 name: "test-pkg".into(),
2171 description: Some(String::new()),
2172 repository: None,
2173 documentation: None,
2174 latest_version: "1.0.0".into(),
2175 };
2176
2177 let range = Range::default();
2178 let item = build_package_completion(&metadata, range).unwrap();
2179
2180 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2181 assert!(content.value.starts_with(r"**test\-pkg** v1\.0\.0"));
2182 } else {
2183 panic!("Expected MarkupContent documentation");
2184 }
2185 }
2186
2187 #[test]
2188 fn test_build_package_completion_truncate_snaps_multibyte_boundary() {
2189 let mut desc = "a".repeat(199);
2195 desc.push('日'); desc.push('*');
2197 desc.push_str(&"b".repeat(50));
2198
2199 let metadata = MockMetadata {
2200 name: "test-pkg".into(),
2201 description: Some(desc),
2202 repository: None,
2203 documentation: None,
2204 latest_version: "1.0.0".into(),
2205 };
2206
2207 let range = Range::default();
2208 let item = build_package_completion(&metadata, range).unwrap();
2209
2210 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2211 let lines: Vec<_> = content.value.lines().collect();
2212 let desc_line = lines[2];
2213 assert!(!desc_line.contains('日'));
2214 assert!(desc_line.ends_with("..."));
2215 } else {
2216 panic!("Expected MarkupContent documentation");
2217 }
2218 }
2219
2220 #[test]
2221 fn test_build_version_completion_stable() {
2222 let version = MockVersion {
2223 version: "1.0.0".into(),
2224 yanked: false,
2225 prerelease: false,
2226 };
2227
2228 let now = PublishTime::now();
2229 let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, false);
2230 let item = build_version_completion(&display_item, None, now, true);
2231
2232 assert_eq!(item.label, "1.0.0");
2233 assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
2234 assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
2235 assert_eq!(item.documentation, None);
2236 assert_eq!(item.preselect, Some(false));
2237 assert_eq!(item.sort_text, Some("00000".to_string()));
2238 assert_eq!(item.text_edit, None); }
2240
2241 #[test]
2242 fn test_build_version_completion_latest() {
2243 let version = MockVersion {
2244 version: "1.0.0".into(),
2245 yanked: false,
2246 prerelease: false,
2247 };
2248
2249 let now = PublishTime::now();
2250 let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
2251 let item = build_version_completion(&display_item, None, now, true);
2252
2253 assert_eq!(item.label, "1.0.0 (latest)");
2254 assert_eq!(item.kind, Some(CompletionItemKind::VALUE));
2255 assert_eq!(item.detail, Some("Update serde to 1.0.0".to_string()));
2256 assert_eq!(item.documentation, None);
2257 assert_eq!(item.preselect, Some(true));
2258 assert_eq!(item.sort_text, Some("00000".to_string()));
2259 assert_eq!(item.text_edit, None); }
2261
2262 #[test]
2263 fn test_build_version_completion_not_latest() {
2264 let version = MockVersion {
2265 version: "0.9.0".into(),
2266 yanked: false,
2267 prerelease: false,
2268 };
2269
2270 let now = PublishTime::now();
2271 let display_item = VersionDisplayItem::new(&version, &pkg("tokio"), 1, false);
2272 let item = build_version_completion(&display_item, None, now, true);
2273
2274 assert_eq!(item.label, "0.9.0");
2275 assert_eq!(item.detail, Some("Update tokio to 0.9.0".to_string()));
2276 assert_eq!(item.documentation, None);
2277 assert_eq!(item.preselect, Some(false));
2278 assert_eq!(item.sort_text, Some("00001".to_string()));
2279 assert_eq!(item.text_edit, None); }
2281
2282 #[test]
2283 fn test_build_version_completion_sort_order() {
2284 let v1 = MockVersion {
2285 version: "1.0.0".into(),
2286 yanked: false,
2287 prerelease: false,
2288 };
2289 let v2 = MockVersion {
2290 version: "0.9.0".into(),
2291 yanked: false,
2292 prerelease: false,
2293 };
2294 let v3 = MockVersion {
2295 version: "0.8.0".into(),
2296 yanked: false,
2297 prerelease: false,
2298 };
2299
2300 let display_item1 = VersionDisplayItem::new(&v1, &pkg("test"), 0, true);
2301 let display_item2 = VersionDisplayItem::new(&v2, &pkg("test"), 1, false);
2302 let display_item3 = VersionDisplayItem::new(&v3, &pkg("test"), 2, false);
2303 let now = PublishTime::now();
2304 let item1 = build_version_completion(&display_item1, None, now, true);
2305 let item2 = build_version_completion(&display_item2, None, now, true);
2306 let item3 = build_version_completion(&display_item3, None, now, true);
2307
2308 assert_eq!(item1.sort_text.as_ref().unwrap(), "00000");
2310 assert_eq!(item2.sort_text.as_ref().unwrap(), "00001");
2311 assert_eq!(item3.sort_text.as_ref().unwrap(), "00002");
2312
2313 assert_eq!(item1.preselect, Some(true));
2315 assert_eq!(item2.preselect, Some(false));
2316 assert_eq!(item3.preselect, Some(false));
2317 }
2318
2319 #[test]
2320 fn test_version_completion_semantic_ordering() {
2321 let versions = [
2322 MockVersion {
2323 version: "0.14.0".into(),
2324 yanked: false,
2325 prerelease: false,
2326 },
2327 MockVersion {
2328 version: "0.8.0".into(),
2329 yanked: false,
2330 prerelease: false,
2331 },
2332 MockVersion {
2333 version: "0.2.0".into(),
2334 yanked: false,
2335 prerelease: false,
2336 },
2337 ];
2338
2339 let now = PublishTime::now();
2340 let items: Vec<_> = versions
2341 .iter()
2342 .enumerate()
2343 .map(|(idx, v)| {
2344 let display_item = VersionDisplayItem::new(v, &pkg("test"), idx, idx == 0);
2345 build_version_completion(&display_item, None, now, true)
2346 })
2347 .collect();
2348
2349 assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
2350 assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
2351 assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
2352
2353 let mut sorted_items = items;
2354 sorted_items.sort_by(|a, b| {
2355 a.sort_text
2356 .as_ref()
2357 .unwrap()
2358 .cmp(b.sort_text.as_ref().unwrap())
2359 });
2360
2361 assert_eq!(sorted_items[0].label, "0.14.0 (latest)");
2362 assert_eq!(sorted_items[1].label, "0.8.0");
2363 assert_eq!(sorted_items[2].label, "0.2.0");
2364 }
2365
2366 #[test]
2367 fn test_version_completion_index_ordering() {
2368 let versions = ["1.20.0", "1.9.0", "1.2.0", "0.99.0", "0.50.0"];
2369
2370 let now = PublishTime::now();
2371 let items: Vec<_> = versions
2372 .iter()
2373 .enumerate()
2374 .map(|(idx, ver)| {
2375 let v = MockVersion {
2376 version: (*ver).into(),
2377 yanked: false,
2378 prerelease: false,
2379 };
2380 let display_item = VersionDisplayItem::new(&v, &pkg("test"), idx, idx == 0);
2381 build_version_completion(&display_item, None, now, true)
2382 })
2383 .collect();
2384
2385 assert_eq!(items[0].sort_text.as_ref().unwrap(), "00000");
2386 assert_eq!(items[1].sort_text.as_ref().unwrap(), "00001");
2387 assert_eq!(items[2].sort_text.as_ref().unwrap(), "00002");
2388 assert_eq!(items[3].sort_text.as_ref().unwrap(), "00003");
2389 assert_eq!(items[4].sort_text.as_ref().unwrap(), "00004");
2390
2391 let mut sorted_items = items;
2392 sorted_items.sort_by(|a, b| {
2393 a.sort_text
2394 .as_ref()
2395 .unwrap()
2396 .cmp(b.sort_text.as_ref().unwrap())
2397 });
2398
2399 assert_eq!(sorted_items[0].label, "1.20.0 (latest)");
2400 assert_eq!(sorted_items[1].label, "1.9.0");
2401 assert_eq!(sorted_items[2].label, "1.2.0");
2402 assert_eq!(sorted_items[3].label, "0.99.0");
2403 assert_eq!(sorted_items[4].label, "0.50.0");
2404 }
2405
2406 #[test]
2407 fn test_version_display_item_latest() {
2408 let version = MockVersion {
2409 version: "1.0.0".into(),
2410 yanked: false,
2411 prerelease: false,
2412 };
2413
2414 let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
2415
2416 assert_eq!(item.version, "1.0.0");
2417 assert_eq!(item.label, "1.0.0 (latest)");
2418 assert_eq!(item.description, "Update serde to 1.0.0");
2419 assert_eq!(item.index, 0);
2420 assert!(item.is_latest);
2421 }
2422
2423 #[test]
2424 fn test_version_display_item_not_latest() {
2425 let version = MockVersion {
2426 version: "0.9.0".into(),
2427 yanked: false,
2428 prerelease: false,
2429 };
2430
2431 let item = VersionDisplayItem::new(&version, &pkg("tokio"), 1, false);
2432
2433 assert_eq!(item.version, "0.9.0");
2434 assert_eq!(item.label, "0.9.0");
2435 assert_eq!(item.description, "Update tokio to 0.9.0");
2436 assert_eq!(item.index, 1);
2437 assert!(!item.is_latest);
2438 }
2439
2440 #[test]
2441 fn test_prepare_version_display_items_filters_yanked() {
2442 let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
2443 std::sync::Arc::new(MockVersion {
2444 version: "1.0.0".into(),
2445 yanked: false,
2446 prerelease: false,
2447 }),
2448 std::sync::Arc::new(MockVersion {
2449 version: "0.9.0".into(),
2450 yanked: true,
2451 prerelease: false,
2452 }),
2453 std::sync::Arc::new(MockVersion {
2454 version: "0.8.0".into(),
2455 yanked: false,
2456 prerelease: false,
2457 }),
2458 ];
2459
2460 let items = prepare_version_display_items(&versions, &pkg("test"));
2461
2462 assert_eq!(items.len(), 2);
2463 assert_eq!(items[0].version, "1.0.0");
2464 assert_eq!(items[0].label, "1.0.0 (latest)");
2465 assert!(items[0].is_latest);
2466 assert_eq!(items[1].version, "0.8.0");
2467 assert_eq!(items[1].label, "0.8.0");
2468 assert!(!items[1].is_latest);
2469 }
2470
2471 #[test]
2472 fn test_prepare_version_display_items_limits_to_5() {
2473 let versions: Vec<std::sync::Arc<dyn crate::Version>> = (0..10)
2474 .map(|i| {
2475 std::sync::Arc::new(MockVersion {
2476 version: format!("1.0.{}", i).into(),
2477 yanked: false,
2478 prerelease: false,
2479 }) as std::sync::Arc<dyn crate::Version>
2480 })
2481 .collect();
2482
2483 let items = prepare_version_display_items(&versions, &pkg("test"));
2484
2485 assert_eq!(items.len(), 5);
2486 assert_eq!(items[0].version, "1.0.0");
2487 assert_eq!(items[0].label, "1.0.0 (latest)");
2488 assert_eq!(items[4].version, "1.0.4");
2489 assert_eq!(items[4].label, "1.0.4");
2490 }
2491
2492 #[test]
2493 fn test_prepare_version_display_items_empty() {
2494 let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![];
2495
2496 let items = prepare_version_display_items(&versions, &pkg("test"));
2497
2498 assert_eq!(items.len(), 0);
2499 }
2500
2501 #[test]
2502 fn test_prepare_version_display_items_all_yanked() {
2503 let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
2504 std::sync::Arc::new(MockVersion {
2505 version: "1.0.0".into(),
2506 yanked: true,
2507 prerelease: false,
2508 }),
2509 std::sync::Arc::new(MockVersion {
2510 version: "0.9.0".into(),
2511 yanked: true,
2512 prerelease: false,
2513 }),
2514 ];
2515
2516 let items = prepare_version_display_items(&versions, &pkg("test"));
2517
2518 assert_eq!(items.len(), 0);
2519 }
2520
2521 #[test]
2522 fn test_build_feature_completion() {
2523 let item = build_feature_completion("derive", &pkg("serde"), None);
2524
2525 assert_eq!(item.label, "derive");
2526 assert_eq!(item.kind, Some(CompletionItemKind::PROPERTY));
2527 assert_eq!(item.detail, Some("Feature of serde".to_string()));
2528 assert!(item.documentation.is_none());
2529 assert!(item.text_edit.is_none());
2530 assert_eq!(item.sort_text, Some("derive".to_string()));
2531 }
2532
2533 #[test]
2534 fn test_build_feature_completion_with_range() {
2535 let range = Range::default();
2536 let item = build_feature_completion("derive", &pkg("serde"), Some(range));
2537
2538 assert_eq!(item.label, "derive");
2539 assert!(item.text_edit.is_some());
2540 }
2541
2542 #[test]
2543 fn test_position_in_range_within() {
2544 let range = Range {
2545 start: Position {
2546 line: 0,
2547 character: 5,
2548 },
2549 end: Position {
2550 line: 0,
2551 character: 10,
2552 },
2553 };
2554
2555 let position = Position {
2556 line: 0,
2557 character: 7,
2558 };
2559
2560 assert!(position_in_range(position, range));
2561 }
2562
2563 #[test]
2564 fn test_position_in_range_at_start() {
2565 let range = Range {
2566 start: Position {
2567 line: 0,
2568 character: 5,
2569 },
2570 end: Position {
2571 line: 0,
2572 character: 10,
2573 },
2574 };
2575
2576 let position = Position {
2577 line: 0,
2578 character: 5,
2579 };
2580
2581 assert!(position_in_range(position, range));
2582 }
2583
2584 #[test]
2585 fn test_position_in_range_at_end() {
2586 let range = Range {
2587 start: Position {
2588 line: 0,
2589 character: 5,
2590 },
2591 end: Position {
2592 line: 0,
2593 character: 10,
2594 },
2595 };
2596
2597 let position = Position {
2598 line: 0,
2599 character: 10,
2600 };
2601
2602 assert!(position_in_range(position, range));
2603 }
2604
2605 #[test]
2606 fn test_position_in_range_one_past_end() {
2607 let range = Range {
2608 start: Position {
2609 line: 0,
2610 character: 5,
2611 },
2612 end: Position {
2613 line: 0,
2614 character: 10,
2615 },
2616 };
2617
2618 let position = Position {
2620 line: 0,
2621 character: 11,
2622 };
2623
2624 assert!(position_in_range(position, range));
2625 }
2626
2627 #[test]
2628 fn test_position_in_range_before() {
2629 let range = Range {
2630 start: Position {
2631 line: 0,
2632 character: 5,
2633 },
2634 end: Position {
2635 line: 0,
2636 character: 10,
2637 },
2638 };
2639
2640 let position = Position {
2641 line: 0,
2642 character: 4,
2643 };
2644
2645 assert!(!position_in_range(position, range));
2646 }
2647
2648 #[test]
2649 fn test_position_in_range_after() {
2650 let range = Range {
2651 start: Position {
2652 line: 0,
2653 character: 5,
2654 },
2655 end: Position {
2656 line: 0,
2657 character: 10,
2658 },
2659 };
2660
2661 let position = Position {
2662 line: 0,
2663 character: 12,
2664 };
2665
2666 assert!(!position_in_range(position, range));
2667 }
2668
2669 #[test]
2672 fn test_utf16_to_byte_offset_ascii() {
2673 let s = "hello";
2674 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
2675 assert_eq!(utf16_to_byte_offset(s, 2), Some(2));
2676 assert_eq!(utf16_to_byte_offset(s, 5), Some(5));
2677 }
2678
2679 #[test]
2680 fn test_utf16_to_byte_offset_multibyte() {
2681 let s = "日本語";
2683 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
2684 assert_eq!(utf16_to_byte_offset(s, 1), Some(3));
2685 assert_eq!(utf16_to_byte_offset(s, 2), Some(6));
2686 assert_eq!(utf16_to_byte_offset(s, 3), Some(9));
2687 }
2688
2689 #[test]
2690 fn test_utf16_to_byte_offset_emoji() {
2691 let s = "😀test";
2693 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
2694 assert_eq!(utf16_to_byte_offset(s, 2), Some(4)); assert_eq!(utf16_to_byte_offset(s, 3), Some(5)); }
2697
2698 #[test]
2699 fn test_utf16_to_byte_offset_mixed() {
2700 let s = "hello 世界 😀!";
2702 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)); }
2708
2709 #[test]
2710 fn test_utf16_to_byte_offset_out_of_bounds() {
2711 let s = "hello";
2712 assert_eq!(utf16_to_byte_offset(s, 100), None);
2713 }
2714
2715 #[test]
2716 fn test_utf16_to_byte_offset_empty() {
2717 let s = "";
2718 assert_eq!(utf16_to_byte_offset(s, 0), Some(0));
2719 assert_eq!(utf16_to_byte_offset(s, 1), None);
2720 }
2721
2722 #[test]
2725 fn test_byte_to_utf16_offset_ascii() {
2726 let s = "hello";
2727 assert_eq!(byte_to_utf16_offset(s, 0), 0);
2728 assert_eq!(byte_to_utf16_offset(s, 2), 2);
2729 assert_eq!(byte_to_utf16_offset(s, 5), 5);
2730 }
2731
2732 #[test]
2733 fn test_byte_to_utf16_offset_multibyte() {
2734 let s = "日本語";
2736 assert_eq!(byte_to_utf16_offset(s, 0), 0);
2737 assert_eq!(byte_to_utf16_offset(s, 3), 1);
2738 assert_eq!(byte_to_utf16_offset(s, 6), 2);
2739 assert_eq!(byte_to_utf16_offset(s, 9), 3);
2740 }
2741
2742 #[test]
2743 fn test_byte_to_utf16_offset_emoji() {
2744 let s = "😀test";
2746 assert_eq!(byte_to_utf16_offset(s, 0), 0);
2747 assert_eq!(byte_to_utf16_offset(s, 4), 2); assert_eq!(byte_to_utf16_offset(s, 5), 3); }
2750
2751 #[test]
2752 fn test_byte_to_utf16_offset_mixed() {
2753 let s = "hello 世界 😀!";
2755 assert_eq!(byte_to_utf16_offset(s, 0), 0); assert_eq!(byte_to_utf16_offset(s, 6), 6); assert_eq!(byte_to_utf16_offset(s, 9), 7); assert_eq!(byte_to_utf16_offset(s, 13), 9); assert_eq!(byte_to_utf16_offset(s, 17), 11); }
2761
2762 #[test]
2763 fn test_byte_to_utf16_offset_empty() {
2764 let s = "";
2765 assert_eq!(byte_to_utf16_offset(s, 0), 0);
2766 }
2767
2768 #[test]
2771 fn test_build_package_completion_long_description_ascii() {
2772 let long_desc = "a".repeat(250);
2773 let metadata = MockMetadata {
2774 name: "test-pkg".into(),
2775 description: Some(long_desc),
2776 repository: None,
2777 documentation: None,
2778 latest_version: "1.0.0".into(),
2779 };
2780
2781 let range = Range::default();
2782 let item = build_package_completion(&metadata, range).unwrap();
2783
2784 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2785 let lines: Vec<_> = content.value.lines().collect();
2787 assert!(lines[2].ends_with("..."));
2788 assert!(lines[2].len() <= 203); } else {
2790 panic!("Expected MarkupContent documentation");
2791 }
2792 }
2793
2794 #[test]
2795 fn test_build_package_completion_long_description_unicode() {
2796 let mut long_desc = String::new();
2799 for _ in 0..67 {
2800 long_desc.push('日');
2801 }
2802
2803 let metadata = MockMetadata {
2804 name: "test-pkg".into(),
2805 description: Some(long_desc),
2806 repository: None,
2807 documentation: None,
2808 latest_version: "1.0.0".into(),
2809 };
2810
2811 let range = Range::default();
2812 let item = build_package_completion(&metadata, range).unwrap();
2813
2814 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2816 let lines: Vec<_> = content.value.lines().collect();
2817 assert!(lines[2].ends_with("..."));
2818 assert!(lines[2].is_char_boundary(lines[2].len()));
2820 } else {
2821 panic!("Expected MarkupContent documentation");
2822 }
2823 }
2824
2825 #[test]
2826 fn test_build_package_completion_long_description_emoji() {
2827 let long_desc = "😀".repeat(51);
2830
2831 let metadata = MockMetadata {
2832 name: "test-pkg".into(),
2833 description: Some(long_desc),
2834 repository: None,
2835 documentation: None,
2836 latest_version: "1.0.0".into(),
2837 };
2838
2839 let range = Range::default();
2840 let item = build_package_completion(&metadata, range).unwrap();
2841
2842 if let Some(Documentation::MarkupContent(content)) = item.documentation {
2844 let lines: Vec<_> = content.value.lines().collect();
2845 assert!(lines[2].ends_with("..."));
2846 assert!(lines[2].is_char_boundary(lines[2].len()));
2848 } else {
2849 panic!("Expected MarkupContent documentation");
2850 }
2851 }
2852
2853 #[test]
2854 fn test_extract_prefix_unicode_package_name() {
2855 let content = "日本語-crate = \"1.0\"";
2857 let position = Position {
2858 line: 0,
2859 character: 3, };
2861 let range = Range {
2862 start: Position {
2863 line: 0,
2864 character: 0,
2865 },
2866 end: Position {
2867 line: 0,
2868 character: 10,
2869 },
2870 };
2871
2872 let prefix = extract_prefix(content, position, range);
2873 assert_eq!(prefix, "日本語");
2874 }
2875
2876 #[test]
2877 fn test_extract_prefix_emoji_in_content() {
2878 let content = "emoji-😀-crate = \"1.0\"";
2880 let position = Position {
2881 line: 0,
2882 character: 8, };
2884 let range = Range {
2885 start: Position {
2886 line: 0,
2887 character: 0,
2888 },
2889 end: Position {
2890 line: 0,
2891 character: 14,
2892 },
2893 };
2894
2895 let prefix = extract_prefix(content, position, range);
2896 assert_eq!(prefix, "emoji-😀");
2897 }
2898
2899 #[tokio::test]
2902 async fn test_complete_versions_generic_operator_stripping() {
2903 let registry = MockRegistry {
2904 versions: vec![
2905 MockVersion {
2906 version: "1.0.0".into(),
2907 yanked: false,
2908 prerelease: false,
2909 },
2910 MockVersion {
2911 version: "1.0.1".into(),
2912 yanked: false,
2913 prerelease: false,
2914 },
2915 MockVersion {
2916 version: "1.1.0".into(),
2917 yanked: false,
2918 prerelease: false,
2919 },
2920 MockVersion {
2921 version: "2.0.0".into(),
2922 yanked: false,
2923 prerelease: false,
2924 },
2925 ],
2926 };
2927
2928 let items = complete_versions_generic(
2930 ®istry,
2931 &pkg("test-pkg"),
2932 "^1.0",
2933 &['^', '~', '=', '<', '>'],
2934 FreshnessSettings::default(),
2935 )
2936 .await;
2937
2938 assert_eq!(items.len(), 2);
2940 assert_eq!(items[0].label, "1.0.0 (latest)");
2941 assert_eq!(items[1].label, "1.0.1");
2942
2943 let items = complete_versions_generic(
2945 ®istry,
2946 &pkg("test-pkg"),
2947 "~1.1",
2948 &['^', '~', '=', '<', '>'],
2949 FreshnessSettings::default(),
2950 )
2951 .await;
2952
2953 assert_eq!(items.len(), 1);
2954 assert_eq!(items[0].label, "1.1.0 (latest)");
2955
2956 let items = complete_versions_generic(
2958 ®istry,
2959 &pkg("test-pkg"),
2960 "=2.0",
2961 &['^', '~', '=', '<', '>'],
2962 FreshnessSettings::default(),
2963 )
2964 .await;
2965
2966 assert_eq!(items.len(), 1);
2967 assert_eq!(items[0].label, "2.0.0 (latest)");
2968
2969 let items = complete_versions_generic(
2971 ®istry,
2972 &pkg("test-pkg"),
2973 "1.0",
2974 &['^', '~', '=', '<', '>'],
2975 FreshnessSettings::default(),
2976 )
2977 .await;
2978
2979 assert_eq!(items.len(), 2);
2980 assert_eq!(items[0].label, "1.0.0 (latest)");
2981 assert_eq!(items[1].label, "1.0.1");
2982 }
2983
2984 #[tokio::test]
2988 async fn test_complete_versions_generic_delegates_to_from_byte_identical() {
2989 let registry = MockRegistry {
2990 versions: vec![MockVersion {
2991 version: "1.0.0".into(),
2992 yanked: false,
2993 prerelease: false,
2994 }],
2995 };
2996
2997 let via_generic = complete_versions_generic(
2998 ®istry,
2999 &pkg("test-pkg"),
3000 "1.0",
3001 &[],
3002 FreshnessSettings::default(),
3003 )
3004 .await;
3005 let via_from = complete_versions_generic_from(
3006 ®istry,
3007 &pkg("test-pkg"),
3008 &crate::parser::DependencySource::Registry,
3009 "1.0",
3010 &[],
3011 FreshnessSettings::default(),
3012 )
3013 .await;
3014
3015 assert_eq!(via_generic.len(), via_from.len());
3016 assert_eq!(via_generic[0].label, via_from[0].label);
3017 }
3018
3019 struct RoutingMockRegistry;
3026
3027 impl crate::Registry for RoutingMockRegistry {
3028 fn get_versions<'a>(
3029 &'a self,
3030 _name: &'a crate::PackageName,
3031 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
3032 {
3033 Box::pin(async move {
3034 Ok(vec![Box::new(MockVersion {
3035 version: "9.9.9".into(),
3036 yanked: false,
3037 prerelease: false,
3038 }) as Box<dyn crate::Version>])
3039 })
3040 }
3041
3042 fn get_versions_from<'a>(
3043 &'a self,
3044 _name: &'a crate::PackageName,
3045 source: &'a crate::parser::DependencySource,
3046 _freshness: FreshnessSettings,
3047 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
3048 {
3049 let version = if matches!(
3050 source,
3051 crate::parser::DependencySource::AlternateRegistry { .. }
3052 ) {
3053 "2.0.0"
3054 } else {
3055 "1.0.0"
3056 };
3057 Box::pin(async move {
3058 Ok(vec![Box::new(MockVersion {
3059 version: version.into(),
3060 yanked: false,
3061 prerelease: false,
3062 }) as Box<dyn crate::Version>])
3063 })
3064 }
3065
3066 fn get_latest_matching<'a>(
3067 &'a self,
3068 _name: &'a crate::PackageName,
3069 _req: &'a crate::VersionReq,
3070 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Option<Box<dyn crate::Version>>>>
3071 {
3072 Box::pin(async move { Ok(None) })
3073 }
3074
3075 fn search<'a>(
3076 &'a self,
3077 _query: &'a str,
3078 _limit: usize,
3079 ) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
3080 {
3081 Box::pin(async move { Ok(vec![]) })
3082 }
3083
3084 fn as_any(&self) -> &dyn Any {
3085 self
3086 }
3087 }
3088
3089 #[tokio::test]
3090 async fn test_complete_versions_generic_from_routes_source_to_get_versions_from() {
3091 let registry = RoutingMockRegistry;
3092 let alternate = crate::parser::DependencySource::AlternateRegistry {
3093 index: "gitlab-ci:deadbeef".into(),
3094 mirrors_crates_io: false,
3095 };
3096
3097 let items = complete_versions_generic_from(
3098 ®istry,
3099 &pkg("test-pkg"),
3100 &alternate,
3101 "",
3102 &[],
3103 FreshnessSettings::default(),
3104 )
3105 .await;
3106 assert_eq!(items[0].label, "2.0.0 (latest)");
3107
3108 assert_ne!(items[0].label, "9.9.9 (latest)");
3111 }
3112
3113 #[tokio::test]
3114 async fn test_complete_versions_generic_fallback_when_no_prefix_match() {
3115 let registry = MockRegistry {
3116 versions: vec![
3117 MockVersion {
3118 version: "1.0.0".into(),
3119 yanked: false,
3120 prerelease: false,
3121 },
3122 MockVersion {
3123 version: "1.1.0".into(),
3124 yanked: false,
3125 prerelease: false,
3126 },
3127 MockVersion {
3128 version: "2.0.0".into(),
3129 yanked: false,
3130 prerelease: false,
3131 },
3132 MockVersion {
3133 version: "2.1.0".into(),
3134 yanked: true, prerelease: false,
3136 },
3137 ],
3138 };
3139
3140 let items = complete_versions_generic(
3142 ®istry,
3143 &pkg("test-pkg"),
3144 "3.0",
3145 &['^', '~', '=', '<', '>'],
3146 FreshnessSettings::default(),
3147 )
3148 .await;
3149
3150 assert_eq!(items.len(), 3);
3152 assert_eq!(items[0].label, "1.0.0 (latest)");
3153 assert_eq!(items[1].label, "1.1.0");
3154 assert_eq!(items[2].label, "2.0.0");
3155
3156 assert!(!items.iter().any(|item| item.label == "2.1.0"));
3158
3159 let items = complete_versions_generic(
3161 ®istry,
3162 &pkg("test-pkg"),
3163 "",
3164 &[],
3165 FreshnessSettings::default(),
3166 )
3167 .await;
3168
3169 assert_eq!(items.len(), 3);
3170 assert_eq!(items[0].label, "1.0.0 (latest)");
3171 assert_eq!(items[1].label, "1.1.0");
3172 assert_eq!(items[2].label, "2.0.0");
3173 }
3174
3175 #[tokio::test]
3176 async fn test_complete_versions_generic_filters_yanked_in_prefix_match() {
3177 let registry = MockRegistry {
3178 versions: vec![
3179 MockVersion {
3180 version: "1.0.0".into(),
3181 yanked: false,
3182 prerelease: false,
3183 },
3184 MockVersion {
3185 version: "1.0.1".into(),
3186 yanked: true, prerelease: false,
3188 },
3189 MockVersion {
3190 version: "1.0.2".into(),
3191 yanked: false,
3192 prerelease: false,
3193 },
3194 ],
3195 };
3196
3197 let items = complete_versions_generic(
3199 ®istry,
3200 &pkg("test-pkg"),
3201 "1.0",
3202 &[],
3203 FreshnessSettings::default(),
3204 )
3205 .await;
3206
3207 assert_eq!(items.len(), 2);
3209 assert_eq!(items[0].label, "1.0.0 (latest)");
3210 assert_eq!(items[1].label, "1.0.2");
3211
3212 assert!(!items.iter().any(|item| item.label == "1.0.1"));
3214 }
3215
3216 #[tokio::test]
3217 async fn test_complete_versions_generic_filters_unsafe_version_string() {
3218 let registry = MockRegistry {
3224 versions: vec![
3225 MockVersion {
3226 version: "1.0.0".into(),
3227 yanked: false,
3228 prerelease: false,
3229 },
3230 MockVersion {
3231 version: "1.0.1\", \"evil\": \"true".into(),
3232 yanked: false,
3233 prerelease: false,
3234 },
3235 ],
3236 };
3237
3238 let items = complete_versions_generic(
3239 ®istry,
3240 &pkg("test-pkg"),
3241 "1.0",
3242 &[],
3243 FreshnessSettings::default(),
3244 )
3245 .await;
3246
3247 assert!(
3248 !items.iter().any(|item| item.label.contains("evil")),
3249 "an unsafe version string must never be offered as a completion item: {items:?}"
3250 );
3251 assert!(
3252 items.iter().any(|item| item.label.starts_with("1.0.0")),
3253 "a safe version must still be offered: {items:?}"
3254 );
3255 }
3256
3257 #[tokio::test]
3258 async fn test_complete_versions_generic_limit_5() {
3259 let versions: Vec<_> = (0..10)
3261 .map(|i| MockVersion {
3262 version: format!("1.0.{}", i).into(),
3263 yanked: false,
3264 prerelease: false,
3265 })
3266 .collect();
3267
3268 let registry = MockRegistry { versions };
3269
3270 let items = complete_versions_generic(
3272 ®istry,
3273 &pkg("test-pkg"),
3274 "1.0",
3275 &[],
3276 FreshnessSettings::default(),
3277 )
3278 .await;
3279
3280 assert_eq!(items.len(), 5);
3281 assert_eq!(items[0].label, "1.0.0 (latest)");
3282 assert_eq!(items[4].label, "1.0.4");
3283 }
3284
3285 #[tokio::test]
3286 async fn test_complete_versions_generic_go_no_operators() {
3287 let registry = MockRegistry {
3288 versions: vec![
3289 MockVersion {
3290 version: "v1.9.0".into(),
3291 yanked: false,
3292 prerelease: false,
3293 },
3294 MockVersion {
3295 version: "v1.9.1".into(),
3296 yanked: false,
3297 prerelease: false,
3298 },
3299 MockVersion {
3300 version: "v1.10.0".into(),
3301 yanked: false,
3302 prerelease: false,
3303 },
3304 ],
3305 };
3306
3307 let items = complete_versions_generic(
3309 ®istry,
3310 &pkg("github.com/gin-gonic/gin"),
3311 "v1.9",
3312 &[],
3313 FreshnessSettings::default(),
3314 )
3315 .await;
3316
3317 assert_eq!(items.len(), 2);
3318 assert_eq!(items[0].label, "v1.9.0 (latest)");
3319 assert_eq!(items[1].label, "v1.9.1");
3320 }
3321
3322 fn make_dep_with_features_range(
3325 name: &str,
3326 name_range: Range,
3327 features_range: Range,
3328 ) -> MockDependency {
3329 MockDependency {
3330 name: name.into(),
3331 name_range,
3332 version_range: None,
3333 features_range: Some(features_range),
3334 }
3335 }
3336
3337 #[test]
3338 fn test_detect_feature_context_inline() {
3339 let features_range = Range {
3342 start: Position {
3343 line: 0,
3344 character: 36,
3345 },
3346 end: Position {
3347 line: 0,
3348 character: 52,
3349 },
3350 };
3351 let dep = make_dep_with_features_range(
3352 "serde",
3353 Range {
3354 start: Position {
3355 line: 0,
3356 character: 0,
3357 },
3358 end: Position {
3359 line: 0,
3360 character: 5,
3361 },
3362 },
3363 features_range,
3364 );
3365 let parse_result = MockParseResult {
3366 dependencies: vec![dep],
3367 };
3368
3369 let content = r#"serde = { version = "1", features = ["derive", "std"] }"#;
3370
3371 let position = Position {
3374 line: 0,
3375 character: 41,
3376 };
3377 let context = detect_completion_context(&parse_result, position, content);
3378 assert!(
3379 matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
3380 if package_name == "serde" && prefix == "der"),
3381 "Expected Feature context with prefix 'der', got {context:?}"
3382 );
3383 }
3384
3385 #[test]
3386 fn test_detect_feature_context_empty_prefix() {
3387 let features_range = Range {
3389 start: Position {
3390 line: 0,
3391 character: 11,
3392 },
3393 end: Position {
3394 line: 0,
3395 character: 15,
3396 },
3397 };
3398 let dep = make_dep_with_features_range(
3399 "tokio",
3400 Range {
3401 start: Position {
3402 line: 0,
3403 character: 0,
3404 },
3405 end: Position {
3406 line: 0,
3407 character: 5,
3408 },
3409 },
3410 features_range,
3411 );
3412 let parse_result = MockParseResult {
3413 dependencies: vec![dep],
3414 };
3415
3416 let content = r#"features = [""]"#;
3417 let position = Position {
3419 line: 0,
3420 character: 13,
3421 };
3422 let context = detect_completion_context(&parse_result, position, content);
3423 assert!(
3424 matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
3425 if package_name == "tokio" && prefix.is_empty()),
3426 "Expected Feature context with empty prefix, got {context:?}"
3427 );
3428 }
3429
3430 #[test]
3431 fn test_detect_feature_context_second_item() {
3432 let features_range = Range {
3434 start: Position {
3435 line: 0,
3436 character: 11,
3437 },
3438 end: Position {
3439 line: 0,
3440 character: 28,
3441 },
3442 };
3443 let dep = make_dep_with_features_range(
3444 "tokio",
3445 Range {
3446 start: Position {
3447 line: 0,
3448 character: 0,
3449 },
3450 end: Position {
3451 line: 0,
3452 character: 5,
3453 },
3454 },
3455 features_range,
3456 );
3457 let parse_result = MockParseResult {
3458 dependencies: vec![dep],
3459 };
3460
3461 let content = r#"features = ["full", "rt-"]"#;
3462 let position = Position {
3464 line: 0,
3465 character: 24,
3466 };
3467 let context = detect_completion_context(&parse_result, position, content);
3468 assert!(
3469 matches!(context, CompletionContext::Feature { ref package_name, ref prefix }
3470 if package_name == "tokio" && prefix == "rt-"),
3471 "Expected Feature context with prefix 'rt-', got {context:?}"
3472 );
3473 }
3474
3475 #[test]
3476 fn test_detect_no_feature_context_outside_range() {
3477 let features_range = Range {
3478 start: Position {
3479 line: 2,
3480 character: 11,
3481 },
3482 end: Position {
3483 line: 2,
3484 character: 20,
3485 },
3486 };
3487 let dep = make_dep_with_features_range(
3488 "serde",
3489 Range {
3490 start: Position {
3491 line: 2,
3492 character: 0,
3493 },
3494 end: Position {
3495 line: 2,
3496 character: 5,
3497 },
3498 },
3499 features_range,
3500 );
3501 let parse_result = MockParseResult {
3502 dependencies: vec![dep],
3503 };
3504
3505 let content = "[package]\nname = \"test\"\nfeatures = [\"full\"]";
3507 let position = Position {
3508 line: 0,
3509 character: 5,
3510 };
3511 let context = detect_completion_context(&parse_result, position, content);
3512 assert_eq!(context, CompletionContext::None);
3513 }
3514
3515 #[test]
3516 fn test_extract_feature_prefix_basic() {
3517 let content = r#"serde = { features = ["derive"] }"#;
3518 let position = Position {
3521 line: 0,
3522 character: 27,
3523 };
3524 let prefix = extract_feature_prefix(content, position);
3525 assert_eq!(prefix, "deri");
3526 }
3527
3528 #[test]
3529 fn test_extract_feature_prefix_empty() {
3530 let content = r#"features = [""]"#;
3531 let position = Position {
3533 line: 0,
3534 character: 13,
3535 };
3536 let prefix = extract_feature_prefix(content, position);
3537 assert_eq!(prefix, "");
3538 }
3539
3540 #[test]
3541 fn test_extract_feature_prefix_multiline() {
3542 let content = "features = [\n \"rt-multi-thread\",\n \"mac\"\n]";
3543 let position = Position {
3546 line: 2,
3547 character: 8,
3548 };
3549 let prefix = extract_feature_prefix(content, position);
3550 assert_eq!(prefix, "mac");
3551 }
3552
3553 #[test]
3554 fn test_extract_feature_prefix_no_quote() {
3555 let content = "features = [\n \n]";
3556 let position = Position {
3558 line: 1,
3559 character: 4,
3560 };
3561 let prefix = extract_feature_prefix(content, position);
3562 assert_eq!(prefix, "");
3563 }
3564
3565 #[test]
3566 fn test_extract_feature_prefix_between_items_no_quote() {
3567 let content = r#"features = ["full", ]"#;
3570 let position = Position {
3572 line: 0,
3573 character: 19,
3574 };
3575 let prefix = extract_feature_prefix(content, position);
3576 assert_eq!(prefix, "");
3577 }
3578
3579 #[test]
3580 fn test_extract_feature_prefix_cursor_after_opening_bracket() {
3581 let content = "features = []";
3583 let position = Position {
3584 line: 0,
3585 character: 12,
3586 };
3587 let prefix = extract_feature_prefix(content, position);
3588 assert_eq!(prefix, "");
3589 }
3590
3591 #[test]
3595 fn test_version_display_item_captures_published_at() {
3596 let version = MockVersionWithAge {
3597 version: "1.0.0".into(),
3598 published_at: Some(PublishTime::from_unix_secs(1_000)),
3599 };
3600
3601 let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
3602
3603 assert_eq!(item.published_at, Some(PublishTime::from_unix_secs(1_000)));
3604 }
3605
3606 #[test]
3607 fn test_version_display_item_published_at_none_when_unavailable() {
3608 let version = MockVersion {
3611 version: "1.0.0".into(),
3612 yanked: false,
3613 prerelease: false,
3614 };
3615
3616 let item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
3617
3618 assert_eq!(item.published_at, None);
3619 }
3620
3621 #[test]
3622 fn test_build_version_completion_label_details_present_when_published_at_known() {
3623 let now = PublishTime::from_unix_secs(10_000);
3624 let published_two_hours_ago = PublishTime::from_unix_secs(10_000 - 2 * 3600);
3625 let version = MockVersionWithAge {
3626 version: "1.2.3".into(),
3627 published_at: Some(published_two_hours_ago),
3628 };
3629 let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
3630
3631 let item = build_version_completion(&display_item, None, now, true);
3632
3633 let details = item
3634 .label_details
3635 .expect("label_details must be set when published_at is known");
3636 assert_eq!(details.detail, Some(" 2 hours ago".to_string()));
3637 assert_eq!(details.description, None);
3638 }
3639
3640 #[test]
3641 fn test_build_version_completion_label_details_absent_when_freshness_disabled() {
3642 let now = PublishTime::from_unix_secs(10_000);
3645 let published_two_hours_ago = PublishTime::from_unix_secs(10_000 - 2 * 3600);
3646 let version = MockVersionWithAge {
3647 version: "1.2.3".into(),
3648 published_at: Some(published_two_hours_ago),
3649 };
3650 let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
3651
3652 let item = build_version_completion(&display_item, None, now, false);
3653
3654 assert!(item.label_details.is_none());
3655 }
3656
3657 #[test]
3658 fn test_build_version_completion_label_details_absent_when_published_at_unknown() {
3659 let version = MockVersion {
3660 version: "1.2.3".into(),
3661 yanked: false,
3662 prerelease: false,
3663 };
3664 let display_item = VersionDisplayItem::new(&version, &pkg("serde"), 0, true);
3665
3666 let item = build_version_completion(&display_item, None, PublishTime::now(), true);
3667
3668 assert!(item.label_details.is_none());
3669 }
3670
3671 #[test]
3676 fn test_build_version_completion_byte_identical_output_without_freshness_data() {
3677 let versions: Vec<std::sync::Arc<dyn crate::Version>> = vec![
3678 std::sync::Arc::new(MockVersion {
3679 version: "1.0.0".into(),
3680 yanked: false,
3681 prerelease: false,
3682 }),
3683 std::sync::Arc::new(MockVersion {
3684 version: "0.9.0".into(),
3685 yanked: true,
3686 prerelease: false,
3687 }),
3688 std::sync::Arc::new(MockVersion {
3689 version: "0.8.0".into(),
3690 yanked: false,
3691 prerelease: false,
3692 }),
3693 ];
3694
3695 let display_items = prepare_version_display_items(&versions, &pkg("test"));
3696 assert_eq!(display_items.len(), 2, "yanked filtering must be unchanged");
3697
3698 let now = PublishTime::now();
3699 let items: Vec<_> = display_items
3700 .iter()
3701 .map(|item| build_version_completion(item, None, now, true))
3702 .collect();
3703
3704 assert_eq!(items[0].label, "1.0.0 (latest)");
3705 assert_eq!(items[0].sort_text, Some("00000".to_string()));
3706 assert_eq!(items[0].preselect, Some(true));
3707 assert_eq!(items[0].label_details, None);
3708
3709 assert_eq!(items[1].label, "0.8.0");
3710 assert_eq!(items[1].sort_text, Some("00001".to_string()));
3711 assert_eq!(items[1].preselect, Some(false));
3712 assert_eq!(items[1].label_details, None);
3713 }
3714}