1use crate::config::DepsConfig;
6use crate::document::{ServerState, ensure_document_loaded};
7use deps_core::EcosystemId;
8use deps_core::completion::COMPLETION_SEARCH_TIMEOUT;
9use deps_core::{
10 is_safe_maven_coordinate_segment, is_safe_package_name, is_safe_registry_url,
11 is_safe_version_string, lsp_helpers::warn_rejected_value,
12};
13use std::sync::Arc;
14use tokio::sync::RwLock;
15use tower_lsp_server::Client;
16use tower_lsp_server::ls_types::{
17 CompletionItem, CompletionItemKind, CompletionList, CompletionParams, CompletionResponse,
18 InsertTextFormat,
19};
20
21pub async fn handle_completion(
35 state: Arc<ServerState>,
36 params: CompletionParams,
37 client: Client,
38 config: Arc<RwLock<DepsConfig>>,
39) -> Option<CompletionResponse> {
40 let uri = ¶ms.text_document_position.text_document.uri;
41 let position = params.text_document_position.position;
42
43 tracing::info!(
44 "completion request: uri={:?}, line={}, character={}",
45 uri,
46 position.line,
47 position.character
48 );
49
50 let freshness = { config.read().await.freshness.to_settings() };
53
54 let package_search_is_incomplete = state
61 .ecosystem_registry
62 .get_for_uri(uri)
63 .is_some_and(|e| e.package_search_is_incomplete());
64
65 let context_less_response = || {
73 if package_search_is_incomplete {
74 Some(CompletionResponse::List(CompletionList {
75 is_incomplete: true,
76 items: vec![],
77 }))
78 } else {
79 None
80 }
81 };
82
83 if state.get_document(uri).is_none() {
86 tracing::info!("completion: document not loaded, loading from disk");
87
88 let load_result = tokio::time::timeout(
90 std::time::Duration::from_millis(200),
91 ensure_document_loaded(uri, Arc::clone(&state), client.clone(), Arc::clone(&config)),
92 )
93 .await;
94
95 match load_result {
96 Ok(true) => {
97 tracing::debug!("completion: document loaded successfully");
99 }
100 Ok(false) | Err(_) => {
101 tracing::warn!("completion: document load failed or timed out");
103 return context_less_response();
104 }
105 }
106 }
107
108 let Some((ecosystem_id, ecosystem_kind, content, parse_result)) =
118 state.with_document(uri, |doc| {
119 (
120 doc.ecosystem_id(),
121 doc.ecosystem,
122 doc.content.clone(),
123 doc.parse_result_arc(),
124 )
125 })
126 else {
127 tracing::warn!("completion: document not found: {:?}", uri);
128 return context_less_response();
129 };
130
131 tracing::info!(
132 "completion: ecosystem={}, has_parse_result={}",
133 ecosystem_id,
134 parse_result.is_some()
135 );
136
137 let (items, is_incomplete) = if let Some(parse_result) = parse_result {
147 match state.ecosystem_registry.get(ecosystem_id) {
148 Some(ecosystem) => {
149 let completion_result = tokio::time::timeout(
155 COMPLETION_SEARCH_TIMEOUT,
156 ecosystem.generate_completions(
157 parse_result.as_ref(),
158 position,
159 &content,
160 freshness,
161 ),
162 )
163 .await;
164
165 match completion_result {
166 Ok(completions) if completions.items.is_empty() => {
169 tracing::info!("completion: ecosystem returned empty, trying fallback");
170 let fallback_items =
171 fallback_completion(&state, ecosystem_kind, position, &content).await;
172 (
173 fallback_items,
174 completions.is_incomplete || ecosystem.package_search_is_incomplete(),
175 )
176 }
177 Ok(completions) => (completions.items, completions.is_incomplete),
178 Err(_) => {
183 tracing::warn!(
184 "completion: generate_completions timed out after \
185 {}s, skipping fallback search",
186 COMPLETION_SEARCH_TIMEOUT.as_secs()
187 );
188 (vec![], false)
189 }
190 }
191 }
192 None => {
193 tracing::warn!("completion: ecosystem not found for id: {ecosystem_id}");
194 (vec![], false)
195 }
196 }
197 } else {
198 (
205 fallback_completion(&state, ecosystem_kind, position, &content).await,
206 package_search_is_incomplete,
207 )
208 };
209
210 tracing::info!("completion: returning {} items", items.len());
211
212 if is_incomplete {
213 Some(CompletionResponse::List(CompletionList {
218 is_incomplete: true,
219 items,
220 }))
221 } else if items.is_empty() {
222 None
223 } else {
224 Some(CompletionResponse::Array(items))
225 }
226}
227
228async fn fallback_completion(
232 state: &ServerState,
233 ecosystem_kind: EcosystemId,
234 position: tower_lsp_server::ls_types::Position,
235 content: &str,
236) -> Vec<CompletionItem> {
237 tracing::info!(
238 "fallback_completion: starting for ecosystem={}",
239 ecosystem_kind
240 );
241
242 let line = match content.lines().nth(position.line as usize) {
244 Some(l) => l,
245 None => {
246 tracing::info!("fallback_completion: line {} not found", position.line);
247 return vec![];
248 }
249 };
250
251 tracing::info!("fallback_completion: line content = {:?}", line);
252
253 if !is_in_dependencies_section(content, position.line as usize, ecosystem_kind) {
254 tracing::info!("fallback_completion: not in dependencies section");
255 return vec![];
256 }
257
258 let prefix = extract_prefix(line, position.character, ecosystem_kind);
260
261 tracing::info!("fallback_completion: prefix = {:?}", prefix);
262
263 if prefix.is_empty() || prefix.contains('=') || prefix.chars().count() < 2 {
267 tracing::info!("fallback_completion: prefix rejected (empty, contains =, or < 2 chars)");
268 return vec![];
269 }
270
271 let ecosystem = match state.ecosystem_registry.get(ecosystem_kind.id()) {
273 Some(e) => e,
274 None => return vec![],
275 };
276
277 let registry = ecosystem.registry();
278
279 search_packages(registry.as_ref(), ecosystem_kind, prefix).await
281}
282
283fn extract_prefix(line: &str, character: u32, ecosystem_kind: EcosystemId) -> &str {
303 let prefix_end =
304 deps_core::completion::utf16_to_byte_offset(line, character).unwrap_or(line.len());
305 let prefix = line[..prefix_end].trim();
306 if uses_json_quoted_keys(ecosystem_kind) || uses_toml_string_array_values(ecosystem_kind) {
307 prefix.trim_matches('"')
308 } else if uses_xml_tag_values(ecosystem_kind) {
309 strip_leading_xml_tag(prefix)
310 } else {
311 prefix
312 }
313}
314
315const fn uses_xml_tag_values(ecosystem_kind: EcosystemId) -> bool {
325 match ecosystem_kind {
326 EcosystemId::Maven => true,
327 EcosystemId::Npm
328 | EcosystemId::Composer
329 | EcosystemId::Cargo
330 | EcosystemId::Pypi
331 | EcosystemId::Go
332 | EcosystemId::Dart
333 | EcosystemId::Gradle
334 | EcosystemId::Swift
335 | EcosystemId::NuGet
336 | EcosystemId::Bundler
337 | EcosystemId::Deno
338 | EcosystemId::GithubActions
339 | EcosystemId::GitlabCi => false,
340 }
341}
342
343fn strip_leading_xml_tag(prefix: &str) -> &str {
359 prefix.rfind('>').map_or(prefix, |gt| &prefix[gt + 1..])
360}
361
362const fn uses_json_quoted_keys(ecosystem_kind: EcosystemId) -> bool {
385 match ecosystem_kind {
386 EcosystemId::Npm | EcosystemId::Composer => true,
387 EcosystemId::Cargo
388 | EcosystemId::Pypi
389 | EcosystemId::Go
390 | EcosystemId::Dart
391 | EcosystemId::Maven
392 | EcosystemId::Gradle
393 | EcosystemId::Swift
394 | EcosystemId::NuGet
395 | EcosystemId::Bundler
396 | EcosystemId::Deno
397 | EcosystemId::GithubActions
398 | EcosystemId::GitlabCi => false,
399 }
400}
401
402const fn uses_toml_string_array_values(ecosystem_kind: EcosystemId) -> bool {
414 match ecosystem_kind {
415 EcosystemId::Pypi => true,
416 EcosystemId::Cargo
417 | EcosystemId::Npm
418 | EcosystemId::Composer
419 | EcosystemId::Go
420 | EcosystemId::Dart
421 | EcosystemId::Maven
422 | EcosystemId::Gradle
423 | EcosystemId::Swift
424 | EcosystemId::NuGet
425 | EcosystemId::Bundler
426 | EcosystemId::Deno
427 | EcosystemId::GithubActions
428 | EcosystemId::GitlabCi => false,
429 }
430}
431
432fn is_in_dependencies_section(
439 content: &str,
440 line_number: usize,
441 ecosystem_id: EcosystemId,
442) -> bool {
443 match ecosystem_id {
444 EcosystemId::Cargo => is_in_toml_dependencies(content, line_number),
445 EcosystemId::Pypi => {
452 is_in_toml_dependencies(content, line_number)
453 || is_in_pypi_project_dependencies_array(content, line_number)
454 }
455 EcosystemId::Npm => is_in_json_dependencies(
456 content,
457 line_number,
458 &[
459 "dependencies",
460 "devDependencies",
461 "peerDependencies",
462 "optionalDependencies",
463 ],
464 ),
465 EcosystemId::Composer => {
466 is_in_json_dependencies(content, line_number, &["require", "require-dev"])
467 }
468 EcosystemId::Maven => is_in_xml_tag_section(content, line_number, "dependencies"),
469 EcosystemId::Go => is_in_go_require(content, line_number),
470 EcosystemId::Dart => is_in_yaml_dependencies(content, line_number),
471 EcosystemId::Bundler => false,
480 EcosystemId::Swift => false,
485 EcosystemId::Gradle => false,
489 EcosystemId::NuGet => false,
494 EcosystemId::Deno => is_in_json_dependencies(content, line_number, &["imports"]),
495 EcosystemId::GithubActions => is_github_actions_uses_line(content, line_number),
500 EcosystemId::GitlabCi => false,
505 }
506}
507
508fn is_github_actions_uses_line(content: &str, line_number: usize) -> bool {
511 content
512 .lines()
513 .nth(line_number)
514 .map(str::trim_start)
515 .is_some_and(|trimmed| trimmed.starts_with("uses:") || trimmed.starts_with("- uses:"))
516}
517
518fn is_in_toml_dependencies(content: &str, line_number: usize) -> bool {
523 let lines: Vec<_> = content.lines().enumerate().take(line_number + 1).collect();
526
527 for (_, line) in lines.iter().rev() {
528 let line = line.trim();
529
530 if line.starts_with('[') && line.ends_with(']') {
532 return line == "[dependencies]"
534 || line == "[dev-dependencies]"
535 || line == "[build-dependencies]"
536 || line == "[workspace.dependencies]"
537 || line == "[project.dependencies]"
538 || line == "[project.optional-dependencies]"
539 || line.starts_with("[target.")
540 && (line.contains(".dependencies]")
541 || line.contains(".dev-dependencies]")
542 || line.contains(".build-dependencies]"));
543 }
544 }
545
546 false
547}
548
549fn is_in_pypi_project_dependencies_array(content: &str, line_number: usize) -> bool {
564 let mut in_project = false;
565 let mut in_array = false;
566 let mut depth: i32 = 0;
567
568 for (i, line) in content.lines().enumerate() {
569 if i > line_number {
570 break;
571 }
572 let trimmed = strip_trailing_toml_comment(line.trim());
573
574 if trimmed.starts_with('[') && trimmed.ends_with(']') {
575 in_array = false;
576 in_project = trimmed == "[project]";
577 continue;
578 }
579
580 if !in_array && in_project && is_dependencies_array_start(trimmed) {
581 in_array = true;
582 depth = 0;
583 }
584
585 if in_array {
586 if i == line_number {
587 return true;
588 }
589 for ch in trimmed.chars() {
590 match ch {
591 '[' => depth += 1,
592 ']' => depth -= 1,
593 _ => {}
594 }
595 }
596 if depth <= 0 {
597 in_array = false;
598 }
599 }
600 }
601
602 false
603}
604
605fn strip_trailing_toml_comment(line: &str) -> &str {
613 let mut in_string: Option<char> = None;
614 for (idx, ch) in line.char_indices() {
615 match in_string {
616 Some(quote) if ch == quote => in_string = None,
617 Some(_) => {}
618 None if ch == '"' || ch == '\'' => in_string = Some(ch),
619 None if ch == '#' => return line[..idx].trim_end(),
620 None => {}
621 }
622 }
623 line
624}
625
626fn is_dependencies_array_start(trimmed: &str) -> bool {
630 trimmed
631 .strip_prefix("dependencies")
632 .map(str::trim_start)
633 .and_then(|rest| rest.strip_prefix('='))
634 .is_some_and(|rest| rest.trim_start().starts_with('['))
635}
636
637fn is_in_json_dependencies(content: &str, line_number: usize, keys: &[&str]) -> bool {
642 let mut in_dependencies = false;
643 let mut brace_depth = 0;
644 let needles: Vec<String> = keys.iter().map(|key| format!("\"{key}\":")).collect();
646
647 for (i, line) in content.lines().enumerate() {
648 if i > line_number {
650 break;
651 }
652
653 let trimmed = line.trim();
654
655 if trimmed.starts_with('"')
657 && needles
658 .iter()
659 .any(|needle| trimmed.contains(needle.as_str()))
660 {
661 in_dependencies = true;
662 brace_depth = 0;
663 }
664
665 if in_dependencies {
667 for ch in trimmed.chars() {
668 match ch {
669 '{' => brace_depth += 1,
670 '}' => {
671 brace_depth -= 1;
672 if brace_depth <= 0 {
674 in_dependencies = false;
675 }
676 }
677 _ => {}
678 }
679 }
680
681 if i == line_number && in_dependencies && brace_depth > 0 {
683 return true;
684 }
685 }
686 }
687
688 false
689}
690
691fn is_in_xml_tag_section(content: &str, line_number: usize, tag: &str) -> bool {
697 let open_prefix = format!("<{tag}");
698 let close = format!("</{tag}>");
699 let mut depth: usize = 0;
700
701 for (i, line) in content.lines().enumerate() {
702 if i > line_number {
703 break;
704 }
705
706 let opens_here = count_open_tags(line, &open_prefix);
707 depth += opens_here;
708 if i == line_number && opens_here > 0 {
712 return true;
713 }
714
715 depth = depth.saturating_sub(line.matches(close.as_str()).count());
716 if i == line_number && depth > 0 {
717 return true;
718 }
719 }
720
721 false
722}
723
724fn count_open_tags(line: &str, open_prefix: &str) -> usize {
728 let mut count = 0;
729 let mut search_from = 0;
730
731 while let Some(rel_idx) = line[search_from..].find(open_prefix) {
732 let idx = search_from + rel_idx;
733 let after = &line[idx + open_prefix.len()..];
734 if after.starts_with('>') || after.starts_with(char::is_whitespace) {
735 count += 1;
736 }
737 search_from = idx + open_prefix.len();
738 }
739
740 count
741}
742
743fn is_in_go_require(content: &str, line_number: usize) -> bool {
748 let mut in_require_block = false;
749
750 for (i, line) in content.lines().enumerate() {
751 if i > line_number {
752 break;
753 }
754
755 let trimmed = line.trim();
756 let is_block_start = trimmed
757 .strip_prefix("require")
758 .is_some_and(|rest| rest.trim_start().starts_with('('));
759
760 if is_block_start {
761 in_require_block = true;
762 } else if in_require_block && trimmed.starts_with(')') {
763 in_require_block = false;
764 }
765
766 if i == line_number {
767 return in_require_block || is_block_start || trimmed.starts_with("require ");
768 }
769 }
770
771 false
772}
773
774fn is_in_yaml_dependencies(content: &str, line_number: usize) -> bool {
780 const SECTION_KEYS: &[&str] = &[
781 "dependencies:",
782 "dev_dependencies:",
783 "dependency_overrides:",
784 ];
785 let mut in_dependencies = false;
786
787 for (i, line) in content.lines().enumerate() {
788 if i > line_number {
789 break;
790 }
791
792 let trimmed = line.trim_start();
793 if trimmed.is_empty() || trimmed.starts_with('#') {
794 continue;
795 }
796
797 if trimmed.len() == line.len() {
799 in_dependencies = SECTION_KEYS.iter().any(|key| trimmed.starts_with(key));
800 }
801 }
802
803 in_dependencies
804}
805
806async fn search_packages(
814 registry: &dyn deps_core::Registry,
815 ecosystem_id: EcosystemId,
816 query: &str,
817) -> Vec<CompletionItem> {
818 tracing::info!(
819 "search_packages: query={:?}, ecosystem={}",
820 query,
821 ecosystem_id
822 );
823
824 let results =
825 match tokio::time::timeout(COMPLETION_SEARCH_TIMEOUT, registry.search(query, 50)).await {
826 Ok(Ok(r)) => {
827 tracing::info!("search_packages: found {} results", r.len());
828 r
829 }
830 Ok(Err(e)) => {
831 tracing::warn!("search_packages: search failed: {}", e);
832 return vec![];
833 }
834 Err(_) => {
835 tracing::warn!(
836 "search_packages: timed out after {}s",
837 COMPLETION_SEARCH_TIMEOUT.as_secs()
838 );
839 return vec![];
840 }
841 };
842
843 results
845 .iter()
846 .filter_map(|metadata| create_package_completion_item(metadata.as_ref(), ecosystem_id))
847 .collect()
848}
849
850fn create_package_completion_item(
865 metadata: &dyn deps_core::Metadata,
866 ecosystem_id: EcosystemId,
867) -> Option<CompletionItem> {
868 let name = metadata.name();
869 let latest = metadata.latest_version().as_str();
870 let description = metadata.description();
871
872 if !is_safe_package_name(name.as_str()) {
873 warn_rejected_value(
874 "is_safe_package_name",
875 "package name completion item",
876 name.as_str(),
877 );
878 return None;
879 }
880
881 if !latest.is_empty() && !is_safe_version_string(latest) {
882 warn_rejected_value(
883 "is_safe_version_string",
884 "package name completion item",
885 latest,
886 );
887 return None;
888 }
889
890 let insert_text = match ecosystem_id {
891 EcosystemId::Cargo => format!("\"{name}\" = \"{latest}\""),
895 EcosystemId::Pypi => name.to_string(),
902 EcosystemId::Npm | EcosystemId::Composer => format!("\"{name}\": \"^{latest}\""),
903 EcosystemId::Go => format!("{name} {latest}"),
904 EcosystemId::Dart => format!("\"{name}\": ^{latest}"),
908 EcosystemId::Maven => {
909 let (group_id, artifact_id) = match name.as_str().split_once(':') {
913 Some((group_id, artifact_id)) => (Some(group_id), artifact_id),
914 None => (None, name.as_str()),
915 };
916 if !is_safe_maven_coordinate_segment(artifact_id) {
917 warn_rejected_value(
918 "is_safe_maven_coordinate_segment",
919 "maven package name completion item",
920 artifact_id,
921 );
922 return None;
923 }
924 if let Some(g) = group_id
925 && !is_safe_maven_coordinate_segment(g)
926 {
927 warn_rejected_value(
928 "is_safe_maven_coordinate_segment",
929 "maven package name completion item",
930 g,
931 );
932 return None;
933 }
934 group_id.map_or_else(
935 || format!("<artifactId>{artifact_id}</artifactId><version>{latest}</version>"),
936 |group_id| format!(
937 "<groupId>{group_id}</groupId><artifactId>{artifact_id}</artifactId><version>{latest}</version>"
938 ),
939 )
940 }
941 EcosystemId::Gradle => format!("implementation(\"{name}:{latest}\")"),
942 EcosystemId::Swift => {
943 let url = metadata
944 .repository()
945 .map_or_else(|| format!("https://github.com/{name}"), str::to_string);
946 if !is_safe_registry_url(&url) {
947 warn_rejected_value(
948 "is_safe_registry_url",
949 "swift package name completion item",
950 &url,
951 );
952 return None;
953 }
954 if latest.is_empty() {
955 format!(".package(url: \"{url}\")")
956 } else {
957 format!(".package(url: \"{url}\", from: \"{latest}\")")
958 }
959 }
960 EcosystemId::NuGet => {
961 format!("<PackageReference Include=\"{name}\" Version=\"{latest}\" />")
962 }
963 EcosystemId::Bundler => format!("gem \"{name}\", \"~> {latest}\""),
964 EcosystemId::Deno => {
965 let bare = name
968 .as_str()
969 .split_once(':')
970 .map_or(name.as_str(), |(_, rest)| rest);
971 if latest.is_empty() {
975 format!("\"{bare}\": \"{name}\"")
976 } else {
977 format!("\"{bare}\": \"{name}@^{latest}\"")
978 }
979 }
980 EcosystemId::GithubActions => {
989 if !name.as_str().contains('/') {
990 warn_rejected_value(
991 "owner/repo shape",
992 "github actions package name completion item",
993 name.as_str(),
994 );
995 return None;
996 }
997 if latest.is_empty() {
998 name.to_string()
999 } else {
1000 format!("{name}@{latest}")
1001 }
1002 }
1003 EcosystemId::GitlabCi => {
1010 if latest.is_empty() {
1011 name.to_string()
1012 } else {
1013 format!("{name}@{latest}")
1014 }
1015 }
1016 };
1017
1018 let detail = if latest.is_empty() {
1020 None
1021 } else {
1022 Some(format!("Latest: {latest}"))
1023 };
1024
1025 Some(CompletionItem {
1026 label: name.to_string(),
1027 kind: Some(CompletionItemKind::MODULE),
1028 detail,
1029 documentation: description
1030 .map(|d| tower_lsp_server::ls_types::Documentation::String(d.into())),
1031 insert_text: Some(insert_text),
1032 insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
1033 ..Default::default()
1034 })
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040 use crate::document::DocumentState;
1041 use crate::test_utils::test_helpers::create_test_client_and_config;
1042 use tower_lsp_server::ls_types::{
1043 Position, TextDocumentIdentifier, TextDocumentPositionParams,
1044 };
1045
1046 fn mock_ecosystem_state(
1050 id: &'static str,
1051 manifest_filename: &'static str,
1052 registry: Arc<dyn deps_core::Registry>,
1053 ) -> ServerState {
1054 use deps_core::{
1055 DiagnosticMessages, DiagnosticPolicy, Ecosystem, EcosystemFormatter, OsvNaming,
1056 PackageNaming, PackageRendering, ParseResult, RequirementResolution, SourcePolicy,
1057 };
1058 use std::any::Any;
1059 use tower_lsp_server::ls_types::Uri;
1060
1061 struct MockFormatter;
1062 impl PackageNaming for MockFormatter {}
1063
1064 impl PackageRendering for MockFormatter {
1065 fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
1066 version.to_string()
1067 }
1068
1069 fn package_url(&self, name: &deps_core::PackageName) -> String {
1070 format!("https://example.com/{name}")
1071 }
1072 }
1073
1074 impl RequirementResolution for MockFormatter {}
1075
1076 impl DiagnosticMessages for MockFormatter {}
1077
1078 impl DiagnosticPolicy for MockFormatter {}
1079
1080 impl SourcePolicy for MockFormatter {}
1081
1082 impl OsvNaming for MockFormatter {}
1083
1084 struct MockEcosystem {
1085 id: &'static str,
1086 manifest_filename: &'static str,
1087 registry: Arc<dyn deps_core::Registry>,
1088 }
1089 impl deps_core::ecosystem::private::Sealed for MockEcosystem {}
1090 impl Ecosystem for MockEcosystem {
1091 fn id(&self) -> &'static str {
1092 self.id
1093 }
1094 fn display_name(&self) -> &'static str {
1095 self.id
1096 }
1097 fn manifest_filenames(&self) -> &[&'static str] {
1098 std::slice::from_ref(&self.manifest_filename)
1099 }
1100 fn parse_manifest<'a>(
1101 &'a self,
1102 _content: &'a str,
1103 _uri: &'a Uri,
1104 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>>
1105 {
1106 Box::pin(async move { unimplemented!() })
1107 }
1108 fn registry(&self) -> Arc<dyn deps_core::Registry> {
1109 Arc::clone(&self.registry)
1110 }
1111 fn formatter(&self) -> &dyn EcosystemFormatter {
1112 &MockFormatter
1113 }
1114 fn generate_completions<'a>(
1115 &'a self,
1116 _parse_result: &'a dyn ParseResult,
1117 _position: tower_lsp_server::ls_types::Position,
1118 _content: &'a str,
1119 _freshness: deps_core::FreshnessSettings,
1120 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::completion::Completions>
1121 {
1122 Box::pin(async move { unimplemented!() })
1123 }
1124 fn as_any(&self) -> &dyn Any {
1125 self
1126 }
1127 }
1128
1129 let state = ServerState::new();
1130 state.ecosystem_registry.register(Arc::new(MockEcosystem {
1131 id,
1132 manifest_filename,
1133 registry,
1134 }));
1135 state
1136 }
1137
1138 fn mock_cargo_state(registry: Arc<dyn deps_core::Registry>) -> ServerState {
1142 mock_ecosystem_state("cargo", "Cargo.toml", registry)
1143 }
1144
1145 fn mock_maven_state(registry: Arc<dyn deps_core::Registry>) -> ServerState {
1147 mock_ecosystem_state("maven", "pom.xml", registry)
1148 }
1149
1150 fn mock_pypi_state(registry: Arc<dyn deps_core::Registry>) -> ServerState {
1152 mock_ecosystem_state("pypi", "pyproject.toml", registry)
1153 }
1154
1155 #[tokio::test]
1156 async fn test_completion_returns_empty_for_missing_document() {
1157 let state = Arc::new(ServerState::new());
1158 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1159
1160 let params = CompletionParams {
1161 text_document_position: TextDocumentPositionParams {
1162 text_document: TextDocumentIdentifier { uri },
1163 position: Position::new(0, 0),
1164 },
1165 work_done_progress_params: Default::default(),
1166 partial_result_params: Default::default(),
1167 context: None,
1168 };
1169
1170 let (client, config) = create_test_client_and_config();
1171 let result = handle_completion(state, params, client, config).await;
1172 assert!(result.is_none());
1175 }
1176
1177 #[tokio::test]
1187 async fn test_completion_missing_document_reports_incomplete_for_flagged_ecosystem() {
1188 use deps_core::completion::Completions;
1189 use deps_core::ecosystem::private::Sealed;
1190 use deps_core::{
1191 DiagnosticMessages, DiagnosticPolicy, Ecosystem, EcosystemFormatter, Metadata,
1192 OsvNaming, PackageNaming, PackageRendering, ParseResult, Registry,
1193 RequirementResolution, SourcePolicy, Version,
1194 };
1195 use std::any::Any;
1196 use tower_lsp_server::ls_types::Uri;
1197
1198 struct NoopRegistry;
1199 impl Registry for NoopRegistry {
1200 fn get_versions<'a>(
1201 &'a self,
1202 _name: &'a deps_core::PackageName,
1203 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
1204 {
1205 Box::pin(async move { Ok(vec![]) })
1206 }
1207 fn get_latest_matching<'a>(
1208 &'a self,
1209 _name: &'a deps_core::PackageName,
1210 _req: &'a deps_core::VersionReq,
1211 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
1212 {
1213 Box::pin(async move { Ok(None) })
1214 }
1215 fn search<'a>(
1216 &'a self,
1217 _query: &'a str,
1218 _limit: usize,
1219 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
1220 {
1221 Box::pin(async move { Ok(vec![]) })
1222 }
1223 fn as_any(&self) -> &dyn Any {
1224 self
1225 }
1226 }
1227
1228 struct NoopFormatter;
1229 impl PackageNaming for NoopFormatter {}
1230
1231 impl PackageRendering for NoopFormatter {
1232 fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
1233 version.to_string()
1234 }
1235
1236 fn package_url(&self, name: &deps_core::PackageName) -> String {
1237 format!("https://example.com/{name}")
1238 }
1239 }
1240
1241 impl RequirementResolution for NoopFormatter {}
1242
1243 impl DiagnosticMessages for NoopFormatter {}
1244
1245 impl DiagnosticPolicy for NoopFormatter {}
1246
1247 impl SourcePolicy for NoopFormatter {}
1248
1249 impl OsvNaming for NoopFormatter {}
1250
1251 struct IncompleteEcosystem;
1255 impl Sealed for IncompleteEcosystem {}
1256 impl Ecosystem for IncompleteEcosystem {
1257 fn id(&self) -> &'static str {
1258 "cargo"
1259 }
1260 fn display_name(&self) -> &'static str {
1261 "cargo"
1262 }
1263 fn manifest_filenames(&self) -> &[&'static str] {
1264 &["Cargo.toml"]
1265 }
1266 fn parse_manifest<'a>(
1267 &'a self,
1268 _content: &'a str,
1269 _uri: &'a Uri,
1270 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>>
1271 {
1272 Box::pin(async move { unimplemented!() })
1273 }
1274 fn registry(&self) -> Arc<dyn Registry> {
1275 Arc::new(NoopRegistry)
1276 }
1277 fn formatter(&self) -> &dyn EcosystemFormatter {
1278 &NoopFormatter
1279 }
1280 fn package_search_is_incomplete(&self) -> bool {
1281 true
1282 }
1283 fn generate_completions<'a>(
1284 &'a self,
1285 _parse_result: &'a dyn ParseResult,
1286 _position: tower_lsp_server::ls_types::Position,
1287 _content: &'a str,
1288 _freshness: deps_core::FreshnessSettings,
1289 ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
1290 Box::pin(async move { unimplemented!() })
1291 }
1292 fn as_any(&self) -> &dyn Any {
1293 self
1294 }
1295 }
1296
1297 let state = Arc::new(ServerState::new());
1298 state
1299 .ecosystem_registry
1300 .register(Arc::new(IncompleteEcosystem));
1301 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1305
1306 let params = CompletionParams {
1307 text_document_position: TextDocumentPositionParams {
1308 text_document: TextDocumentIdentifier { uri },
1309 position: Position::new(0, 0),
1310 },
1311 work_done_progress_params: Default::default(),
1312 partial_result_params: Default::default(),
1313 context: None,
1314 };
1315
1316 let (client, config) = create_test_client_and_config();
1317 let result = handle_completion(state, params, client, config).await;
1318 match result {
1319 Some(CompletionResponse::List(list)) => {
1320 assert!(list.is_incomplete);
1321 assert!(list.items.is_empty());
1322 }
1323 other => panic!("expected List{{is_incomplete:true, items:[]}}, got {other:?}"),
1324 }
1325 }
1326
1327 #[tokio::test]
1328 async fn test_completion_delegates_to_ecosystem() {
1329 let state = Arc::new(ServerState::new());
1330 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1331
1332 let content = "[dependencies]\nserde = \"1.0\"".to_string();
1333
1334 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1336 let parse_result = ecosystem.parse_manifest(&content, &uri).await.unwrap();
1337
1338 let doc = DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
1339 state.update_document(uri.clone(), doc);
1340
1341 let params = CompletionParams {
1342 text_document_position: TextDocumentPositionParams {
1343 text_document: TextDocumentIdentifier { uri },
1344 position: Position::new(1, 9),
1345 },
1346 work_done_progress_params: Default::default(),
1347 partial_result_params: Default::default(),
1348 context: None,
1349 };
1350
1351 let (client, config) = create_test_client_and_config();
1354 let _result = handle_completion(state, params, client, config).await;
1355 }
1357
1358 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1375 async fn test_concurrent_document_write_not_blocked_by_in_flight_completion_search() {
1376 use crate::test_utils::blocking_ecosystem::{
1377 BlockingEcosystem, BlockingHook, MockParseResult,
1378 };
1379 use deps_core::ParseResult;
1380 use tokio::sync::Barrier;
1381
1382 let state = Arc::new(ServerState::new());
1383 let started = Arc::new(Barrier::new(2));
1384 state
1385 .ecosystem_registry
1386 .register(Arc::new(BlockingEcosystem {
1387 started: Arc::clone(&started),
1388 hook: BlockingHook::Completions,
1389 }));
1390
1391 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1392 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1393 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
1394 let doc = DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
1395 state.update_document(uri.clone(), doc);
1396
1397 let (client, config) = create_test_client_and_config();
1398
1399 let completion_task = tokio::spawn({
1400 let state = Arc::clone(&state);
1401 let uri = uri.clone();
1402 async move {
1403 let params = CompletionParams {
1404 text_document_position: TextDocumentPositionParams {
1405 text_document: TextDocumentIdentifier { uri },
1406 position: Position::new(1, 9),
1407 },
1408 work_done_progress_params: Default::default(),
1409 partial_result_params: Default::default(),
1410 context: None,
1411 };
1412 handle_completion(state, params, client, config).await
1413 }
1414 });
1415
1416 started.wait().await;
1420
1421 let write_task = tokio::spawn({
1430 let state = Arc::clone(&state);
1431 let uri = uri.clone();
1432 async move {
1433 state.documents.get_mut(&uri).unwrap().set_loading();
1434 }
1435 });
1436 let write_result =
1437 tokio::time::timeout(std::time::Duration::from_millis(500), write_task).await;
1438
1439 completion_task.abort();
1440
1441 assert!(
1442 write_result.is_ok(),
1443 "#319 regression: a concurrent documents.get_mut on the same URI must not \
1444 block on an in-flight completion search — the DashMap shard Ref must be \
1445 dropped before the COMPLETION_SEARCH_TIMEOUT-bounded await, not after it"
1446 );
1447 }
1448
1449 #[tokio::test]
1458 async fn test_completion_freshness_enabled_live_reload_changes_label_details_on_next_request() {
1459 use deps_core::ecosystem::private::Sealed;
1460 use deps_core::{
1461 Dependency, DiagnosticMessages, DiagnosticPolicy, Ecosystem, EcosystemFormatter,
1462 Metadata, OsvNaming, PackageNaming, PackageRendering, ParseResult, Registry,
1463 RequirementResolution, SourcePolicy, Version,
1464 };
1465 use std::any::Any;
1466 use std::path::Path;
1467 use tower_lsp_server::ls_types::{CompletionItemLabelDetails, Uri};
1468
1469 struct NoopRegistry;
1470 impl Registry for NoopRegistry {
1471 fn get_versions<'a>(
1472 &'a self,
1473 _name: &'a deps_core::PackageName,
1474 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
1475 {
1476 Box::pin(async move { Ok(vec![]) })
1477 }
1478 fn get_latest_matching<'a>(
1479 &'a self,
1480 _name: &'a deps_core::PackageName,
1481 _req: &'a deps_core::VersionReq,
1482 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
1483 {
1484 Box::pin(async move { Ok(None) })
1485 }
1486 fn search<'a>(
1487 &'a self,
1488 _query: &'a str,
1489 _limit: usize,
1490 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
1491 {
1492 Box::pin(async move { Ok(vec![]) })
1493 }
1494 fn as_any(&self) -> &dyn Any {
1495 self
1496 }
1497 }
1498
1499 struct NoopFormatter;
1500 impl PackageNaming for NoopFormatter {}
1501
1502 impl PackageRendering for NoopFormatter {
1503 fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
1504 version.to_string()
1505 }
1506
1507 fn package_url(&self, name: &deps_core::PackageName) -> String {
1508 format!("https://example.com/{name}")
1509 }
1510 }
1511
1512 impl RequirementResolution for NoopFormatter {}
1513
1514 impl DiagnosticMessages for NoopFormatter {}
1515
1516 impl DiagnosticPolicy for NoopFormatter {}
1517
1518 impl SourcePolicy for NoopFormatter {}
1519
1520 impl OsvNaming for NoopFormatter {}
1521
1522 struct FreshnessEchoEcosystem;
1527 impl Sealed for FreshnessEchoEcosystem {}
1528 impl Ecosystem for FreshnessEchoEcosystem {
1529 fn id(&self) -> &'static str {
1530 "cargo"
1531 }
1532 fn display_name(&self) -> &'static str {
1533 "cargo"
1534 }
1535 fn manifest_filenames(&self) -> &[&'static str] {
1536 &["Cargo.toml"]
1537 }
1538 fn parse_manifest<'a>(
1539 &'a self,
1540 _content: &'a str,
1541 _uri: &'a Uri,
1542 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>>
1543 {
1544 Box::pin(async move { unimplemented!() })
1545 }
1546 fn registry(&self) -> Arc<dyn Registry> {
1547 Arc::new(NoopRegistry)
1548 }
1549 fn formatter(&self) -> &dyn EcosystemFormatter {
1550 &NoopFormatter
1551 }
1552 fn generate_completions<'a>(
1553 &'a self,
1554 _parse_result: &'a dyn ParseResult,
1555 _position: tower_lsp_server::ls_types::Position,
1556 _content: &'a str,
1557 freshness: deps_core::FreshnessSettings,
1558 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::completion::Completions>
1559 {
1560 Box::pin(async move {
1561 vec![CompletionItem {
1562 label: "1.0.0".to_string(),
1563 kind: Some(CompletionItemKind::VALUE),
1564 label_details: freshness.enabled.then(|| CompletionItemLabelDetails {
1565 detail: Some(" 1 hour ago".to_string()),
1566 description: None,
1567 }),
1568 ..Default::default()
1569 }]
1570 .into()
1571 })
1572 }
1573 fn as_any(&self) -> &dyn Any {
1574 self
1575 }
1576 }
1577
1578 struct MockParseResult {
1579 uri: Uri,
1580 }
1581 impl ParseResult for MockParseResult {
1582 fn dependencies(&self) -> Vec<&dyn Dependency> {
1583 vec![]
1584 }
1585 fn workspace_root(&self) -> Option<&Path> {
1586 None
1587 }
1588 fn uri(&self) -> &Uri {
1589 &self.uri
1590 }
1591 fn as_any(&self) -> &dyn Any {
1592 self
1593 }
1594 }
1595
1596 let state = Arc::new(ServerState::new());
1597 state
1599 .ecosystem_registry
1600 .register(Arc::new(FreshnessEchoEcosystem));
1601 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
1602
1603 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1604 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
1605 let doc = DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
1606 state.update_document(uri.clone(), doc);
1607
1608 let params = || CompletionParams {
1609 text_document_position: TextDocumentPositionParams {
1610 text_document: TextDocumentIdentifier { uri: uri.clone() },
1611 position: Position::new(0, 0),
1612 },
1613 work_done_progress_params: Default::default(),
1614 partial_result_params: Default::default(),
1615 context: None,
1616 };
1617
1618 let (client, config) = create_test_client_and_config();
1619 assert!(
1620 config.read().await.freshness.enabled,
1621 "default config ships freshness enabled"
1622 );
1623
1624 let before = handle_completion(
1625 Arc::clone(&state),
1626 params(),
1627 client.clone(),
1628 Arc::clone(&config),
1629 )
1630 .await
1631 .expect("completion response");
1632 let CompletionResponse::Array(items) = before else {
1633 panic!("expected an array response");
1634 };
1635 assert!(
1636 items[0].label_details.is_some(),
1637 "freshness enabled by default: label_details must be present"
1638 );
1639
1640 config.write().await.freshness.enabled = false;
1643
1644 let after = handle_completion(state, params(), client, config)
1645 .await
1646 .expect("completion response");
1647 let CompletionResponse::Array(items) = after else {
1648 panic!("expected an array response");
1649 };
1650 assert!(
1651 items[0].label_details.is_none(),
1652 "freshness disabled via live-reload: label_details must disappear on the very \
1653 next completion request"
1654 );
1655 }
1656
1657 #[test]
1658 fn test_is_in_toml_dependencies_basic() {
1659 let content = r#"
1660[package]
1661name = "test"
1662
1663[dependencies]
1664serde
1665"#;
1666 assert!(is_in_toml_dependencies(content, 5));
1667 assert!(!is_in_toml_dependencies(content, 1));
1668 }
1669
1670 #[test]
1671 fn test_is_in_toml_dependencies_dev_deps() {
1672 let content = r"
1673[dev-dependencies]
1674tokio
1675";
1676 assert!(is_in_toml_dependencies(content, 2));
1677 }
1678
1679 #[test]
1680 fn test_is_in_toml_dependencies_build_deps() {
1681 let content = r"
1682[build-dependencies]
1683cc
1684";
1685 assert!(is_in_toml_dependencies(content, 2));
1686 }
1687
1688 #[test]
1689 fn test_is_in_toml_dependencies_project_deps() {
1690 let content = r"
1691[project.dependencies]
1692requests
1693";
1694 assert!(is_in_toml_dependencies(content, 2));
1695 }
1696
1697 #[test]
1698 fn test_is_in_toml_dependencies_workspace_deps() {
1699 let content = r#"
1700[workspace.dependencies]
1701serde = "1.0"
1702"#;
1703 assert!(is_in_toml_dependencies(content, 2));
1704 }
1705
1706 #[test]
1707 fn test_is_in_toml_dependencies_target_specific() {
1708 let content = r"
1709[target.'cfg(windows)'.dependencies]
1710winapi
1711";
1712 assert!(is_in_toml_dependencies(content, 2));
1713 }
1714
1715 #[test]
1716 fn test_is_in_toml_dependencies_wrong_section() {
1717 let content = r#"
1718[package]
1719name = "test"
1720
1721[profile.release]
1722opt-level = 3
1723"#;
1724 assert!(!is_in_toml_dependencies(content, 2));
1725 assert!(!is_in_toml_dependencies(content, 5));
1726 }
1727
1728 #[test]
1729 fn test_is_in_toml_dependencies_multiple_sections() {
1730 let content = r#"
1731[dependencies]
1732serde = "1.0"
1733
1734[dev-dependencies]
1735tokio
1736"#;
1737 assert!(is_in_toml_dependencies(content, 2));
1738 assert!(is_in_toml_dependencies(content, 5));
1739 }
1740
1741 const NPM_KEYS: &[&str] = &[
1742 "dependencies",
1743 "devDependencies",
1744 "peerDependencies",
1745 "optionalDependencies",
1746 ];
1747
1748 #[test]
1749 fn test_is_in_json_dependencies_basic() {
1750 let content = r#"{
1751 "name": "test",
1752 "dependencies": {
1753 "express"
1754 }
1755}"#;
1756 assert!(is_in_json_dependencies(content, 3, NPM_KEYS));
1757 assert!(!is_in_json_dependencies(content, 1, NPM_KEYS));
1758 }
1759
1760 #[test]
1761 fn test_is_in_json_dependencies_dev_deps() {
1762 let content = r#"{
1763 "devDependencies": {
1764 "jest": "^29.0.0"
1765 }
1766}"#;
1767 assert!(is_in_json_dependencies(content, 2, NPM_KEYS));
1768 }
1769
1770 #[test]
1771 fn test_is_in_json_dependencies_peer_deps() {
1772 let content = r#"{
1773 "peerDependencies": {
1774 "react"
1775 }
1776}"#;
1777 assert!(is_in_json_dependencies(content, 2, NPM_KEYS));
1778 }
1779
1780 #[test]
1781 fn test_is_in_json_dependencies_optional_deps() {
1782 let content = r#"{
1783 "optionalDependencies": {
1784 "fsevents": "^2.0.0"
1785 }
1786}"#;
1787 assert!(is_in_json_dependencies(content, 2, NPM_KEYS));
1788 }
1789
1790 #[test]
1791 fn test_is_in_json_dependencies_outside_section() {
1792 let content = r#"{
1793 "name": "test",
1794 "dependencies": {
1795 "express": "^4.0.0"
1796 },
1797 "scripts": {
1798 "start": "node index.js"
1799 }
1800}"#;
1801 assert!(is_in_json_dependencies(content, 3, NPM_KEYS));
1802 assert!(!is_in_json_dependencies(content, 6, NPM_KEYS));
1803 }
1804
1805 #[test]
1806 fn test_is_in_json_dependencies_nested_braces() {
1807 let content = r#"{
1808 "dependencies": {
1809 "package": "1.0.0"
1810 }
1811}"#;
1812 assert!(is_in_json_dependencies(content, 2, NPM_KEYS));
1813 }
1814
1815 #[test]
1816 fn test_is_in_json_dependencies_custom_keys() {
1817 let content = r#"{
1818 "require": {
1819 "monolog/monolog": "^2.0"
1820 },
1821 "require-dev": {
1822 "phpunit/phpunit": "^9.0"
1823 }
1824}"#;
1825 assert!(is_in_json_dependencies(
1826 content,
1827 2,
1828 &["require", "require-dev"]
1829 ));
1830 assert!(is_in_json_dependencies(
1831 content,
1832 5,
1833 &["require", "require-dev"]
1834 ));
1835 }
1836
1837 #[test]
1838 fn test_is_in_dependencies_section_cargo() {
1839 let content = r"
1840[dependencies]
1841serde
1842";
1843 assert!(is_in_dependencies_section(content, 2, EcosystemId::Cargo));
1844 assert!(!is_in_dependencies_section(content, 0, EcosystemId::Cargo));
1845 }
1846
1847 #[test]
1848 fn test_is_in_dependencies_section_pypi() {
1849 let content = r"
1850[project.dependencies]
1851requests
1852";
1853 assert!(is_in_dependencies_section(content, 2, EcosystemId::Pypi));
1854 }
1855
1856 #[test]
1860 fn test_is_in_dependencies_section_pypi_project_array_no_literal_header() {
1861 let content = "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\ndependencies = [\n \"requests>=2.31.0\",\n \"flas\n]\n";
1862 assert!(is_in_dependencies_section(content, 5, EcosystemId::Pypi));
1864 assert!(is_in_dependencies_section(content, 4, EcosystemId::Pypi));
1866 assert!(!is_in_dependencies_section(content, 1, EcosystemId::Pypi));
1868 assert!(!is_in_dependencies_section(content, 2, EcosystemId::Pypi));
1869 assert!(!is_in_dependencies_section(content, 0, EcosystemId::Pypi));
1871 }
1872
1873 #[test]
1874 fn test_is_in_dependencies_section_pypi_project_array_single_line() {
1875 let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n";
1876 assert!(is_in_dependencies_section(content, 1, EcosystemId::Pypi));
1877 }
1878
1879 #[test]
1883 fn test_is_in_dependencies_section_pypi_project_array_scoped_to_project_table() {
1884 let content = "[tool.other]\ndependencies = [\n \"foo\n]\n";
1885 assert!(!is_in_dependencies_section(content, 2, EcosystemId::Pypi));
1886 }
1887
1888 #[test]
1894 fn test_is_in_dependencies_section_pypi_project_array_resyncs_after_unbalanced_extras_bracket()
1895 {
1896 let content = "[project]\ndependencies = [\n \"uvicorn[stan\n]\n\n[tool.pytest.ini_options]\naddopts = \"-v\"\n";
1897 assert!(is_in_dependencies_section(content, 2, EcosystemId::Pypi));
1899 assert!(!is_in_dependencies_section(content, 6, EcosystemId::Pypi));
1902 }
1903
1904 #[test]
1908 fn test_is_in_dependencies_section_pypi_project_array_ignores_bracket_in_comment() {
1909 let content = "[project]\ndependencies = [\n \"requests>=2.0.0\", # pinned per [PEP 621\n \"flas\n]\nrequires-python = \">=3.9\"\n";
1910 assert!(is_in_dependencies_section(content, 3, EcosystemId::Pypi));
1912 assert!(!is_in_dependencies_section(content, 5, EcosystemId::Pypi));
1914 }
1915
1916 #[test]
1919 fn test_is_in_dependencies_section_pypi_project_header_with_trailing_comment() {
1920 let content = "[project] # main metadata\ndependencies = [\n \"flas\n]\n";
1921 assert!(is_in_dependencies_section(content, 2, EcosystemId::Pypi));
1922 }
1923
1924 #[test]
1928 fn test_is_in_dependencies_section_pypi_project_state_cleared_by_commented_other_header() {
1929 let content = "[project]\nname = \"x\"\n\n[tool.hatch.envs.default] # test env\ndependencies = [\n \"other\n]\n";
1930 assert!(!is_in_dependencies_section(content, 5, EcosystemId::Pypi));
1931 }
1932
1933 #[test]
1934 fn test_is_in_dependencies_section_npm() {
1935 let content = r#"{
1936 "dependencies": {
1937 "express"
1938 }
1939}"#;
1940 assert!(is_in_dependencies_section(content, 2, EcosystemId::Npm));
1941 }
1942
1943 #[test]
1944 fn test_is_in_dependencies_section_composer() {
1945 let content = r#"{
1946 "require": {
1947 "monolog/monolog": "^2.0"
1948 },
1949 "scripts": {
1950 "test": "phpunit"
1951 }
1952}"#;
1953 assert!(is_in_dependencies_section(
1954 content,
1955 2,
1956 EcosystemId::Composer
1957 ));
1958 assert!(!is_in_dependencies_section(
1959 content,
1960 5,
1961 EcosystemId::Composer
1962 ));
1963 }
1964
1965 #[test]
1966 fn test_is_in_dependencies_section_maven() {
1967 let content = r"
1968<project>
1969 <dependencies>
1970 <dependency></dependency>
1971 </dependencies>
1972</project>
1973";
1974 assert!(is_in_dependencies_section(content, 3, EcosystemId::Maven));
1975 assert!(!is_in_dependencies_section(content, 1, EcosystemId::Maven));
1976 }
1977
1978 #[test]
1979 fn test_is_in_dependencies_section_maven_single_line() {
1980 let content = "<project><dependencies></dependencies></project>\n";
1981 assert!(is_in_dependencies_section(content, 0, EcosystemId::Maven));
1982 }
1983
1984 #[test]
1985 fn test_is_in_dependencies_section_maven_attributed_tag() {
1986 let content = r#"
1987<project>
1988 <dependencies xmlns="http://maven.apache.org/POM/4.0.0">
1989 <dependency></dependency>
1990 </dependencies>
1991</project>
1992"#;
1993 assert!(is_in_dependencies_section(content, 3, EcosystemId::Maven));
1994 }
1995
1996 #[test]
1997 fn test_is_in_dependencies_section_maven_no_false_positive_on_longer_tag_name() {
1998 let content = r"
1999<project>
2000 <dependencyManagement>
2001 <dependencies>
2002 <dependency></dependency>
2003 </dependencies>
2004 </dependencyManagement>
2005</project>
2006";
2007 assert!(!is_in_dependencies_section(content, 2, EcosystemId::Maven));
2009 assert!(is_in_dependencies_section(content, 4, EcosystemId::Maven));
2011 }
2012
2013 #[test]
2014 fn test_is_in_dependencies_section_go_single_line() {
2015 let content = "module example.com/myapp\n\nrequire github.com/gin-gonic/gin v1.9.1\n";
2016 assert!(is_in_dependencies_section(content, 2, EcosystemId::Go));
2017 assert!(!is_in_dependencies_section(content, 0, EcosystemId::Go));
2018 }
2019
2020 #[test]
2021 fn test_is_in_dependencies_section_go_block() {
2022 let content =
2023 "module example.com/myapp\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)\n";
2024 assert!(is_in_dependencies_section(content, 2, EcosystemId::Go));
2025 assert!(is_in_dependencies_section(content, 3, EcosystemId::Go));
2026 assert!(!is_in_dependencies_section(content, 4, EcosystemId::Go));
2027 }
2028
2029 #[test]
2030 fn test_is_in_dependencies_section_dart() {
2031 let content =
2032 "name: myapp\ndependencies:\n http: ^1.0.0\nenvironment:\n sdk: '>=3.0.0'\n";
2033 assert!(is_in_dependencies_section(content, 2, EcosystemId::Dart));
2034 assert!(!is_in_dependencies_section(content, 4, EcosystemId::Dart));
2035 }
2036
2037 #[test]
2038 fn test_is_in_dependencies_section_dart_column_zero_comment() {
2039 let content = "name: myapp\ndependencies:\n# a comment\n http: ^1.0.0\n";
2042 assert!(is_in_dependencies_section(content, 2, EcosystemId::Dart));
2043 assert!(is_in_dependencies_section(content, 3, EcosystemId::Dart));
2044 }
2045
2046 #[test]
2047 fn test_is_in_dependencies_section_deno() {
2048 let content = r#"{
2049 "name": "test",
2050 "imports": {
2051 "@std/fs": "jsr:@std/fs@^1.0"
2052 }
2053}"#;
2054 assert!(is_in_dependencies_section(content, 3, EcosystemId::Deno));
2055 assert!(!is_in_dependencies_section(content, 1, EcosystemId::Deno));
2056 }
2057
2058 #[test]
2059 fn test_is_in_dependencies_section_no_raw_text_boundary_ecosystems() {
2060 let content = "anything at all\n";
2065 assert!(!is_in_dependencies_section(
2066 content,
2067 0,
2068 EcosystemId::Bundler
2069 ));
2070 assert!(!is_in_dependencies_section(content, 0, EcosystemId::Swift));
2071 assert!(!is_in_dependencies_section(content, 0, EcosystemId::Gradle));
2072 assert!(!is_in_dependencies_section(content, 0, EcosystemId::NuGet));
2073 }
2074
2075 #[test]
2076 fn test_create_package_completion_item_cargo() {
2077 struct MockMetadata {
2078 name: deps_core::PackageName,
2079 }
2080 impl deps_core::Metadata for MockMetadata {
2081 fn name(&self) -> &deps_core::PackageName {
2082 &self.name
2083 }
2084 fn description(&self) -> Option<&str> {
2085 Some("A serialization framework")
2086 }
2087 fn repository(&self) -> Option<&str> {
2088 None
2089 }
2090 fn documentation(&self) -> Option<&str> {
2091 None
2092 }
2093 fn latest_version(&self) -> &deps_core::ConcreteVersion {
2094 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
2095 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("1.0.214"));
2096 &VERSION
2097 }
2098 fn as_any(&self) -> &dyn std::any::Any {
2099 self
2100 }
2101 }
2102
2103 let meta = MockMetadata {
2104 name: deps_core::PackageName::new("serde"),
2105 };
2106 let item = create_package_completion_item(&meta, EcosystemId::Cargo).unwrap();
2107
2108 assert_eq!(item.label, "serde");
2109 assert_eq!(item.kind, Some(CompletionItemKind::MODULE));
2110 assert_eq!(item.detail, Some("Latest: 1.0.214".to_string()));
2111 assert_eq!(
2112 item.insert_text,
2113 Some("\"serde\" = \"1.0.214\"".to_string())
2114 );
2115 assert_eq!(item.insert_text_format, Some(InsertTextFormat::PLAIN_TEXT));
2116 }
2117
2118 #[test]
2119 fn test_create_package_completion_item_npm() {
2120 struct MockMetadata {
2121 name: deps_core::PackageName,
2122 }
2123 impl deps_core::Metadata for MockMetadata {
2124 fn name(&self) -> &deps_core::PackageName {
2125 &self.name
2126 }
2127 fn description(&self) -> Option<&str> {
2128 Some("Fast web framework")
2129 }
2130 fn repository(&self) -> Option<&str> {
2131 None
2132 }
2133 fn documentation(&self) -> Option<&str> {
2134 None
2135 }
2136 fn latest_version(&self) -> &deps_core::ConcreteVersion {
2137 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
2138 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("4.18.2"));
2139 &VERSION
2140 }
2141 fn as_any(&self) -> &dyn std::any::Any {
2142 self
2143 }
2144 }
2145
2146 let meta = MockMetadata {
2147 name: deps_core::PackageName::new("express"),
2148 };
2149 let item = create_package_completion_item(&meta, EcosystemId::Npm).unwrap();
2150
2151 assert_eq!(item.label, "express");
2152 assert_eq!(
2153 item.insert_text,
2154 Some("\"express\": \"^4.18.2\"".to_string())
2155 );
2156 }
2157
2158 #[test]
2159 fn test_create_package_completion_item_pypi() {
2160 struct MockMetadata {
2161 name: deps_core::PackageName,
2162 }
2163 impl deps_core::Metadata for MockMetadata {
2164 fn name(&self) -> &deps_core::PackageName {
2165 &self.name
2166 }
2167 fn description(&self) -> Option<&str> {
2168 None
2169 }
2170 fn repository(&self) -> Option<&str> {
2171 None
2172 }
2173 fn documentation(&self) -> Option<&str> {
2174 None
2175 }
2176 fn latest_version(&self) -> &deps_core::ConcreteVersion {
2177 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
2178 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("2.31.0"));
2179 &VERSION
2180 }
2181 fn as_any(&self) -> &dyn std::any::Any {
2182 self
2183 }
2184 }
2185
2186 let meta = MockMetadata {
2187 name: deps_core::PackageName::new("requests"),
2188 };
2189 let item = create_package_completion_item(&meta, EcosystemId::Pypi).unwrap();
2190
2191 assert_eq!(item.label, "requests");
2192 assert_eq!(item.insert_text, Some("requests".to_string()));
2198 }
2199
2200 struct MockMetadata {
2201 name: deps_core::PackageName,
2202 repository: Option<&'static str>,
2203 latest_version: deps_core::ConcreteVersion,
2204 }
2205 impl deps_core::Metadata for MockMetadata {
2206 fn name(&self) -> &deps_core::PackageName {
2207 &self.name
2208 }
2209 fn description(&self) -> Option<&str> {
2210 None
2211 }
2212 fn repository(&self) -> Option<&str> {
2213 self.repository
2214 }
2215 fn documentation(&self) -> Option<&str> {
2216 None
2217 }
2218 fn latest_version(&self) -> &deps_core::ConcreteVersion {
2219 &self.latest_version
2220 }
2221 fn as_any(&self) -> &dyn std::any::Any {
2222 self
2223 }
2224 }
2225
2226 #[test]
2227 fn test_create_package_completion_item_cargo_dotted_name_quotes_toml_key() {
2228 let meta = MockMetadata {
2236 name: deps_core::PackageName::new("some.crate"),
2237 repository: None,
2238 latest_version: "6.1".into(),
2239 };
2240 let item = create_package_completion_item(&meta, EcosystemId::Cargo).unwrap();
2241
2242 assert_eq!(
2243 item.insert_text,
2244 Some("\"some.crate\" = \"6.1\"".to_string())
2245 );
2246 }
2247
2248 #[test]
2249 fn test_create_package_completion_item_pypi_dotted_name_stays_bare_string() {
2250 let meta = MockMetadata {
2254 name: deps_core::PackageName::new("zope.interface"),
2255 repository: None,
2256 latest_version: "6.1".into(),
2257 };
2258 let item = create_package_completion_item(&meta, EcosystemId::Pypi).unwrap();
2259
2260 assert_eq!(item.insert_text, Some("zope.interface".to_string()));
2261 }
2262
2263 #[test]
2264 fn test_create_package_completion_item_maven_group_artifact() {
2265 let meta = MockMetadata {
2266 name: deps_core::PackageName::new("org.apache.commons:commons-lang3"),
2267 repository: None,
2268 latest_version: "3.14.0".into(),
2269 };
2270 let item = create_package_completion_item(&meta, EcosystemId::Maven).unwrap();
2271
2272 assert_eq!(
2273 item.insert_text,
2274 Some(
2275 "<groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId>\
2276 <version>3.14.0</version>"
2277 .to_string()
2278 )
2279 );
2280 }
2281
2282 #[test]
2283 fn test_create_package_completion_item_maven_no_colon() {
2284 let meta = MockMetadata {
2285 name: deps_core::PackageName::new("commons-lang3"),
2286 repository: None,
2287 latest_version: "3.14.0".into(),
2288 };
2289 let item = create_package_completion_item(&meta, EcosystemId::Maven).unwrap();
2290
2291 assert_eq!(
2292 item.insert_text,
2293 Some("<artifactId>commons-lang3</artifactId><version>3.14.0</version>".to_string())
2294 );
2295 }
2296
2297 #[test]
2298 fn test_create_package_completion_item_maven_rejects_xml_breakout_artifact_id() {
2299 let meta = MockMetadata {
2303 name: deps_core::PackageName::new(
2304 "org.apache.commons:commons</artifactId><parent><groupId>evil",
2305 ),
2306 repository: None,
2307 latest_version: "3.14.0".into(),
2308 };
2309
2310 assert!(create_package_completion_item(&meta, EcosystemId::Maven).is_none());
2311 }
2312
2313 #[test]
2314 fn test_create_package_completion_item_maven_rejects_xml_breakout_group_id() {
2315 let meta = MockMetadata {
2316 name: deps_core::PackageName::new("org.evil</groupId><parent>:commons-lang3"),
2317 repository: None,
2318 latest_version: "3.14.0".into(),
2319 };
2320
2321 assert!(create_package_completion_item(&meta, EcosystemId::Maven).is_none());
2322 }
2323
2324 #[test]
2325 fn test_create_package_completion_item_maven_no_colon_rejects_xml_breakout() {
2326 let meta = MockMetadata {
2327 name: deps_core::PackageName::new("commons</artifactId><parent>"),
2328 repository: None,
2329 latest_version: "3.14.0".into(),
2330 };
2331
2332 assert!(create_package_completion_item(&meta, EcosystemId::Maven).is_none());
2333 }
2334
2335 #[test]
2336 fn test_create_package_completion_item_rejects_unsafe_latest_version() {
2337 let meta = MockMetadata {
2341 name: deps_core::PackageName::new("serde"),
2342 repository: None,
2343 latest_version: "1.0.0\", git = \"https://evil".into(),
2344 };
2345
2346 assert!(create_package_completion_item(&meta, EcosystemId::Cargo).is_none());
2347 }
2348
2349 #[test]
2350 fn test_create_package_completion_item_swift_with_repository() {
2351 let meta = MockMetadata {
2352 name: deps_core::PackageName::new("apple/swift-nio"),
2353 repository: Some("https://github.com/apple/swift-nio"),
2354 latest_version: "2.62.0".into(),
2355 };
2356 let item = create_package_completion_item(&meta, EcosystemId::Swift).unwrap();
2357
2358 assert_eq!(
2359 item.insert_text,
2360 Some(
2361 ".package(url: \"https://github.com/apple/swift-nio\", from: \"2.62.0\")"
2362 .to_string()
2363 )
2364 );
2365 }
2366
2367 #[test]
2368 fn test_create_package_completion_item_swift_empty_latest_omits_from_clause() {
2369 let meta = MockMetadata {
2370 name: deps_core::PackageName::new("apple/swift-nio"),
2371 repository: Some("https://github.com/apple/swift-nio"),
2372 latest_version: "".into(),
2373 };
2374 let item = create_package_completion_item(&meta, EcosystemId::Swift).unwrap();
2375
2376 assert_eq!(
2377 item.insert_text,
2378 Some(".package(url: \"https://github.com/apple/swift-nio\")".to_string())
2379 );
2380 }
2381
2382 #[test]
2383 fn test_create_package_completion_item_swift_no_repository_falls_back_to_name() {
2384 let meta = MockMetadata {
2385 name: deps_core::PackageName::new("apple/swift-nio"),
2386 repository: None,
2387 latest_version: "2.62.0".into(),
2388 };
2389 let item = create_package_completion_item(&meta, EcosystemId::Swift).unwrap();
2390
2391 assert_eq!(
2392 item.insert_text,
2393 Some(
2394 ".package(url: \"https://github.com/apple/swift-nio\", from: \"2.62.0\")"
2395 .to_string()
2396 )
2397 );
2398 }
2399
2400 #[test]
2401 fn test_create_package_completion_item_swift_rejects_string_literal_breakout_repository() {
2402 let meta = MockMetadata {
2405 name: deps_core::PackageName::new("apple/swift-nio"),
2406 repository: Some(
2407 "https://evil.example\", .exact(\"1.0.0\")), .package(url: \"https://real",
2408 ),
2409 latest_version: "2.62.0".into(),
2410 };
2411
2412 assert!(create_package_completion_item(&meta, EcosystemId::Swift).is_none());
2413 }
2414
2415 #[test]
2416 fn test_create_package_completion_item_swift_rejects_malicious_name_in_fallback_url() {
2417 let meta = MockMetadata {
2418 name: deps_core::PackageName::new("apple/swift-nio\", .exact(\"1\")) //"),
2419 repository: None,
2420 latest_version: "2.62.0".into(),
2421 };
2422
2423 assert!(create_package_completion_item(&meta, EcosystemId::Swift).is_none());
2424 }
2425
2426 #[test]
2427 fn test_create_package_completion_item_swift_rejects_malicious_name_even_with_repository() {
2428 let meta = MockMetadata {
2435 name: deps_core::PackageName::new("apple/swift-nio\", .exact(\"1\")) //"),
2436 repository: Some("https://github.com/apple/swift-nio"),
2437 latest_version: "2.62.0".into(),
2438 };
2439
2440 assert!(create_package_completion_item(&meta, EcosystemId::Swift).is_none());
2441 }
2442
2443 #[test]
2444 fn test_create_package_completion_item_deno_strips_scheme_for_alias_key() {
2445 let meta = MockMetadata {
2446 name: deps_core::PackageName::new("jsr:@std/fs"),
2447 repository: None,
2448 latest_version: "1.0.24".into(),
2449 };
2450 let item = create_package_completion_item(&meta, EcosystemId::Deno).unwrap();
2451
2452 assert_eq!(
2453 item.insert_text,
2454 Some("\"@std/fs\": \"jsr:@std/fs@^1.0.24\"".to_string())
2455 );
2456 }
2457
2458 #[test]
2459 fn test_create_package_completion_item_deno_npm_scheme() {
2460 let meta = MockMetadata {
2461 name: deps_core::PackageName::new("npm:react"),
2462 repository: None,
2463 latest_version: "18.3.1".into(),
2464 };
2465 let item = create_package_completion_item(&meta, EcosystemId::Deno).unwrap();
2466
2467 assert_eq!(
2468 item.insert_text,
2469 Some("\"react\": \"npm:react@^18.3.1\"".to_string())
2470 );
2471 }
2472
2473 #[test]
2474 fn test_create_package_completion_item_deno_empty_latest_omits_version_clause() {
2475 let meta = MockMetadata {
2477 name: deps_core::PackageName::new("jsr:@std/fs"),
2478 repository: None,
2479 latest_version: "".into(),
2480 };
2481 let item = create_package_completion_item(&meta, EcosystemId::Deno).unwrap();
2482
2483 assert_eq!(
2484 item.insert_text,
2485 Some("\"@std/fs\": \"jsr:@std/fs\"".to_string())
2486 );
2487 }
2488
2489 #[test]
2490 fn test_create_package_completion_item_composer() {
2491 let meta = MockMetadata {
2492 name: deps_core::PackageName::new("monolog/monolog"),
2493 repository: None,
2494 latest_version: "3.5.0".into(),
2495 };
2496 let item = create_package_completion_item(&meta, EcosystemId::Composer).unwrap();
2497
2498 assert_eq!(
2499 item.insert_text,
2500 Some("\"monolog/monolog\": \"^3.5.0\"".to_string())
2501 );
2502 }
2503
2504 #[test]
2505 fn test_create_package_completion_item_go() {
2506 let meta = MockMetadata {
2507 name: deps_core::PackageName::new("github.com/stretchr/testify"),
2508 repository: None,
2509 latest_version: "v1.9.0".into(),
2510 };
2511 let item = create_package_completion_item(&meta, EcosystemId::Go).unwrap();
2512
2513 assert_eq!(
2514 item.insert_text,
2515 Some("github.com/stretchr/testify v1.9.0".to_string())
2516 );
2517 }
2518
2519 #[test]
2520 fn test_create_package_completion_item_dart() {
2521 let meta = MockMetadata {
2522 name: deps_core::PackageName::new("path"),
2523 repository: None,
2524 latest_version: "1.9.0".into(),
2525 };
2526 let item = create_package_completion_item(&meta, EcosystemId::Dart).unwrap();
2527
2528 assert_eq!(item.insert_text, Some("\"path\": ^1.9.0".to_string()));
2529 }
2530
2531 #[test]
2532 fn test_create_package_completion_item_gradle() {
2533 let meta = MockMetadata {
2534 name: deps_core::PackageName::new("org.apache.commons:commons-lang3"),
2535 repository: None,
2536 latest_version: "3.14.0".into(),
2537 };
2538 let item = create_package_completion_item(&meta, EcosystemId::Gradle).unwrap();
2539
2540 assert_eq!(
2541 item.insert_text,
2542 Some("implementation(\"org.apache.commons:commons-lang3:3.14.0\")".to_string())
2543 );
2544 }
2545
2546 #[test]
2547 fn test_create_package_completion_item_nuget() {
2548 let meta = MockMetadata {
2549 name: deps_core::PackageName::new("Newtonsoft.Json"),
2550 repository: None,
2551 latest_version: "13.0.3".into(),
2552 };
2553 let item = create_package_completion_item(&meta, EcosystemId::NuGet).unwrap();
2554
2555 assert_eq!(
2556 item.insert_text,
2557 Some("<PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />".to_string())
2558 );
2559 }
2560
2561 #[test]
2562 fn test_create_package_completion_item_bundler() {
2563 let meta = MockMetadata {
2564 name: deps_core::PackageName::new("rails"),
2565 repository: None,
2566 latest_version: "7.1.3".into(),
2567 };
2568 let item = create_package_completion_item(&meta, EcosystemId::Bundler).unwrap();
2569
2570 assert_eq!(
2571 item.insert_text,
2572 Some("gem \"rails\", \"~> 7.1.3\"".to_string())
2573 );
2574 }
2575
2576 #[test]
2577 fn test_create_package_completion_item_rejects_malicious_name_toml_json_breakout() {
2578 let evil = deps_core::PackageName::new("evil\"\nbackdoor = \"9.9.9");
2582 for ecosystem in [
2583 EcosystemId::Cargo,
2584 EcosystemId::Pypi,
2585 EcosystemId::Npm,
2586 EcosystemId::Composer,
2587 EcosystemId::Go,
2588 EcosystemId::Dart,
2589 EcosystemId::Gradle,
2590 EcosystemId::NuGet,
2591 EcosystemId::Bundler,
2592 EcosystemId::Deno,
2593 ] {
2594 let meta = MockMetadata {
2595 name: evil.clone(),
2596 repository: None,
2597 latest_version: "9.9.9".into(),
2598 };
2599 assert!(
2600 create_package_completion_item(&meta, ecosystem).is_none(),
2601 "expected {ecosystem:?} to reject the malicious name"
2602 );
2603 }
2604 }
2605
2606 #[tokio::test]
2607 async fn test_fallback_triggered_when_parse_fails() {
2608 let state = Arc::new(ServerState::new());
2609 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
2610
2611 let content = r"[dependencies]
2613ser"
2614 .to_string();
2615
2616 let doc = DocumentState::new_without_parse_result(EcosystemId::Cargo, content.clone());
2618 state.update_document(uri.clone(), doc);
2619
2620 let params = CompletionParams {
2621 text_document_position: TextDocumentPositionParams {
2622 text_document: TextDocumentIdentifier { uri },
2623 position: Position::new(1, 3), },
2625 work_done_progress_params: Default::default(),
2626 partial_result_params: Default::default(),
2627 context: None,
2628 };
2629
2630 let (client, config) = create_test_client_and_config();
2632 let result = handle_completion(state, params, client, config).await;
2633 drop(result);
2636 }
2637
2638 #[test]
2639 fn test_fallback_rejects_single_char_prefix() {
2640 let content = r"
2641[dependencies]
2642s
2643";
2644
2645 let line = content.lines().nth(2).unwrap();
2647 let prefix = extract_prefix(line, 1, EcosystemId::Cargo);
2648
2649 assert_eq!(prefix.len(), 1);
2651 assert!(prefix.chars().count() < 2);
2652 }
2653
2654 #[tokio::test]
2655 async fn test_fallback_completion_rejects_single_cjk_char_prefix() {
2656 use deps_core::{Metadata, Registry, Version};
2657 use std::any::Any;
2658
2659 struct PanicsIfSearchedRegistry;
2663 impl Registry for PanicsIfSearchedRegistry {
2664 fn get_versions<'a>(
2665 &'a self,
2666 _name: &'a deps_core::PackageName,
2667 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
2668 {
2669 Box::pin(async move { Ok(vec![]) })
2670 }
2671 fn get_latest_matching<'a>(
2672 &'a self,
2673 _name: &'a deps_core::PackageName,
2674 _req: &'a deps_core::VersionReq,
2675 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
2676 {
2677 Box::pin(async move { Ok(None) })
2678 }
2679 fn search<'a>(
2680 &'a self,
2681 _query: &'a str,
2682 _limit: usize,
2683 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
2684 {
2685 panic!("guard must short-circuit before reaching registry search");
2686 }
2687 fn as_any(&self) -> &dyn Any {
2688 self
2689 }
2690 }
2691
2692 let state = mock_cargo_state(Arc::new(PanicsIfSearchedRegistry));
2693 let content = "[dependencies]\n日\n";
2694 let position = Position::new(1, 1); let items = fallback_completion(&state, EcosystemId::Cargo, position, content).await;
2697 assert!(items.is_empty());
2698 }
2699
2700 #[tokio::test]
2701 async fn test_fallback_completion_passes_two_char_prefixes_to_search() {
2702 use deps_core::{Metadata, Registry, Version};
2703 use std::any::Any;
2704
2705 struct MockMetadata {
2706 name: deps_core::PackageName,
2707 }
2708 impl Metadata for MockMetadata {
2709 fn name(&self) -> &deps_core::PackageName {
2710 &self.name
2711 }
2712 fn description(&self) -> Option<&str> {
2713 None
2714 }
2715 fn repository(&self) -> Option<&str> {
2716 None
2717 }
2718 fn documentation(&self) -> Option<&str> {
2719 None
2720 }
2721 fn latest_version(&self) -> &deps_core::ConcreteVersion {
2722 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
2723 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("1.0.0"));
2724 &VERSION
2725 }
2726 fn as_any(&self) -> &dyn Any {
2727 self
2728 }
2729 }
2730
2731 struct StubRegistry;
2732 impl Registry for StubRegistry {
2733 fn get_versions<'a>(
2734 &'a self,
2735 _name: &'a deps_core::PackageName,
2736 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
2737 {
2738 Box::pin(async move { Ok(vec![]) })
2739 }
2740 fn get_latest_matching<'a>(
2741 &'a self,
2742 _name: &'a deps_core::PackageName,
2743 _req: &'a deps_core::VersionReq,
2744 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
2745 {
2746 Box::pin(async move { Ok(None) })
2747 }
2748 fn search<'a>(
2749 &'a self,
2750 _query: &'a str,
2751 _limit: usize,
2752 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
2753 {
2754 Box::pin(async move {
2755 Ok(vec![Box::new(MockMetadata {
2756 name: deps_core::PackageName::new("serde"),
2757 }) as Box<dyn Metadata>])
2758 })
2759 }
2760 fn as_any(&self) -> &dyn Any {
2761 self
2762 }
2763 }
2764
2765 let cjk_state = mock_cargo_state(Arc::new(StubRegistry));
2768 let cjk_items = fallback_completion(
2769 &cjk_state,
2770 EcosystemId::Cargo,
2771 Position::new(1, 2),
2772 "[dependencies]\n日本\n",
2773 )
2774 .await;
2775 assert_eq!(cjk_items.len(), 1);
2776 assert_eq!(cjk_items[0].label, "serde");
2777
2778 let ascii_state = mock_cargo_state(Arc::new(StubRegistry));
2781 let ascii_items = fallback_completion(
2782 &ascii_state,
2783 EcosystemId::Cargo,
2784 Position::new(1, 2),
2785 "[dependencies]\nse\n",
2786 )
2787 .await;
2788 assert_eq!(ascii_items.len(), 1);
2789 assert_eq!(ascii_items[0].label, "serde");
2790 }
2791
2792 #[test]
2793 fn test_extract_prefix_strips_leading_xml_tag_for_maven() {
2794 assert_eq!(
2796 extract_prefix(" <artifactId>gua", 17, EcosystemId::Maven),
2797 "gua"
2798 );
2799 }
2800
2801 #[test]
2802 fn test_extract_prefix_maven_unclosed_tag_is_unchanged() {
2803 assert_eq!(
2805 extract_prefix(" <artifactId", 13, EcosystemId::Maven),
2806 "<artifactId"
2807 );
2808 }
2809
2810 #[test]
2817 fn test_extract_prefix_maven_strips_last_tag_not_first() {
2818 let line = " <dependency><groupId>com.google.guava</groupId><artifactId>gua";
2819 assert_eq!(extract_prefix(line, 66, EcosystemId::Maven), "gua");
2820 }
2821
2822 #[test]
2831 fn test_extract_prefix_maven_after_closed_tag_is_empty() {
2832 let line = " <artifactId>guava</artifactId>";
2833 assert_eq!(extract_prefix(line, 34, EcosystemId::Maven), "");
2834 }
2835
2836 #[tokio::test]
2844 async fn test_fallback_completion_maven_query_matches_tag_value() {
2845 use deps_core::{Metadata, Registry};
2846 use std::any::Any;
2847 use std::sync::Mutex;
2848
2849 struct CapturingRegistry {
2850 captured_query: Mutex<Option<String>>,
2851 }
2852 impl Registry for CapturingRegistry {
2853 fn get_versions<'a>(
2854 &'a self,
2855 _name: &'a deps_core::PackageName,
2856 ) -> deps_core::ecosystem::BoxFuture<
2857 'a,
2858 deps_core::Result<Vec<Box<dyn deps_core::Version>>>,
2859 > {
2860 Box::pin(async move { Ok(vec![]) })
2861 }
2862 fn get_latest_matching<'a>(
2863 &'a self,
2864 _name: &'a deps_core::PackageName,
2865 _req: &'a deps_core::VersionReq,
2866 ) -> deps_core::ecosystem::BoxFuture<
2867 'a,
2868 deps_core::Result<Option<Box<dyn deps_core::Version>>>,
2869 > {
2870 Box::pin(async move { Ok(None) })
2871 }
2872 fn search<'a>(
2873 &'a self,
2874 query: &'a str,
2875 _limit: usize,
2876 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
2877 {
2878 *self.captured_query.lock().unwrap() = Some(query.to_string());
2879 Box::pin(async move { Ok(vec![]) })
2880 }
2881 fn as_any(&self) -> &dyn Any {
2882 self
2883 }
2884 }
2885
2886 let registry = Arc::new(CapturingRegistry {
2887 captured_query: Mutex::new(None),
2888 });
2889 let state = mock_maven_state(Arc::clone(®istry) as Arc<dyn Registry>);
2890
2891 let content = "<dependencies>\n <dependency>\n <artifactId>gua\n";
2892 fallback_completion(&state, EcosystemId::Maven, Position::new(2, 19), content).await;
2893
2894 assert_eq!(
2895 registry.captured_query.lock().unwrap().as_deref(),
2896 Some("gua")
2897 );
2898 }
2899
2900 #[tokio::test]
2907 async fn test_fallback_completion_pypi_project_array_query_has_no_leaked_quote() {
2908 use deps_core::{Metadata, Registry};
2909 use std::any::Any;
2910 use std::sync::Mutex;
2911
2912 struct CapturingRegistry {
2913 captured_query: Mutex<Option<String>>,
2914 }
2915 impl Registry for CapturingRegistry {
2916 fn get_versions<'a>(
2917 &'a self,
2918 _name: &'a deps_core::PackageName,
2919 ) -> deps_core::ecosystem::BoxFuture<
2920 'a,
2921 deps_core::Result<Vec<Box<dyn deps_core::Version>>>,
2922 > {
2923 Box::pin(async move { Ok(vec![]) })
2924 }
2925 fn get_latest_matching<'a>(
2926 &'a self,
2927 _name: &'a deps_core::PackageName,
2928 _req: &'a deps_core::VersionReq,
2929 ) -> deps_core::ecosystem::BoxFuture<
2930 'a,
2931 deps_core::Result<Option<Box<dyn deps_core::Version>>>,
2932 > {
2933 Box::pin(async move { Ok(None) })
2934 }
2935 fn search<'a>(
2936 &'a self,
2937 query: &'a str,
2938 _limit: usize,
2939 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
2940 {
2941 *self.captured_query.lock().unwrap() = Some(query.to_string());
2942 Box::pin(async move { Ok(vec![]) })
2943 }
2944 fn as_any(&self) -> &dyn Any {
2945 self
2946 }
2947 }
2948
2949 let registry = Arc::new(CapturingRegistry {
2950 captured_query: Mutex::new(None),
2951 });
2952 let state = mock_pypi_state(Arc::clone(®istry) as Arc<dyn Registry>);
2953
2954 let content = "[project]\nname = \"myapp\"\nversion = \"0.1.0\"\ndependencies = [\n \"requests>=2.31.0\",\n \"flas\n]\n";
2955 fallback_completion(&state, EcosystemId::Pypi, Position::new(5, 9), content).await;
2956
2957 assert_eq!(
2958 registry.captured_query.lock().unwrap().as_deref(),
2959 Some("flas")
2960 );
2961 }
2962
2963 #[test]
2971 fn test_is_in_dependencies_section_and_extract_prefix_pypi_optional_dependencies_group() {
2972 let content = "[project.optional-dependencies]\ndev = [\n \"pytest\",\n \"flas\n]\n";
2973 assert!(is_in_dependencies_section(content, 3, EcosystemId::Pypi));
2974
2975 let line = content.lines().nth(3).unwrap();
2976 assert_eq!(
2977 extract_prefix(line, line.len() as u32, EcosystemId::Pypi),
2978 "flas"
2979 );
2980 }
2981
2982 #[test]
2983 fn test_fallback_rejects_prefix_with_equals() {
2984 let content = r#"
2985[dependencies]
2986serde = "1.0"
2987"#;
2988
2989 let line = content.lines().nth(2).unwrap();
2991 let prefix = extract_prefix(line, 12, EcosystemId::Cargo); assert!(prefix.contains('='));
2995 }
2996
2997 #[test]
2998 fn test_prefix_extraction_cursor_beyond_line() {
2999 let content = r"
3000[dependencies]
3001serde
3002";
3003
3004 let line = content.lines().nth(2).unwrap();
3006 assert_eq!(line, "serde");
3007
3008 let prefix = extract_prefix(line, 100, EcosystemId::Cargo);
3010
3011 assert_eq!(prefix, "serde");
3013 assert_eq!(prefix.len(), 5); }
3015
3016 #[test]
3017 fn test_extract_prefix_fallback_when_character_exceeds_line() {
3018 let line = "café";
3022 let character = line.chars().map(|c| c.len_utf16() as u32).sum::<u32>() + 10;
3023 assert_eq!(extract_prefix(line, character, EcosystemId::Cargo), "café");
3024 }
3025
3026 #[test]
3027 fn test_extract_prefix_strips_leading_quote_for_json_ecosystems() {
3028 let line = " \"expr";
3031 assert_eq!(
3032 extract_prefix(line, line.len() as u32, EcosystemId::Npm),
3033 "expr"
3034 );
3035 assert_eq!(
3036 extract_prefix(line, line.len() as u32, EcosystemId::Composer),
3037 "expr"
3038 );
3039 }
3040
3041 #[test]
3042 fn test_extract_prefix_strips_trailing_quote_for_json_ecosystems() {
3043 let line = " \"express\"";
3046 assert_eq!(
3047 extract_prefix(line, line.len() as u32, EcosystemId::Npm),
3048 "express"
3049 );
3050 assert_eq!(
3051 extract_prefix(line, line.len() as u32, EcosystemId::Composer),
3052 "express"
3053 );
3054 }
3055
3056 #[test]
3057 fn test_extract_prefix_leaves_quotes_for_non_json_ecosystems() {
3058 let line = "\"expr";
3063 assert_eq!(
3064 extract_prefix(line, line.len() as u32, EcosystemId::Cargo),
3065 "\"expr"
3066 );
3067 }
3068
3069 #[test]
3074 fn test_extract_prefix_strips_leading_quote_for_pypi_toml_array() {
3075 let line = " \"flas";
3076 assert_eq!(
3077 extract_prefix(line, line.len() as u32, EcosystemId::Pypi),
3078 "flas"
3079 );
3080 }
3081
3082 #[test]
3083 fn test_extract_prefix_strips_trailing_quote_for_pypi_toml_array() {
3084 let line = " \"pytest\"";
3085 assert_eq!(
3086 extract_prefix(line, line.len() as u32, EcosystemId::Pypi),
3087 "pytest"
3088 );
3089 }
3090
3091 #[test]
3092 fn test_extract_prefix_does_not_panic_on_multibyte_char_boundary() {
3093 let line = " \"é";
3096 let character: u32 = line.chars().map(|c| c.len_utf16() as u32).sum();
3097 assert_eq!(extract_prefix(line, character, EcosystemId::Cargo), "\"é");
3098 }
3099
3100 #[test]
3101 fn test_extract_prefix_multibyte_word_not_truncated() {
3102 let line = "café";
3103 let character: u32 = line.chars().map(|c| c.len_utf16() as u32).sum();
3104 assert_eq!(extract_prefix(line, character, EcosystemId::Cargo), "café");
3105 }
3106
3107 #[test]
3108 fn test_extract_prefix_cjk_word_not_truncated() {
3109 let line = "日本";
3110 let character: u32 = line.chars().map(|c| c.len_utf16() as u32).sum();
3111 assert_eq!(extract_prefix(line, character, EcosystemId::Cargo), "日本");
3112 }
3113
3114 #[tokio::test]
3115 async fn test_search_packages_returns_results_within_timeout() {
3116 use deps_core::{Metadata, Registry, Version};
3117 use std::any::Any;
3118
3119 struct MockMetadata {
3120 name: deps_core::PackageName,
3121 }
3122 impl Metadata for MockMetadata {
3123 fn name(&self) -> &deps_core::PackageName {
3124 &self.name
3125 }
3126 fn description(&self) -> Option<&str> {
3127 None
3128 }
3129 fn repository(&self) -> Option<&str> {
3130 None
3131 }
3132 fn documentation(&self) -> Option<&str> {
3133 None
3134 }
3135 fn latest_version(&self) -> &deps_core::ConcreteVersion {
3136 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
3137 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("4.18.2"));
3138 &VERSION
3139 }
3140 fn as_any(&self) -> &dyn Any {
3141 self
3142 }
3143 }
3144
3145 struct FastRegistry;
3146 impl Registry for FastRegistry {
3147 fn get_versions<'a>(
3148 &'a self,
3149 _name: &'a deps_core::PackageName,
3150 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
3151 {
3152 Box::pin(async move { Ok(vec![]) })
3153 }
3154
3155 fn get_latest_matching<'a>(
3156 &'a self,
3157 _name: &'a deps_core::PackageName,
3158 _req: &'a deps_core::VersionReq,
3159 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
3160 {
3161 Box::pin(async move { Ok(None) })
3162 }
3163
3164 fn search<'a>(
3165 &'a self,
3166 _query: &'a str,
3167 _limit: usize,
3168 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
3169 {
3170 Box::pin(async move {
3171 Ok(vec![Box::new(MockMetadata {
3172 name: deps_core::PackageName::new("express"),
3173 }) as Box<dyn Metadata>])
3174 })
3175 }
3176
3177 fn as_any(&self) -> &dyn Any {
3178 self
3179 }
3180 }
3181
3182 let items = search_packages(&FastRegistry, EcosystemId::Npm, "express").await;
3183
3184 assert_eq!(items.len(), 1);
3185 assert_eq!(items[0].label, "express");
3186 }
3187
3188 #[tokio::test]
3189 async fn test_search_packages_drops_maven_xml_breakout_keeps_safe_result() {
3190 use deps_core::{Metadata, Registry, Version};
3194 use std::any::Any;
3195
3196 struct MockMetadata {
3197 name: deps_core::PackageName,
3198 }
3199 impl Metadata for MockMetadata {
3200 fn name(&self) -> &deps_core::PackageName {
3201 &self.name
3202 }
3203 fn description(&self) -> Option<&str> {
3204 None
3205 }
3206 fn repository(&self) -> Option<&str> {
3207 None
3208 }
3209 fn documentation(&self) -> Option<&str> {
3210 None
3211 }
3212 fn latest_version(&self) -> &deps_core::ConcreteVersion {
3213 static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
3214 std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("3.14.0"));
3215 &VERSION
3216 }
3217 fn as_any(&self) -> &dyn Any {
3218 self
3219 }
3220 }
3221
3222 struct MavenRegistry;
3223 impl Registry for MavenRegistry {
3224 fn get_versions<'a>(
3225 &'a self,
3226 _name: &'a deps_core::PackageName,
3227 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
3228 {
3229 Box::pin(async move { Ok(vec![]) })
3230 }
3231
3232 fn get_latest_matching<'a>(
3233 &'a self,
3234 _name: &'a deps_core::PackageName,
3235 _req: &'a deps_core::VersionReq,
3236 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
3237 {
3238 Box::pin(async move { Ok(None) })
3239 }
3240
3241 fn search<'a>(
3242 &'a self,
3243 _query: &'a str,
3244 _limit: usize,
3245 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
3246 {
3247 Box::pin(async move {
3248 Ok(vec![
3249 Box::new(MockMetadata {
3250 name: deps_core::PackageName::new("org.apache.commons:commons-lang3"),
3251 }) as Box<dyn Metadata>,
3252 Box::new(MockMetadata {
3253 name: deps_core::PackageName::new(
3254 "org.evil:payload</artifactId><parent>",
3255 ),
3256 }) as Box<dyn Metadata>,
3257 ])
3258 })
3259 }
3260
3261 fn as_any(&self) -> &dyn Any {
3262 self
3263 }
3264 }
3265
3266 let items = search_packages(&MavenRegistry, EcosystemId::Maven, "commons").await;
3267
3268 assert_eq!(items.len(), 1);
3269 assert_eq!(items[0].label, "org.apache.commons:commons-lang3");
3270 }
3271
3272 #[tokio::test(start_paused = true)]
3273 async fn test_search_packages_times_out_and_returns_empty() {
3274 use deps_core::{Metadata, Registry, Version};
3275 use std::any::Any;
3276 use std::time::Duration;
3277
3278 struct SlowRegistry;
3279 impl Registry for SlowRegistry {
3280 fn get_versions<'a>(
3281 &'a self,
3282 _name: &'a deps_core::PackageName,
3283 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
3284 {
3285 Box::pin(async move { Ok(vec![]) })
3286 }
3287
3288 fn get_latest_matching<'a>(
3289 &'a self,
3290 _name: &'a deps_core::PackageName,
3291 _req: &'a deps_core::VersionReq,
3292 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
3293 {
3294 Box::pin(async move { Ok(None) })
3295 }
3296
3297 fn search<'a>(
3298 &'a self,
3299 _query: &'a str,
3300 _limit: usize,
3301 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
3302 {
3303 Box::pin(async move {
3304 tokio::time::sleep(Duration::from_mins(1)).await;
3307 Ok(vec![])
3308 })
3309 }
3310
3311 fn as_any(&self) -> &dyn Any {
3312 self
3313 }
3314 }
3315
3316 let items = search_packages(&SlowRegistry, EcosystemId::Npm, "expr").await;
3317
3318 assert!(
3319 items.is_empty(),
3320 "should return empty on timeout, not block"
3321 );
3322 }
3323
3324 #[tokio::test(start_paused = true)]
3325 async fn test_handle_completion_primary_path_times_out_and_skips_fallback() {
3326 use deps_core::{
3327 Dependency, DiagnosticMessages, DiagnosticPolicy, Ecosystem, EcosystemFormatter,
3328 OsvNaming, PackageNaming, PackageRendering, ParseResult, RequirementResolution,
3329 SourcePolicy,
3330 };
3331 use std::any::Any;
3332 use std::path::Path;
3333 use std::time::Duration;
3334 use tower_lsp_server::ls_types::Uri;
3335
3336 struct MockFormatter;
3337 impl PackageNaming for MockFormatter {}
3338
3339 impl PackageRendering for MockFormatter {
3340 fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
3341 version.to_string()
3342 }
3343
3344 fn package_url(&self, name: &deps_core::PackageName) -> String {
3345 format!("https://example.com/{name}")
3346 }
3347 }
3348
3349 impl RequirementResolution for MockFormatter {}
3350
3351 impl DiagnosticMessages for MockFormatter {}
3352
3353 impl DiagnosticPolicy for MockFormatter {}
3354
3355 impl SourcePolicy for MockFormatter {}
3356
3357 impl OsvNaming for MockFormatter {}
3358
3359 struct SlowEcosystem;
3363 impl deps_core::ecosystem::private::Sealed for SlowEcosystem {}
3364 impl Ecosystem for SlowEcosystem {
3365 fn id(&self) -> &'static str {
3366 "cargo"
3367 }
3368 fn display_name(&self) -> &'static str {
3369 "Cargo (slow mock)"
3370 }
3371 fn manifest_filenames(&self) -> &[&'static str] {
3372 &["Cargo.toml"]
3373 }
3374 fn parse_manifest<'a>(
3375 &'a self,
3376 _content: &'a str,
3377 _uri: &'a Uri,
3378 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>>
3379 {
3380 Box::pin(async move { unimplemented!() })
3381 }
3382 fn registry(&self) -> Arc<dyn deps_core::Registry> {
3383 unimplemented!()
3384 }
3385 fn formatter(&self) -> &dyn EcosystemFormatter {
3386 &MockFormatter
3387 }
3388 fn generate_completions<'a>(
3389 &'a self,
3390 _parse_result: &'a dyn ParseResult,
3391 _position: tower_lsp_server::ls_types::Position,
3392 _content: &'a str,
3393 _freshness: deps_core::FreshnessSettings,
3394 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::completion::Completions>
3395 {
3396 Box::pin(async move {
3397 tokio::time::sleep(Duration::from_mins(1)).await;
3400 deps_core::completion::Completions::default()
3401 })
3402 }
3403 fn as_any(&self) -> &dyn Any {
3404 self
3405 }
3406 }
3407
3408 struct MockParseResult {
3409 uri: Uri,
3410 }
3411 impl ParseResult for MockParseResult {
3412 fn dependencies(&self) -> Vec<&dyn Dependency> {
3413 vec![]
3414 }
3415 fn workspace_root(&self) -> Option<&Path> {
3416 None
3417 }
3418 fn uri(&self) -> &Uri {
3419 &self.uri
3420 }
3421 fn as_any(&self) -> &dyn Any {
3422 self
3423 }
3424 }
3425
3426 let state = Arc::new(ServerState::new());
3427 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
3428
3429 state.ecosystem_registry.register(Arc::new(SlowEcosystem));
3431
3432 let content = "[dependencies]\nserde = \"1\"\n".to_string();
3433 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
3434 let doc = DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
3435 state.update_document(uri.clone(), doc);
3436
3437 let params = CompletionParams {
3438 text_document_position: TextDocumentPositionParams {
3439 text_document: TextDocumentIdentifier { uri },
3440 position: Position::new(1, 5), },
3442 work_done_progress_params: Default::default(),
3443 partial_result_params: Default::default(),
3444 context: None,
3445 };
3446
3447 let (client, config) = create_test_client_and_config();
3448 let result = handle_completion(state, params, client, config).await;
3449
3450 assert!(result.is_none());
3453 }
3454
3455 #[tokio::test]
3467 async fn test_generate_completions_is_incomplete_flows_into_response_both_branches() {
3468 use deps_core::completion::Completions;
3469 use deps_core::ecosystem::private::Sealed;
3470 use deps_core::{
3471 Dependency, DiagnosticMessages, DiagnosticPolicy, Ecosystem, EcosystemFormatter,
3472 Metadata, OsvNaming, PackageNaming, PackageRendering, ParseResult, Registry,
3473 RequirementResolution, SourcePolicy, Version,
3474 };
3475 use std::any::Any;
3476 use tower_lsp_server::ls_types::Uri;
3477
3478 struct NoopRegistry;
3479 impl Registry for NoopRegistry {
3480 fn get_versions<'a>(
3481 &'a self,
3482 _name: &'a deps_core::PackageName,
3483 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
3484 {
3485 Box::pin(async move { Ok(vec![]) })
3486 }
3487 fn get_latest_matching<'a>(
3488 &'a self,
3489 _name: &'a deps_core::PackageName,
3490 _req: &'a deps_core::VersionReq,
3491 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
3492 {
3493 Box::pin(async move { Ok(None) })
3494 }
3495 fn search<'a>(
3496 &'a self,
3497 _query: &'a str,
3498 _limit: usize,
3499 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
3500 {
3501 Box::pin(async move { Ok(vec![]) })
3502 }
3503 fn as_any(&self) -> &dyn Any {
3504 self
3505 }
3506 }
3507
3508 struct NoopFormatter;
3509 impl PackageNaming for NoopFormatter {}
3510
3511 impl PackageRendering for NoopFormatter {
3512 fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
3513 version.to_string()
3514 }
3515
3516 fn package_url(&self, name: &deps_core::PackageName) -> String {
3517 format!("https://example.com/{name}")
3518 }
3519 }
3520
3521 impl RequirementResolution for NoopFormatter {}
3522
3523 impl DiagnosticMessages for NoopFormatter {}
3524
3525 impl DiagnosticPolicy for NoopFormatter {}
3526
3527 impl SourcePolicy for NoopFormatter {}
3528
3529 impl OsvNaming for NoopFormatter {}
3530
3531 struct IncompleteEcosystem {
3534 has_item: bool,
3535 }
3536 impl Sealed for IncompleteEcosystem {}
3537 impl Ecosystem for IncompleteEcosystem {
3538 fn id(&self) -> &'static str {
3539 "cargo"
3540 }
3541 fn display_name(&self) -> &'static str {
3542 "cargo"
3543 }
3544 fn manifest_filenames(&self) -> &[&'static str] {
3545 &["Cargo.toml"]
3546 }
3547 fn parse_manifest<'a>(
3548 &'a self,
3549 _content: &'a str,
3550 _uri: &'a Uri,
3551 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Box<dyn ParseResult>>>
3552 {
3553 Box::pin(async move { unimplemented!() })
3554 }
3555 fn registry(&self) -> Arc<dyn Registry> {
3556 Arc::new(NoopRegistry)
3557 }
3558 fn formatter(&self) -> &dyn EcosystemFormatter {
3559 &NoopFormatter
3560 }
3561 fn generate_completions<'a>(
3562 &'a self,
3563 _parse_result: &'a dyn ParseResult,
3564 _position: tower_lsp_server::ls_types::Position,
3565 _content: &'a str,
3566 _freshness: deps_core::FreshnessSettings,
3567 ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
3568 let items = if self.has_item {
3569 vec![CompletionItem {
3570 label: "requests".to_string(),
3571 ..Default::default()
3572 }]
3573 } else {
3574 vec![]
3575 };
3576 Box::pin(async move {
3577 Completions {
3578 items,
3579 is_incomplete: true,
3580 }
3581 })
3582 }
3583 fn as_any(&self) -> &dyn Any {
3584 self
3585 }
3586 }
3587
3588 struct MockParseResult {
3589 uri: Uri,
3590 }
3591 impl ParseResult for MockParseResult {
3592 fn dependencies(&self) -> Vec<&dyn Dependency> {
3593 vec![]
3594 }
3595 fn workspace_root(&self) -> Option<&std::path::Path> {
3596 None
3597 }
3598 fn uri(&self) -> &Uri {
3599 &self.uri
3600 }
3601 fn as_any(&self) -> &dyn Any {
3602 self
3603 }
3604 }
3605
3606 async fn run(has_item: bool) -> Option<CompletionResponse> {
3607 let state = Arc::new(ServerState::new());
3608 state
3609 .ecosystem_registry
3610 .register(Arc::new(IncompleteEcosystem { has_item }));
3611
3612 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
3613 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
3614 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
3615 let doc =
3616 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
3617 state.update_document(uri.clone(), doc);
3618
3619 let params = CompletionParams {
3620 text_document_position: TextDocumentPositionParams {
3621 text_document: TextDocumentIdentifier { uri },
3622 position: Position::new(0, 0),
3623 },
3624 work_done_progress_params: Default::default(),
3625 partial_result_params: Default::default(),
3626 context: None,
3627 };
3628
3629 let (client, config) = create_test_client_and_config();
3630 handle_completion(state, params, client, config).await
3631 }
3632
3633 match run(false).await {
3634 Some(CompletionResponse::List(list)) => {
3635 assert!(
3636 list.is_incomplete,
3637 "empty branch must still carry is_incomplete"
3638 );
3639 assert!(list.items.is_empty());
3640 }
3641 other => panic!("expected List{{is_incomplete:true, items:[]}}, got {other:?}"),
3642 }
3643
3644 match run(true).await {
3645 Some(CompletionResponse::List(list)) => {
3646 assert!(list.is_incomplete);
3647 assert_eq!(list.items.len(), 1);
3648 assert_eq!(list.items[0].label, "requests");
3649 }
3650 other => panic!("expected List{{is_incomplete:true, items:[requests]}}, got {other:?}"),
3651 }
3652 }
3653}