1pub(crate) mod auto_import;
92mod builtins;
93mod context;
94mod file_path;
95mod functions;
96mod items;
97mod keywords;
98mod methods;
99mod packages;
100mod regex_patterns;
101pub(crate) mod scope_distance;
102mod snippets;
103mod sort;
104pub(crate) mod test_more;
105mod variables;
106mod workspace;
107
108pub use self::context::CompletionContext;
110pub use self::items::{CompletionItem, CompletionItemKind};
111pub use self::methods::get_dbi_method_documentation;
112pub use self::test_more::get_test_more_documentation;
113
114use perl_parser_core::ast::Node;
115use perl_parser_core::ast::NodeKind;
116use perl_semantic_analyzer::symbol::{SymbolExtractor, SymbolKind, SymbolTable};
117use perl_workspace_index::workspace_index::WorkspaceIndex;
118use std::collections::{HashMap, HashSet};
119use std::sync::Arc;
120
121type ImportMap = HashMap<String, HashSet<String>>;
128
129pub struct CompletionProvider {
131 symbol_table: SymbolTable,
132 workspace_index: Option<Arc<WorkspaceIndex>>,
133 import_map: ImportMap,
134}
135
136impl CompletionProvider {
137 pub fn new_with_index(ast: &Node, workspace_index: Option<Arc<WorkspaceIndex>>) -> Self {
164 Self::new_with_index_and_source(ast, "", workspace_index)
165 }
166
167 pub fn new_with_index_and_source(
209 ast: &Node,
210 source: &str,
211 workspace_index: Option<Arc<WorkspaceIndex>>,
212 ) -> Self {
213 let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
214 let import_map = Self::extract_import_map(ast);
215
216 CompletionProvider { symbol_table, workspace_index, import_map }
217 }
218
219 fn extract_import_map(ast: &Node) -> ImportMap {
224 let mut map: ImportMap = HashMap::new();
225
226 fn collect(node: &Node, map: &mut ImportMap) {
227 match &node.kind {
228 NodeKind::Use { module, args, .. } => {
229 let first_char: Option<char> = module.chars().next();
231 if !first_char.is_some_and(|c: char| c.is_ascii_uppercase()) {
232 return;
233 }
234
235 if args.is_empty() {
237 return;
238 }
239
240 let mut symbols: HashSet<String> = HashSet::new();
241 let mut has_symbol_args = false;
242
243 for arg in args {
244 let first_byte = arg.as_bytes().first().copied().unwrap_or(0);
246 if first_byte.is_ascii_digit() {
247 continue;
248 }
249 if arg.starts_with('-') {
251 continue;
252 }
253 if arg.starts_with('{') {
255 continue;
256 }
257
258 if arg.starts_with("qw") {
259 let content = arg
260 .trim_start_matches("qw")
261 .trim_start_matches(|c: char| "([{/<|!".contains(c))
262 .trim_end_matches(|c: char| ")]}/|!>".contains(c));
263 for word in content.split_whitespace() {
264 if !word.is_empty() {
265 symbols.insert(word.to_string());
266 has_symbol_args = true;
267 }
268 }
269 } else {
270 let cleaned = arg.trim_matches(|c: char| c == '\'' || c == '"');
272 if !cleaned.is_empty() {
273 symbols.insert(cleaned.to_string());
274 has_symbol_args = true;
275 }
276 }
277 }
278
279 if has_symbol_args {
280 map.entry(module.clone()).or_default().extend(symbols);
281 } else {
282 map.entry(module.clone()).or_default();
284 }
285 }
286 NodeKind::Program { statements } | NodeKind::Block { statements } => {
287 for stmt in statements {
288 collect(stmt, map);
289 }
290 }
291 _ => {}
292 }
293 }
294
295 collect(ast, &mut map);
296 map
297 }
298
299 pub fn new(ast: &Node) -> Self {
333 Self::new_with_index(ast, None)
334 }
335
336 pub fn get_completions_with_path(
379 &self,
380 source: &str,
381 position: usize,
382 filepath: Option<&str>,
383 ) -> Vec<CompletionItem> {
384 self.get_completions_with_path_cancellable(source, position, filepath, &|| false)
385 }
386
387 pub fn get_completions_with_path_cancellable(
439 &self,
440 source: &str,
441 position: usize,
442 filepath: Option<&str>,
443 is_cancelled: &dyn Fn() -> bool,
444 ) -> Vec<CompletionItem> {
445 if position > source.len() {
447 return vec![];
448 }
449
450 let context = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
451 self.analyze_context(source, position)
452 })) {
453 Ok(mut ctx) => {
454 ctx.in_use_statement = Self::is_use_statement_context(source, position);
455 ctx
456 }
457 Err(_) => {
458 return vec![];
459 }
460 };
461
462 if context.in_comment {
463 return vec![];
464 }
465
466 if is_cancelled() {
468 return vec![];
469 }
470
471 if context.trigger_character == Some('-')
475 && !(context.prefix.ends_with("->") && context.prefix.len() > 2)
476 {
477 return vec![];
478 }
479
480 let mut completions = Vec::new();
481
482 if Self::is_in_regex_flags(source, position) {
486 regex_patterns::add_regex_flag_completions(&mut completions, &context, source);
487 return sort::deduplicate_and_sort(completions);
488 }
489
490 if context.in_regex && !matches!(context.prefix.chars().next(), Some('$' | '@' | '%')) {
494 regex_patterns::add_regex_completions(&mut completions, &context, source);
495 return sort::deduplicate_and_sort(completions);
496 }
497
498 if let Some((module_name, qw_prefix)) = Self::detect_use_qw_import_context(source, position)
501 {
502 workspace::add_use_qw_import_completions(
503 &mut completions,
504 &context,
505 &self.workspace_index,
506 &module_name,
507 &qw_prefix,
508 );
509 } else if context.in_use_statement && !context.prefix.starts_with('$') {
510 workspace::add_use_module_completions(
512 &mut completions,
513 &context,
514 &self.workspace_index,
515 );
516 } else if self.is_has_options_key_context(source, position) {
517 self.add_has_option_completions(&mut completions, &context);
518 } else if (context.trigger_character == Some('>') || context.trigger_character == Some('-'))
519 && context.prefix.ends_with("->")
520 && context.prefix.len() > 2
521 {
522 methods::add_method_completions(&mut completions, &context, source, &self.symbol_table);
526 workspace::add_workspace_method_completions(
528 &mut completions,
529 &context,
530 source,
531 &self.workspace_index,
532 );
533 } else if context.prefix.starts_with('$') && context.prefix.contains("::") {
534 packages::add_package_completions(&mut completions, &context, &self.workspace_index);
535 if !completions.is_empty() {
536 return completions;
537 }
538 variables::add_variable_completions(
539 &mut completions,
540 &context,
541 SymbolKind::scalar(),
542 &self.symbol_table,
543 );
544 if is_cancelled() {
545 return vec![];
546 }
547 variables::add_special_variables(&mut completions, &context, "$");
548 } else if context.prefix.starts_with('$') {
549 variables::add_variable_completions(
551 &mut completions,
552 &context,
553 SymbolKind::scalar(),
554 &self.symbol_table,
555 );
556 if is_cancelled() {
557 return vec![];
558 }
559 variables::add_special_variables(&mut completions, &context, "$");
560 } else if context.prefix.starts_with('@') && context.prefix.contains("::") {
561 packages::add_package_completions(&mut completions, &context, &self.workspace_index);
562 if !completions.is_empty() {
563 return completions;
564 }
565 variables::add_variable_completions(
566 &mut completions,
567 &context,
568 SymbolKind::array(),
569 &self.symbol_table,
570 );
571 if is_cancelled() {
572 return vec![];
573 }
574 variables::add_special_variables(&mut completions, &context, "@");
575 } else if context.prefix.starts_with('@') {
576 variables::add_variable_completions(
578 &mut completions,
579 &context,
580 SymbolKind::array(),
581 &self.symbol_table,
582 );
583 if is_cancelled() {
584 return vec![];
585 }
586 variables::add_special_variables(&mut completions, &context, "@");
587 } else if context.prefix.starts_with('%') && context.prefix.contains("::") {
588 packages::add_package_completions(&mut completions, &context, &self.workspace_index);
589 if !completions.is_empty() {
590 return completions;
591 }
592 variables::add_variable_completions(
593 &mut completions,
594 &context,
595 SymbolKind::hash(),
596 &self.symbol_table,
597 );
598 if is_cancelled() {
599 return vec![];
600 }
601 variables::add_special_variables(&mut completions, &context, "%");
602 } else if context.prefix.starts_with('%') {
603 variables::add_variable_completions(
605 &mut completions,
606 &context,
607 SymbolKind::hash(),
608 &self.symbol_table,
609 );
610 if is_cancelled() {
611 return vec![];
612 }
613 variables::add_special_variables(&mut completions, &context, "%");
614 } else if context.prefix.starts_with('&') {
615 functions::add_function_completions(&mut completions, &context, &self.symbol_table);
617 } else if context.trigger_character == Some(':') && context.prefix.ends_with("::") {
618 packages::add_package_completions(&mut completions, &context, &self.workspace_index);
620 } else if context.in_string {
621 let line_prefix = &source[..context.position];
623 if let Some(start) = line_prefix.rfind(['"', '\'']) {
624 let quote_char = match source.get(start..).and_then(|s| s.chars().next()) {
627 Some(c) => c,
628 None => return completions, };
630 let string_end = source[start + 1..]
631 .find(quote_char)
632 .map(|i| start + 1 + i)
633 .unwrap_or(source.len());
634 let full_string_content = &source[start + 1..string_end];
635
636 if full_string_content.contains('\0') {
638 return completions; }
640
641 let path_prefix = &line_prefix[start + 1..];
642 if path_prefix.contains('/')
644 || path_prefix.contains('\\') || path_prefix
646 .chars()
647 .all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')
648 {
649 let file_context = file_path::FileCompletionContext::new(
650 path_prefix,
651 start + 1,
652 context.position,
653 );
654 completions.extend(file_path::complete_file_paths(&file_context, is_cancelled));
655 }
656 }
657 } else {
658 let keywords = keywords::keywords();
660 if context.prefix.is_empty() || self.could_be_keyword(&context.prefix, keywords) {
661 keywords::add_keyword_completions(&mut completions, &context, keywords);
662 if is_cancelled() {
663 return vec![];
664 }
665 }
666
667 let builtins = builtins::create_builtins();
668 if context.prefix.is_empty() || self.could_be_function(&context.prefix, &builtins) {
669 builtins::add_builtin_completions(&mut completions, &context, &builtins);
670 if is_cancelled() {
671 return vec![];
672 }
673 functions::add_function_completions(&mut completions, &context, &self.symbol_table);
674 if is_cancelled() {
675 return vec![];
676 }
677 }
678
679 snippets::add_snippet_completions(&mut completions, &context);
681 if is_cancelled() {
682 return vec![];
683 }
684
685 variables::add_all_variables(&mut completions, &context, &self.symbol_table);
687 if is_cancelled() {
688 return vec![];
689 }
690
691 workspace::add_workspace_symbol_completions(
693 &mut completions,
694 &context,
695 &self.workspace_index,
696 &self.import_map,
697 );
698 if is_cancelled() {
699 return vec![];
700 }
701
702 if self.is_test_context(source, filepath) {
704 test_more::add_test_more_completions(&mut completions, &context);
705 }
706 }
707
708 sort::deduplicate_and_sort(completions)
710 }
711
712 pub fn get_completions(&self, source: &str, position: usize) -> Vec<CompletionItem> {
753 self.get_completions_with_path(source, position, None)
754 }
755
756 fn detect_use_qw_import_context(source: &str, position: usize) -> Option<(String, String)> {
764 if !source.is_char_boundary(position) {
765 return None;
766 }
767 let before = &source[..position];
768 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
769 let line = before[line_start..].trim_start();
770
771 let rest = line.strip_prefix("use ")?;
773 let rest = rest.trim_start();
774
775 let mod_end =
777 rest.find(|c: char| !c.is_alphanumeric() && c != ':' && c != '_').unwrap_or(rest.len());
778 if mod_end == 0 {
779 return None;
780 }
781 let module_name = &rest[..mod_end];
782
783 if !module_name.starts_with(|c: char| c.is_ascii_uppercase()) {
785 return None;
786 }
787
788 let after_module = &rest[mod_end..];
789
790 let qw_pos = after_module.find("qw")?;
792 let after_qw = &after_module[qw_pos + 2..];
793 let after_qw = after_qw.trim_start();
794
795 let first_char = after_qw.chars().next()?;
797 let close_delim = match first_char {
798 '(' => ')',
799 '[' => ']',
800 '{' => '}',
801 '<' => '>',
802 other => other, };
804
805 let inside_qw = &after_qw[first_char.len_utf8()..];
806
807 if inside_qw.contains(close_delim) {
809 return None;
810 }
811
812 let prefix = inside_qw.rsplit(|c: char| c.is_ascii_whitespace()).next().unwrap_or("");
815
816 Some((module_name.to_string(), prefix.to_string()))
817 }
818
819 fn is_use_statement_context(source: &str, position: usize) -> bool {
828 if !source.is_char_boundary(position) {
830 return false;
831 }
832 let before = &source[..position];
833 let line_start = before.rfind('\n').map(|p| p + 1).unwrap_or(0);
835 let line = before[line_start..].trim_start();
836
837 if let Some(rest) = line.strip_prefix("use ") {
840 let rest = rest.trim_start();
843 if rest.contains(';') || rest.contains('(') || rest.contains("qw") {
845 return false;
846 }
847 let first_char = rest.chars().next();
851 first_char.is_none() || first_char.is_some_and(|c| c.is_ascii_uppercase())
854 } else if let Some(rest) = line.strip_prefix("require ") {
855 let rest = rest.trim_start();
856 !rest.contains(';')
857 } else {
858 false
859 }
860 }
861
862 fn analyze_context(&self, source: &str, position: usize) -> CompletionContext {
864 let (word_prefix, prefix_start) = if position >= 2
867 && &source[position.saturating_sub(2)..position] == "->"
868 {
869 let receiver_start = source[..position.saturating_sub(2)]
873 .rfind(|c: char| {
874 !c.is_alphanumeric() && c != '_' && c != '$' && c != '@' && c != '%' && c != ':'
875 })
876 .map(|p| p + 1)
877 .unwrap_or(0);
878 (source[receiver_start..position].to_string(), receiver_start)
879 } else if position >= 1
880 && source.as_bytes()[position - 1] == b'-'
881 && (position < 2 || source.as_bytes()[position - 2] != b'-')
882 {
883 let receiver_start = source[..position.saturating_sub(1)]
888 .rfind(|c: char| {
889 !c.is_alphanumeric() && c != '_' && c != '$' && c != '@' && c != '%' && c != ':'
890 })
891 .map(|p| p + 1)
892 .unwrap_or(0);
893 let receiver = &source[receiver_start..position - 1];
894 (format!("{receiver}->"), receiver_start)
895 } else {
896 let word_start = source[..position]
897 .rfind(|c: char| {
898 !c.is_alphanumeric()
899 && c != '_'
900 && c != ':'
901 && c != '$'
902 && c != '@'
903 && c != '%'
904 && c != '&'
905 })
906 .map(|p| p + 1)
907 .unwrap_or(0);
908 (source[word_start..position].to_string(), word_start)
909 };
910
911 let trigger_character = if position > 0 {
913 let b = source.as_bytes()[position - 1];
914 if b.is_ascii() { Some(b as char) } else { None }
915 } else {
916 None
917 };
918
919 let in_string = self.is_in_string(source, position);
921 let in_regex = Self::is_in_regex(source, position);
922 let in_comment = self.is_in_comment(source, position);
923
924 let mut context = CompletionContext::new(
925 &self.symbol_table,
926 position,
927 trigger_character,
928 in_string,
929 in_regex,
930 in_comment,
931 word_prefix,
932 prefix_start,
933 );
934 context.cursor_scope_id =
935 scope_distance::scope_at_position(&self.symbol_table, source, position);
936 context
937 }
938
939 #[cfg(not(target_arch = "wasm32"))]
941 #[allow(dead_code)] fn add_file_completions(
943 &self,
944 completions: &mut Vec<CompletionItem>,
945 context: &CompletionContext,
946 ) {
947 self.add_file_completions_with_cancellation(completions, context, &|| false);
948 }
949
950 #[cfg(target_arch = "wasm32")]
952 #[allow(dead_code)] fn add_file_completions(
954 &self,
955 completions: &mut Vec<CompletionItem>,
956 context: &CompletionContext,
957 ) {
958 let _ = (completions, context);
960 }
961
962 #[cfg(not(target_arch = "wasm32"))]
967 fn add_file_completions_with_cancellation(
968 &self,
969 completions: &mut Vec<CompletionItem>,
970 context: &CompletionContext,
971 is_cancelled: &dyn Fn() -> bool,
972 ) {
973 completions.extend(file_path::complete_file_paths(
974 &file_path::FileCompletionContext::new(
975 &context.prefix,
976 context.prefix_start,
977 context.position,
978 ),
979 is_cancelled,
980 ));
981 }
982
983 #[cfg(target_arch = "wasm32")]
985 fn add_file_completions_with_cancellation(
986 &self,
987 completions: &mut Vec<CompletionItem>,
988 context: &CompletionContext,
989 _is_cancelled: &dyn Fn() -> bool,
990 ) {
991 let _ = (completions, context, _is_cancelled);
993 }
994
995 fn is_has_options_key_context(&self, source: &str, position: usize) -> bool {
997 if position > source.len() {
998 return false;
999 }
1000
1001 let prefix = &source[..position];
1002 let statement_start = prefix.rfind(';').map(|idx| idx + 1).unwrap_or(0);
1003 let statement = &prefix[statement_start..];
1004
1005 let Some(has_idx) = Self::find_keyword(statement, "has") else {
1006 return false;
1007 };
1008 let after_has = &statement[has_idx + 3..];
1009
1010 let Some(arrow_idx) = after_has.find("=>") else {
1011 return false;
1012 };
1013 let after_arrow = &after_has[arrow_idx + 2..];
1014
1015 let Some(open_idx) = after_arrow.find('(') else {
1016 return false;
1017 };
1018 let options_text = &after_arrow[open_idx + 1..];
1019
1020 let mut paren_depth = 1i32;
1022 for ch in options_text.chars() {
1023 if ch == '(' {
1024 paren_depth += 1;
1025 } else if ch == ')' {
1026 paren_depth -= 1;
1027 if paren_depth <= 0 {
1028 return false;
1029 }
1030 }
1031 }
1032
1033 let mut depth = 1i32;
1035 let mut segment_start = 0usize;
1036 for (idx, ch) in options_text.char_indices() {
1037 if ch == '(' {
1038 depth += 1;
1039 } else if ch == ')' {
1040 depth -= 1;
1041 } else if ch == ',' && depth == 1 {
1042 segment_start = idx + 1;
1043 }
1044 }
1045
1046 let segment = options_text[segment_start..].trim_start();
1047 if segment.is_empty() {
1048 return true;
1049 }
1050
1051 if segment.contains("=>") {
1053 return false;
1054 }
1055
1056 segment
1057 .chars()
1058 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch.is_ascii_whitespace())
1059 }
1060
1061 fn find_keyword(text: &str, keyword: &str) -> Option<usize> {
1063 let mut start = 0usize;
1064 while let Some(rel_idx) = text[start..].find(keyword) {
1065 let idx = start + rel_idx;
1066 let before = text[..idx].chars().next_back();
1067 let after = text[idx + keyword.len()..].chars().next();
1068
1069 let before_ok = before.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_');
1070 let after_ok = after.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_');
1071 if before_ok && after_ok {
1072 return Some(idx);
1073 }
1074
1075 start = idx + keyword.len();
1076 }
1077 None
1078 }
1079
1080 fn add_has_option_completions(
1082 &self,
1083 completions: &mut Vec<CompletionItem>,
1084 context: &CompletionContext,
1085 ) {
1086 let prefix = context.prefix.trim();
1087 let options = [
1088 ("is", "Accessor mode (`ro`, `rw`, or `rwp`)"),
1089 ("isa", "Type constraint for this attribute"),
1090 ("default", "Default value or builder closure"),
1091 ("required", "Require attribute during construction"),
1092 ("lazy", "Delay default computation until first access"),
1093 ("builder", "Method name used to build the default value"),
1094 ("reader", "Custom reader method name"),
1095 ("writer", "Custom writer method name"),
1096 ("accessor", "Custom combined read/write accessor"),
1097 ("predicate", "Method name to test if attribute is set"),
1098 ("clearer", "Method name to clear attribute value"),
1099 ("handles", "Delegated methods for referenced object"),
1100 ];
1101
1102 for (label, doc) in options {
1103 if prefix.is_empty() || label.starts_with(prefix) {
1104 completions.push(CompletionItem {
1105 label: label.to_string(),
1106 kind: CompletionItemKind::Property,
1107 detail: Some("Moo/Moose option".to_string()),
1108 documentation: Some(doc.to_string()),
1109 insert_text: Some(format!("{label} => ")),
1110 sort_text: Some(format!("0_{label}")),
1111 filter_text: Some(label.to_string()),
1112 additional_edits: vec![],
1113 text_edit_range: Some((context.prefix_start, context.position)),
1114 commit_characters: None,
1115 });
1116 }
1117 }
1118 }
1119
1120 fn could_be_keyword(&self, prefix: &str, keywords: &[&'static str]) -> bool {
1122 keywords.iter().any(|k| k.starts_with(prefix))
1123 }
1124
1125 fn could_be_function(
1127 &self,
1128 prefix: &str,
1129 builtins: &std::collections::HashSet<&'static str>,
1130 ) -> bool {
1131 if builtins.iter().any(|b| b.starts_with(prefix)) {
1133 return true;
1134 }
1135
1136 for (name, symbols) in &self.symbol_table.symbols {
1138 for symbol in symbols {
1139 if symbol.kind == SymbolKind::Subroutine && name.starts_with(prefix) {
1140 return true;
1141 }
1142 }
1143 }
1144
1145 false
1146 }
1147
1148 fn is_in_string(&self, source: &str, position: usize) -> bool {
1150 let before = &source[..position];
1151 let single_quotes = before.matches('\'').count();
1152 let double_quotes = before.matches('"').count();
1153
1154 single_quotes % 2 == 1 || double_quotes % 2 == 1
1156 }
1157
1158 fn is_in_regex(source: &str, position: usize) -> bool {
1165 let before = &source[..position];
1166
1167 let Some(last_slash) = before.rfind('/') else {
1170 return false;
1171 };
1172
1173 let pre_slash = before[..last_slash].trim_end();
1175 if pre_slash.ends_with("=~") || pre_slash.ends_with("!~") {
1176 return true;
1177 }
1178
1179 if Self::pre_slash_has_regex_op(pre_slash) {
1183 return true;
1184 }
1185
1186 if matches!(
1187 pre_slash.split_ascii_whitespace().next_back(),
1188 Some("or") | Some("and") | Some("not")
1189 ) {
1190 return true;
1191 }
1192
1193 if let Some(last_char) = pre_slash.chars().next_back() {
1195 if matches!(last_char, '(' | ',' | '=' | '!' | '&' | '|' | ';' | '{' | '~') {
1198 return true;
1199 }
1200 }
1201
1202 pre_slash.is_empty()
1204 }
1205
1206 fn pre_slash_has_regex_op(pre_slash: &str) -> bool {
1209 let trimmed = pre_slash.trim_end();
1210 for op in &["qr", "m", "s", "tr", "y"] {
1211 if let Some(before_op) = trimmed.strip_suffix(op) {
1212 let boundary_ok = before_op
1215 .chars()
1216 .next_back()
1217 .is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_');
1218 if boundary_ok {
1219 return true;
1220 }
1221 }
1222 }
1223 false
1224 }
1225
1226 pub(crate) fn is_in_regex_flags(source: &str, position: usize) -> bool {
1244 if position == 0 || position > source.len() {
1245 return false;
1246 }
1247 let before = &source[..position];
1248 let flag_chars: &[char] =
1249 &['g', 'i', 'm', 's', 'x', 'e', 'r', 'a', 'd', 'u', 'p', 'l', 'c'];
1250 let without_flags = before.trim_end_matches(|c: char| flag_chars.contains(&c));
1251 if !without_flags.ends_with('/') {
1253 return false;
1254 }
1255 let close_pos = without_flags.len();
1256 if close_pos < 2 {
1257 return false;
1258 }
1259
1260 if Self::is_in_regex(source, close_pos - 1) {
1263 return true;
1264 }
1265
1266 let body = without_flags.trim();
1271 Self::is_multi_delim_regex_at_close(body)
1272 }
1273
1274 fn is_multi_delim_regex_at_close(text: &str) -> bool {
1277 let (op_len, required_slashes) = if text.starts_with("tr/") || text.starts_with("y/") {
1279 let op = if text.starts_with("tr/") { 2 } else { 1 };
1280 (op, 3usize) } else if text.starts_with("s/") {
1282 (1, 3usize) } else {
1284 let stripped = text
1287 .find("=~")
1288 .map(|p| text[p + 2..].trim_start())
1289 .or_else(|| text.find("!~").map(|p| text[p + 2..].trim_start()));
1290 if let Some(rhs) = stripped {
1291 return Self::is_multi_delim_regex_at_close(rhs);
1292 }
1293 return false;
1294 };
1295 let body_after_op = &text[op_len..];
1297 let slash_count = Self::count_unescaped_slashes(body_after_op);
1298 slash_count == required_slashes
1299 }
1300
1301 fn count_unescaped_slashes(s: &str) -> usize {
1303 let mut count = 0usize;
1304 let mut escaped = false;
1305 for ch in s.chars() {
1306 if escaped {
1307 escaped = false;
1308 } else if ch == '\\' {
1309 escaped = true;
1310 } else if ch == '/' {
1311 count += 1;
1312 }
1313 }
1314 count
1315 }
1316
1317 fn is_in_comment(&self, source: &str, position: usize) -> bool {
1319 let line_start = source[..position].rfind('\n').map(|p| p + 1).unwrap_or(0);
1320 let line = &source[line_start..position];
1321 line.contains('#')
1322 }
1323
1324 fn is_test_context(&self, source: &str, filepath: Option<&str>) -> bool {
1326 if let Some(path) = filepath
1328 && path.ends_with(".t")
1329 {
1330 return true;
1331 }
1332
1333 source.contains("use Test::More") || source.contains("use Test2::V0")
1335 }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340 use super::*;
1341 use perl_parser_core::Parser;
1342 use perl_tdd_support::{must, must_some};
1343 use perl_workspace_index::workspace_index::WorkspaceIndex;
1344 use std::sync::Arc;
1345 use url::Url;
1346
1347 #[test]
1348 fn test_variable_completion() {
1349 let code = r#"
1350my $count = 42;
1351my $counter = 0;
1352my @items = ();
1353
1354$c
1355"#;
1356
1357 let mut parser = Parser::new(code);
1358 let ast = must(parser.parse());
1359
1360 let provider = CompletionProvider::new(&ast);
1361 let completions = provider.get_completions(code, code.len() - 1);
1362
1363 assert!(completions.iter().any(|c| c.label == "$count"));
1364 assert!(completions.iter().any(|c| c.label == "$counter"));
1365 }
1366
1367 #[test]
1368 fn test_function_completion() {
1369 let code = r#"
1370sub process_data {
1371 # ...
1372}
1373
1374sub process_items {
1375 # ...
1376}
1377
1378proc
1379"#;
1380
1381 let mut parser = Parser::new(code);
1382 let ast = must(parser.parse());
1383
1384 let provider = CompletionProvider::new(&ast);
1385 let completions = provider.get_completions(code, code.len() - 1);
1386
1387 assert!(completions.iter().any(|c| c.label == "process_data"));
1388 assert!(completions.iter().any(|c| c.label == "process_items"));
1389 }
1390
1391 #[test]
1392 fn test_builtin_completion() {
1393 let code = "pr";
1394
1395 let mut parser = Parser::new(""); let ast = must(parser.parse());
1397
1398 let provider = CompletionProvider::new(&ast);
1399 let completions = provider.get_completions(code, code.len());
1400
1401 assert!(completions.iter().any(|c| c.label == "print"));
1402 assert!(completions.iter().any(|c| c.label == "printf"));
1403 }
1404
1405 #[test]
1406 fn test_current_package_detection() {
1407 let code = r#"package Foo;
1408my $x = 1;
1409$x
1410"#;
1411
1412 let mut parser = Parser::new(code);
1413 let ast = must(parser.parse());
1414 let provider = CompletionProvider::new(&ast);
1415
1416 let context = provider.analyze_context(code, code.len());
1418 assert_eq!(context.current_package, "Foo");
1419 }
1420
1421 #[test]
1422 fn test_package_block_detection() {
1423 let code = r#"package Foo {
1424 my $x;
1425 $x;
1426}
1427package Bar;
1428$"#;
1429
1430 let mut parser = Parser::new(code);
1431 let ast = must(parser.parse());
1432 let provider = CompletionProvider::new(&ast);
1433
1434 let pos_foo = must_some(code.find("$x;")) + 2; let ctx_foo = provider.analyze_context(code, pos_foo);
1437 assert_eq!(ctx_foo.current_package, "Foo");
1438
1439 let pos_bar = code.len();
1441 let ctx_bar = provider.analyze_context(code, pos_bar);
1442 assert_eq!(ctx_bar.current_package, "Bar");
1443 }
1444
1445 #[test]
1446 fn test_incomplete_nested_block_scope_context() {
1447 let code = concat!(
1448 "my $file_var = 0;\n",
1449 "sub process {\n",
1450 " my $sub_var = 1;\n",
1451 " if (1) {\n",
1452 " my $block_var = 2;\n",
1453 " $"
1454 );
1455
1456 let mut parser = Parser::new(code);
1457 let ast = must(parser.parse());
1458 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1459 let context = provider.analyze_context(code, code.len());
1460
1461 let sub_scope = must_some(
1462 provider
1463 .symbol_table
1464 .symbols
1465 .get("sub_var")
1466 .and_then(|symbols| symbols.first())
1467 .map(|symbol| symbol.scope_id),
1468 );
1469 let block_scope = must_some(
1470 provider
1471 .symbol_table
1472 .symbols
1473 .get("block_var")
1474 .and_then(|symbols| symbols.first())
1475 .map(|symbol| symbol.scope_id),
1476 );
1477
1478 assert_eq!(
1479 context.cursor_scope_id, block_scope,
1480 "expected cursor scope to match block_var scope in incomplete nested block; cursor={:?} sub={:?} block={:?}",
1481 context.cursor_scope_id, sub_scope, block_scope
1482 );
1483 }
1484
1485 #[test]
1486 fn test_incomplete_nested_block_variable_sorting() {
1487 let code = concat!(
1488 "my $file_var = 0;\n",
1489 "sub process {\n",
1490 " my $sub_var = 1;\n",
1491 " if (1) {\n",
1492 " my $block_var = 2;\n",
1493 " $"
1494 );
1495
1496 let mut parser = Parser::new(code);
1497 let ast = must(parser.parse());
1498 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1499 let completions = provider.get_completions(code, code.len());
1500
1501 let block_item =
1502 must_some(completions.iter().find(|completion| completion.label == "$block_var"));
1503 let sub_item =
1504 must_some(completions.iter().find(|completion| completion.label == "$sub_var"));
1505
1506 assert!(
1507 block_item.sort_text < sub_item.sort_text,
1508 "expected incomplete block variable to outrank parent variable, got block={:?} sub={:?}",
1509 block_item.sort_text,
1510 sub_item.sort_text
1511 );
1512 }
1513
1514 #[test]
1515 fn test_package_member_completion() {
1516 let index = Arc::new(WorkspaceIndex::new());
1518 let module_uri = must(Url::parse("file:///workspace/MyModule.pm"));
1519 let module_code = r#"package MyModule;
1520our @EXPORT = qw(exported_sub);
1521sub exported_sub { }
1522sub internal_sub { }
15231;
1524"#;
1525 must(index.index_file(module_uri, module_code.to_string()));
1526
1527 let code = "use MyModule;\nMyModule::";
1529 let mut parser = Parser::new(code);
1530 let ast = must(parser.parse());
1531
1532 let provider = CompletionProvider::new_with_index(&ast, Some(index));
1533 let completions = provider.get_completions(code, code.len());
1534
1535 assert!(
1536 completions.iter().any(|c| c.label == "exported_sub"),
1537 "should suggest exported_sub"
1538 );
1539 let exported_sub =
1540 must_some(completions.iter().find(|completion| completion.label == "exported_sub"));
1541 let documentation = must_some(exported_sub.documentation.as_deref());
1542 assert!(
1543 documentation.contains("MyModule::exported_sub"),
1544 "expected package member doc to mention qualified symbol, got: {documentation:?}"
1545 );
1546 }
1547
1548 #[test]
1549 fn test_moo_accessor_method_completion() {
1550 let code = r#"
1551package Example::User;
1552use Moo;
1553
1554has 'name' => (is => 'ro', isa => 'Str');
1555
1556sub greet {
1557 my $self = shift;
1558 return $self->name;
1559}
1560"#;
1561
1562 let mut parser = Parser::new(code);
1563 let ast = must(parser.parse());
1564 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1565
1566 let synthesized = provider
1567 .symbol_table
1568 .symbols
1569 .get("name")
1570 .map(|symbols| symbols.iter().any(|symbol| symbol.kind == SymbolKind::Subroutine))
1571 .unwrap_or(false);
1572 assert!(synthesized, "expected synthesized `name` subroutine symbol in symbol table");
1573
1574 let pos = must_some(code.find("$self->name")) + "$self->".len();
1575 let completions = provider.get_completions(code, pos);
1576
1577 assert!(
1578 completions.iter().any(|item| item.label == "name"),
1579 "expected synthesized Moo accessor `name` in method completion"
1580 );
1581 }
1582
1583 #[test]
1584 fn test_moo_accessor_completion_shows_isa_type() {
1585 let code = r#"
1586package Example::User;
1587use Moo;
1588
1589has 'name' => (is => 'ro', isa => 'Str');
1590has 'age' => (is => 'rw', isa => 'Int');
1591
1592sub greet {
1593 my $self = shift;
1594 $self->
1595}
1596"#;
1597
1598 let mut parser = Parser::new(code);
1599 let ast = must(parser.parse());
1600 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1601
1602 let pos = must_some(code.find("$self->")) + "$self->".len();
1603 let completions = provider.get_completions(code, pos);
1604
1605 let name_item = must_some(completions.iter().find(|c| c.label == "name"));
1607 let name_doc = must_some(name_item.documentation.as_deref());
1608 assert!(
1609 name_doc.contains("Str"),
1610 "expected `Str` type in name accessor documentation, got: {name_doc:?}"
1611 );
1612
1613 let age_item = must_some(completions.iter().find(|c| c.label == "age"));
1615 let age_doc = must_some(age_item.documentation.as_deref());
1616 assert!(
1617 age_doc.contains("Int"),
1618 "expected `Int` type in age accessor documentation, got: {age_doc:?}"
1619 );
1620
1621 let name_detail = must_some(name_item.detail.as_deref());
1623 assert!(
1624 name_detail.contains("accessor"),
1625 "expected 'accessor' in detail for Moo attribute, got: {name_detail:?}"
1626 );
1627 }
1628
1629 #[test]
1630 fn test_moose_accessor_completion_shows_isa_type() {
1631 let code = r#"
1632package Example::Animal;
1633use Moose;
1634
1635has 'species' => (is => 'ro', isa => 'Str', required => 1);
1636
1637sub describe {
1638 my $self = shift;
1639 $self->
1640}
1641"#;
1642
1643 let mut parser = Parser::new(code);
1644 let ast = must(parser.parse());
1645 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1646
1647 let pos = must_some(code.find("$self->")) + "$self->".len();
1648 let completions = provider.get_completions(code, pos);
1649
1650 let species_item = must_some(completions.iter().find(|c| c.label == "species"));
1651 let species_doc = must_some(species_item.documentation.as_deref());
1652 assert!(
1653 species_doc.contains("Str"),
1654 "expected `Str` type in species accessor documentation, got: {species_doc:?}"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_moo_has_option_key_completion() {
1660 let code = r#"
1661use Moo;
1662has 'name' => (re
1663"#;
1664
1665 let mut parser = Parser::new(code);
1666 let ast = must(parser.parse());
1667 let provider = CompletionProvider::new_with_index_and_source(&ast, code, None);
1668
1669 let completions = provider.get_completions(code, code.len());
1670
1671 assert!(
1672 completions.iter().any(|item| item.label == "required"),
1673 "expected `required` option completion inside has(...) context"
1674 );
1675 assert!(
1676 completions.iter().any(|item| item.label == "reader"),
1677 "expected `reader` option completion inside has(...) context"
1678 );
1679 }
1680
1681 #[test]
1682 fn test_regex_completion_binding_operator() {
1683 let code = r#"my $x = "hello"; $x =~ /"#;
1685
1686 let mut parser = Parser::new(code);
1687 let ast = must(parser.parse());
1688 let provider = CompletionProvider::new(&ast);
1689 let completions = provider.get_completions(code, code.len());
1690
1691 assert!(
1693 completions.iter().any(|c| c.label == "\\d"),
1694 "expected \\d regex completion inside =~ /.../"
1695 );
1696 assert!(
1697 completions.iter().any(|c| c.label == "\\w"),
1698 "expected \\w regex completion inside =~ /.../"
1699 );
1700 assert!(
1701 completions.iter().any(|c| c.label == "(?:...)"),
1702 "expected non-capturing group regex completion"
1703 );
1704 }
1705
1706 #[test]
1707 fn test_regex_completion_negated_binding() {
1708 let code = r#"my $x = "test"; $x !~ /"#;
1709
1710 let mut parser = Parser::new(code);
1711 let ast = must(parser.parse());
1712 let provider = CompletionProvider::new(&ast);
1713 let completions = provider.get_completions(code, code.len());
1714
1715 assert!(
1716 completions.iter().any(|c| c.label == "\\d"),
1717 "expected regex completions after !~"
1718 );
1719 }
1720
1721 #[test]
1722 fn test_regex_completion_m_operator() {
1723 let code = "if ($line =~ m/";
1724
1725 let mut parser = Parser::new(code);
1726 let ast = must(parser.parse());
1727 let provider = CompletionProvider::new(&ast);
1728 let completions = provider.get_completions(code, code.len());
1729
1730 assert!(
1731 completions.iter().any(|c| c.label == "\\d"),
1732 "expected regex completions inside m/.../"
1733 );
1734 assert!(
1735 completions.iter().any(|c| c.label == "^"),
1736 "expected anchor completions inside m/.../"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_regex_completion_qr_operator() {
1742 let code = "my $re = qr/";
1743
1744 let mut parser = Parser::new(code);
1745 let ast = must(parser.parse());
1746 let provider = CompletionProvider::new(&ast);
1747 let completions = provider.get_completions(code, code.len());
1748
1749 assert!(
1750 completions.iter().any(|c| c.label == "\\d+"),
1751 "expected common pattern completions inside qr/.../"
1752 );
1753 assert!(
1754 completions.iter().any(|c| c.label == "(?=...)"),
1755 "expected lookahead group completion inside qr/.../"
1756 );
1757 }
1758
1759 #[test]
1760 fn test_regex_completion_s_operator() {
1761 let code = "($line = $input) =~ s/";
1762
1763 let mut parser = Parser::new(code);
1764 let ast = must(parser.parse());
1765 let provider = CompletionProvider::new(&ast);
1766 let completions = provider.get_completions(code, code.len());
1767
1768 assert!(
1769 completions.iter().any(|c| c.label == "\\s+"),
1770 "expected common pattern completions inside s/.../"
1771 );
1772 }
1773
1774 #[test]
1775 fn test_regex_completion_has_all_categories() {
1776 let code = r#"$x =~ /"#;
1777
1778 let mut parser = Parser::new(code);
1779 let ast = must(parser.parse());
1780 let provider = CompletionProvider::new(&ast);
1781 let completions = provider.get_completions(code, code.len());
1782
1783 assert!(completions.iter().any(|c| c.label == "\\d"));
1785 assert!(completions.iter().any(|c| c.label == "\\D"));
1786 assert!(completions.iter().any(|c| c.label == "\\w"));
1787 assert!(completions.iter().any(|c| c.label == "\\W"));
1788 assert!(completions.iter().any(|c| c.label == "\\s"));
1789 assert!(completions.iter().any(|c| c.label == "\\S"));
1790 assert!(completions.iter().any(|c| c.label == "[...]"));
1791 assert!(completions.iter().any(|c| c.label == "[^...]"));
1792
1793 assert!(completions.iter().any(|c| c.label == "^"));
1795 assert!(completions.iter().any(|c| c.label == "$"));
1796 assert!(completions.iter().any(|c| c.label == "\\b"));
1797 assert!(completions.iter().any(|c| c.label == "\\B"));
1798 assert!(completions.iter().any(|c| c.label == "\\A"));
1799 assert!(completions.iter().any(|c| c.label == "\\z"));
1800 assert!(completions.iter().any(|c| c.label == "\\Z"));
1801
1802 assert!(completions.iter().any(|c| c.label == "*"));
1804 assert!(completions.iter().any(|c| c.label == "+"));
1805 assert!(completions.iter().any(|c| c.label == "?"));
1806 assert!(completions.iter().any(|c| c.label == "{n}"));
1807 assert!(completions.iter().any(|c| c.label == "{n,}"));
1808 assert!(completions.iter().any(|c| c.label == "{n,m}"));
1809
1810 assert!(completions.iter().any(|c| c.label == "(...)"));
1812 assert!(completions.iter().any(|c| c.label == "(?:...)"));
1813 assert!(completions.iter().any(|c| c.label == "(?=...)"));
1814 assert!(completions.iter().any(|c| c.label == "(?!...)"));
1815 assert!(completions.iter().any(|c| c.label == "(?<=...)"));
1816 assert!(completions.iter().any(|c| c.label == "(?<!...)"));
1817
1818 assert!(completions.iter().any(|c| c.label == "\\d+"));
1820 assert!(completions.iter().any(|c| c.label == "\\w+"));
1821 assert!(completions.iter().any(|c| c.label == "\\s+"));
1822 assert!(completions.iter().any(|c| c.label == ".*?"));
1823 assert!(completions.iter().any(|c| c.label == ".+?"));
1824 }
1825
1826 #[test]
1827 fn test_regex_completion_items_have_correct_kind() {
1828 let code = r#"$x =~ /"#;
1829
1830 let mut parser = Parser::new(code);
1831 let ast = must(parser.parse());
1832 let provider = CompletionProvider::new(&ast);
1833 let completions = provider.get_completions(code, code.len());
1834
1835 for item in &completions {
1836 assert_eq!(
1837 item.kind,
1838 CompletionItemKind::Snippet,
1839 "regex completion '{}' should be Snippet kind",
1840 item.label
1841 );
1842 }
1843 }
1844
1845 #[test]
1846 fn test_regex_completion_items_have_documentation() {
1847 let code = r#"$x =~ /"#;
1848
1849 let mut parser = Parser::new(code);
1850 let ast = must(parser.parse());
1851 let provider = CompletionProvider::new(&ast);
1852 let completions = provider.get_completions(code, code.len());
1853
1854 for item in &completions {
1855 assert!(
1856 item.documentation.is_some(),
1857 "regex completion '{}' should have documentation",
1858 item.label
1859 );
1860 assert!(item.detail.is_some(), "regex completion '{}' should have detail", item.label);
1861 }
1862 }
1863
1864 #[test]
1865 fn test_regex_completion_not_in_normal_context() {
1866 let code = "my $x = 1;\n";
1868
1869 let mut parser = Parser::new(code);
1870 let ast = must(parser.parse());
1871 let provider = CompletionProvider::new(&ast);
1872 let completions = provider.get_completions(code, code.len());
1873
1874 assert!(
1875 !completions.iter().any(|c| c.label == "\\d"),
1876 "regex completions should NOT appear outside regex context"
1877 );
1878 }
1879
1880 #[test]
1881 fn test_is_in_regex_binding_operator() {
1882 let code = r#"$x =~ /hello"#;
1883 assert!(CompletionProvider::is_in_regex(code, code.len()));
1884 }
1885
1886 #[test]
1887 fn test_is_in_regex_m_operator() {
1888 let code = "m/pattern";
1889 assert!(CompletionProvider::is_in_regex(code, code.len()));
1890 }
1891
1892 #[test]
1893 fn test_is_in_regex_qr_operator() {
1894 let code = "my $re = qr/pattern";
1895 assert!(CompletionProvider::is_in_regex(code, code.len()));
1896 }
1897
1898 #[test]
1899 fn test_is_in_regex_s_operator() {
1900 let code = "$line =~ s/old";
1901 assert!(CompletionProvider::is_in_regex(code, code.len()));
1902 }
1903
1904 #[test]
1905 fn test_is_in_regex_keyword_operator() {
1906 let code = "$x or /pattern";
1907 assert!(CompletionProvider::is_in_regex(code, code.len()));
1908 }
1909
1910 #[test]
1911 fn test_is_not_in_regex_division() {
1912 let code = "my $result = $x / $y";
1914 assert!(
1917 !CompletionProvider::is_in_regex(code, code.len()),
1918 "division should not be detected as regex context"
1919 );
1920 }
1921
1922 #[test]
1923 fn test_regex_completion_preserves_sigil_completions_in_interpolation() {
1924 let code = r#"my $foo = 1; my $bar = qr/^$fo/"#;
1927 let pos = code.len() - 1;
1929
1930 let mut parser = Parser::new(code);
1931 let ast = must(parser.parse());
1932 let provider = CompletionProvider::new(&ast);
1933 let completions = provider.get_completions(code, pos);
1934
1935 assert!(
1936 completions.iter().any(|item| item.label == "$foo"),
1937 "expected interpolated regex variables to keep scalar completions"
1938 );
1939 }
1940
1941 #[test]
1942 fn test_regex_completion_replaces_escape_prefix_range() {
1943 let code = r#"$x =~ /\d"#;
1944
1945 let mut parser = Parser::new(code);
1946 let ast = must(parser.parse());
1947 let provider = CompletionProvider::new(&ast);
1948 let completions = provider.get_completions(code, code.len());
1949
1950 let item = must_some(completions.iter().find(|completion| completion.label == r"\d"));
1951 assert_eq!(
1952 item.text_edit_range,
1953 Some((code.len() - r"\d".len(), code.len())),
1954 "expected regex completion to replace the typed escape sequence"
1955 );
1956 }
1957
1958 #[test]
1959 fn test_regex_completion_replaces_group_prefix_range() {
1960 let code = r#"$x =~ /(?: "#;
1961 let code = code.trim_end();
1962
1963 let mut parser = Parser::new(code);
1964 let ast = must(parser.parse());
1965 let provider = CompletionProvider::new(&ast);
1966 let completions = provider.get_completions(code, code.len());
1967
1968 let item = must_some(completions.iter().find(|completion| completion.label == "(?:...)"));
1969 assert_eq!(
1970 item.text_edit_range,
1971 Some((code.len() - "(?:".len(), code.len())),
1972 "expected regex completion to replace the typed group opener"
1973 );
1974 }
1975
1976 #[test]
1977 fn test_detect_use_qw_import_context_basic() {
1978 let code = "use MyModule qw(";
1980 let result = CompletionProvider::detect_use_qw_import_context(code, code.len());
1981 assert!(result.is_some(), "should detect qw() import context");
1982 let (module, prefix) =
1983 result.as_ref().map(|(m, p)| (m.as_str(), p.as_str())).unwrap_or_default();
1984 assert_eq!(module, "MyModule");
1985 assert_eq!(prefix, "");
1986 }
1987
1988 #[test]
1989 fn test_detect_use_qw_import_context_with_prefix() {
1990 let code = "use File::Basename qw(bas";
1991 let result = CompletionProvider::detect_use_qw_import_context(code, code.len());
1992 assert!(result.is_some(), "should detect qw() import context with prefix");
1993 let (module, prefix) =
1994 result.as_ref().map(|(m, p)| (m.as_str(), p.as_str())).unwrap_or_default();
1995 assert_eq!(module, "File::Basename");
1996 assert_eq!(prefix, "bas");
1997 }
1998
1999 #[test]
2000 fn test_detect_use_qw_import_context_with_existing_imports() {
2001 let code = "use MyModule qw(foo bar ba";
2002 let result = CompletionProvider::detect_use_qw_import_context(code, code.len());
2003 assert!(result.is_some(), "should detect qw() import context after existing imports");
2004 let (module, prefix) =
2005 result.as_ref().map(|(m, p)| (m.as_str(), p.as_str())).unwrap_or_default();
2006 assert_eq!(module, "MyModule");
2007 assert_eq!(prefix, "ba");
2008 }
2009
2010 #[test]
2011 fn test_detect_use_qw_not_after_close() {
2012 let code = "use MyModule qw(foo bar);";
2014 let result = CompletionProvider::detect_use_qw_import_context(code, code.len());
2015 assert!(result.is_none(), "should not detect context after closing paren");
2016 }
2017
2018 #[test]
2019 fn test_detect_use_qw_not_for_pragmas() {
2020 let code = "use strict qw(";
2021 let result = CompletionProvider::detect_use_qw_import_context(code, code.len());
2022 assert!(result.is_none(), "should not detect context for lowercase pragmas");
2023 }
2024
2025 #[test]
2026 fn test_use_qw_import_completion_with_workspace() -> Result<(), Box<dyn std::error::Error>> {
2027 let index = Arc::new(WorkspaceIndex::new());
2029 let module_uri = Url::parse("file:///workspace/MyUtils.pm")?;
2030 let module_code = r#"package MyUtils;
2031use Exporter 'import';
2032our @EXPORT_OK = qw(helper_one helper_two);
2033sub helper_one { }
2034sub helper_two { }
2035sub _private_internal { }
20361;
2037"#;
2038 index.index_file(module_uri, module_code.to_string())?;
2039
2040 let code = "use MyUtils qw(hel";
2042 let mut parser = Parser::new(code);
2043 let ast = must(parser.parse());
2044
2045 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2046 let completions = provider.get_completions(code, code.len());
2047
2048 assert!(
2049 completions.iter().any(|c| c.label == "helper_one"),
2050 "should suggest helper_one from MyUtils: got {:?}",
2051 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2052 );
2053 assert!(
2054 completions.iter().any(|c| c.label == "helper_two"),
2055 "should suggest helper_two from MyUtils"
2056 );
2057 Ok(())
2058 }
2059
2060 #[test]
2061 fn test_use_qw_import_completion_empty_prefix() -> Result<(), Box<dyn std::error::Error>> {
2062 let index = Arc::new(WorkspaceIndex::new());
2063 let module_uri = Url::parse("file:///workspace/Utils.pm")?;
2064 let module_code = r#"package Utils;
2065sub alpha { }
2066sub beta { }
20671;
2068"#;
2069 index.index_file(module_uri, module_code.to_string())?;
2070
2071 let code = "use Utils qw(";
2073 let mut parser = Parser::new(code);
2074 let ast = must(parser.parse());
2075
2076 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2077 let completions = provider.get_completions(code, code.len());
2078
2079 assert!(
2080 completions.iter().any(|c| c.label == "alpha"),
2081 "should suggest alpha with empty prefix: got {:?}",
2082 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2083 );
2084 assert!(
2085 completions.iter().any(|c| c.label == "beta"),
2086 "should suggest beta with empty prefix"
2087 );
2088 Ok(())
2089 }
2090
2091 #[test]
2092 fn test_use_qw_import_completion_detail_shows_module() -> Result<(), Box<dyn std::error::Error>>
2093 {
2094 let index = Arc::new(WorkspaceIndex::new());
2095 let module_uri = Url::parse("file:///workspace/MyLib.pm")?;
2096 let module_code = r#"package MyLib;
2097sub do_work { }
20981;
2099"#;
2100 index.index_file(module_uri, module_code.to_string())?;
2101
2102 let code = "use MyLib qw(do";
2103 let mut parser = Parser::new(code);
2104 let ast = must(parser.parse());
2105
2106 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2107 let completions = provider.get_completions(code, code.len());
2108
2109 let do_work = completions.iter().find(|c| c.label == "do_work");
2110 assert!(do_work.is_some(), "should suggest do_work");
2111 let detail = must_some(do_work.and_then(|c| c.detail.as_deref()));
2112 assert!(detail.contains("MyLib"), "detail should mention module name, got: {detail:?}");
2113 Ok(())
2114 }
2115
2116 #[test]
2117 fn test_self_arrow_resolves_workspace_methods() -> Result<(), Box<dyn std::error::Error>> {
2118 let index = Arc::new(WorkspaceIndex::new());
2126 let module_uri = Url::parse("file:///workspace/MyService.pm")?;
2127 let module_code = r#"package MyService;
2128sub new { bless {}, shift }
2129sub process_request { }
2130sub validate_input { }
21311;
2132"#;
2133 index.index_file(module_uri, module_code.to_string())?;
2134
2135 let code = r#"package MyService;
2138sub run {
2139 my $self = shift;
2140 $self->"#;
2141 let mut parser = Parser::new(code);
2142 let ast = must(parser.parse());
2143
2144 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2145 let completions = provider.get_completions(code, code.len());
2146
2147 assert!(
2148 completions.iter().any(|c| c.label == "process_request"),
2149 "$self-> should suggest process_request from workspace index; got: {:?}",
2150 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2151 );
2152 assert!(
2153 completions.iter().any(|c| c.label == "validate_input"),
2154 "$self-> should suggest validate_input from workspace index"
2155 );
2156 Ok(())
2157 }
2158
2159 #[test]
2160 fn test_this_arrow_resolves_workspace_methods() -> Result<(), Box<dyn std::error::Error>> {
2161 let index = Arc::new(WorkspaceIndex::new());
2163 let module_uri = Url::parse("file:///workspace/MyHandler.pm")?;
2164 let module_code = r#"package MyHandler;
2165sub new { bless {}, shift }
2166sub handle { }
21671;
2168"#;
2169 index.index_file(module_uri, module_code.to_string())?;
2170
2171 let code = r#"package MyHandler;
2173sub run {
2174 my $this = shift;
2175 $this->"#;
2176 let mut parser = Parser::new(code);
2177 let ast = must(parser.parse());
2178
2179 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2180 let completions = provider.get_completions(code, code.len());
2181
2182 assert!(
2183 completions.iter().any(|c| c.label == "handle"),
2184 "$this-> should suggest handle from workspace index; got: {:?}",
2185 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2186 );
2187 Ok(())
2188 }
2189
2190 #[test]
2191 fn test_self_arrow_in_main_package_does_not_resolve() -> Result<(), Box<dyn std::error::Error>>
2192 {
2193 let index = Arc::new(WorkspaceIndex::new());
2197 let module_uri = Url::parse("file:///workspace/MyLib.pm")?;
2198 let module_code = r#"package MyLib;
2199sub new { bless {}, shift }
2200sub helper { }
22011;
2202"#;
2203 index.index_file(module_uri, module_code.to_string())?;
2204
2205 let code = r#"sub run {
2207 my $self = shift;
2208 $self->"#;
2209 let mut parser = Parser::new(code);
2210 let ast = must(parser.parse());
2211
2212 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2213 let completions = provider.get_completions(code, code.len());
2214
2215 assert!(
2217 !completions.iter().any(|c| c.label == "helper"),
2218 "$self-> in main package should not suggest methods from other packages"
2219 );
2220 Ok(())
2221 }
2222
2223 #[test]
2228 fn test_use_statement_context_after_use_keyword() -> Result<(), Box<dyn std::error::Error>> {
2229 let index = Arc::new(WorkspaceIndex::new());
2231 let uri = Url::parse("file:///lib/MyApp.pm")?;
2232 index.index_file(uri, "package MyApp;\n1;\n".to_string())?;
2233 let code = "use ";
2234 let mut parser = Parser::new(code);
2235 let ast = must(parser.parse());
2236 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2237 let completions = provider.get_completions(code, code.len());
2238 assert!(
2239 completions.iter().any(|c| c.label == "MyApp" && c.kind == CompletionItemKind::Module),
2240 "use <cursor> should suggest workspace module names; got: {:?}",
2241 completions.iter().map(|c| (&c.label, &c.kind)).collect::<Vec<_>>()
2242 );
2243 Ok(())
2244 }
2245
2246 #[test]
2247 fn test_use_statement_context_with_prefix() -> Result<(), Box<dyn std::error::Error>> {
2248 let index = Arc::new(WorkspaceIndex::new());
2250 index
2251 .index_file(Url::parse("file:///lib/MyApp.pm")?, "package MyApp;\n1;\n".to_string())?;
2252 index.index_file(
2253 Url::parse("file:///lib/OtherLib.pm")?,
2254 "package OtherLib;\n1;\n".to_string(),
2255 )?;
2256 let code = "use MyA";
2257 let mut parser = Parser::new(code);
2258 let ast = must(parser.parse());
2259 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2260 let completions = provider.get_completions(code, code.len());
2261 assert!(
2262 completions.iter().any(|c| c.label == "MyApp" && c.kind == CompletionItemKind::Module),
2263 "use MyA should suggest MyApp with Module kind"
2264 );
2265 assert!(
2266 !completions.iter().any(|c| c.label == "OtherLib"),
2267 "use MyA should not suggest OtherLib"
2268 );
2269 Ok(())
2270 }
2271
2272 #[test]
2273 fn test_use_statement_skips_pragmas() -> Result<(), Box<dyn std::error::Error>> {
2274 let index = Arc::new(WorkspaceIndex::new());
2279 index.index_file(
2280 Url::parse("file:///lib/Strict.pm")?,
2281 "package Strict;\n1;\n".to_string(),
2282 )?;
2283 let code = "use strict";
2284 let mut parser = Parser::new(code);
2285 let ast = must(parser.parse());
2286 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2287 let completions = provider.get_completions(code, code.len());
2288 assert!(
2289 !completions.iter().any(|c| c.kind == CompletionItemKind::Module),
2290 "use strict should not trigger module completions; got: {:?}",
2291 completions.iter().map(|c| (&c.label, &c.kind)).collect::<Vec<_>>()
2292 );
2293 Ok(())
2294 }
2295
2296 #[test]
2297 fn test_use_statement_skips_past_module_name_at_qw() -> Result<(), Box<dyn std::error::Error>> {
2298 let index = Arc::new(WorkspaceIndex::new());
2303 index.index_file(
2304 Url::parse("file:///lib/Module.pm")?,
2305 "package Module;\nsub foo {}\n1;\n".to_string(),
2306 )?;
2307 let code = "use Module qw(foo";
2308 let mut parser = Parser::new(code);
2309 let ast = must(parser.parse());
2310 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2311 let completions = provider.get_completions(code, code.len());
2312 assert!(
2315 !completions.iter().any(|c| c.kind == CompletionItemKind::Module),
2316 "cursor inside qw() should not get module-name completions; got: {:?}",
2317 completions.iter().map(|c| (&c.label, &c.kind)).collect::<Vec<_>>()
2318 );
2319 Ok(())
2320 }
2321
2322 #[test]
2323 fn test_require_statement_triggers_module_completion() -> Result<(), Box<dyn std::error::Error>>
2324 {
2325 let index = Arc::new(WorkspaceIndex::new());
2326 index
2327 .index_file(Url::parse("file:///lib/Utils.pm")?, "package Utils;\n1;\n".to_string())?;
2328 let code = "require Ut";
2329 let mut parser = Parser::new(code);
2330 let ast = must(parser.parse());
2331 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2332 let completions = provider.get_completions(code, code.len());
2333 assert!(
2334 completions.iter().any(|c| c.label == "Utils" && c.kind == CompletionItemKind::Module),
2335 "require Ut should suggest Utils with Module kind; got: {:?}",
2336 completions.iter().map(|c| (&c.label, &c.kind)).collect::<Vec<_>>()
2337 );
2338 Ok(())
2339 }
2340
2341 #[test]
2342 fn test_use_module_deduplication() -> Result<(), Box<dyn std::error::Error>> {
2343 let index = Arc::new(WorkspaceIndex::new());
2345 index
2346 .index_file(Url::parse("file:///lib/MyApp.pm")?, "package MyApp;\n1;\n".to_string())?;
2347 index.index_file(
2348 Url::parse("file:///lib/MyApp2.pm")?,
2349 "package MyApp;\n1;\n".to_string(), )?;
2351 let code = "use MyA";
2352 let mut parser = Parser::new(code);
2353 let ast = must(parser.parse());
2354 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2355 let completions = provider.get_completions(code, code.len());
2356 let myapp_count = completions.iter().filter(|c| c.label == "MyApp").count();
2357 assert_eq!(
2358 myapp_count, 1,
2359 "Duplicate package declarations should produce exactly one completion"
2360 );
2361 Ok(())
2362 }
2363
2364 #[test]
2365 fn test_use_module_non_use_context_excluded() -> Result<(), Box<dyn std::error::Error>> {
2366 let index = Arc::new(WorkspaceIndex::new());
2370 index.index_file(
2371 Url::parse("file:///lib/MyApp.pm")?,
2372 "package MyApp;\nsub hello {}\n1;\n".to_string(),
2373 )?;
2374 let code = "my $x = MyA";
2375 let mut parser = Parser::new(code);
2376 let ast = must(parser.parse());
2377 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2378 let completions = provider.get_completions(code, code.len());
2379 assert!(
2382 !completions.iter().any(|c| c.sort_text.as_deref() == Some("1_MyApp")),
2383 "Module-priority sort_text should only appear in use context"
2384 );
2385 Ok(())
2386 }
2387
2388 #[test]
2389 fn test_use_statement_past_semicolon_excluded() -> Result<(), Box<dyn std::error::Error>> {
2390 let index = Arc::new(WorkspaceIndex::new());
2395 index.index_file(
2396 Url::parse("file:///lib/Module.pm")?,
2397 "package Module;\n1;\n".to_string(),
2398 )?;
2399 let code = "use Module;";
2400 let mut parser = Parser::new(code);
2401 let ast = must(parser.parse());
2402 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2403 let completions = provider.get_completions(code, code.len());
2404 assert!(
2405 !completions.iter().any(|c| c.kind == CompletionItemKind::Module),
2406 "cursor after `use Module;` should not trigger module-name completions; got: {:?}",
2407 completions.iter().map(|c| (&c.label, &c.kind)).collect::<Vec<_>>()
2408 );
2409 Ok(())
2410 }
2411
2412 #[test]
2415 fn test_regex_named_capture_completion() {
2416 let code = r#"$x =~ /"#;
2418 let mut parser = Parser::new(code);
2419 let ast = must(parser.parse());
2420 let provider = CompletionProvider::new(&ast);
2421 let completions = provider.get_completions(code, code.len());
2422 assert!(
2423 completions.iter().any(|c| c.label == "(?<name>...)"),
2424 "expected named capture group in regex completions; got: {:?}",
2425 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2426 );
2427 }
2428
2429 #[test]
2430 fn test_regex_named_capture_prefix_disambig() {
2431 let code = r#"$x =~ /(?<"#;
2433 let mut parser = Parser::new(code);
2434 let ast = must(parser.parse());
2435 let provider = CompletionProvider::new(&ast);
2436 let completions = provider.get_completions(code, code.len());
2437 assert!(
2438 completions.iter().any(|c| c.label == "(?<=...)"),
2439 "expected lookbehind when prefix is (?<"
2440 );
2441 assert!(
2442 completions.iter().any(|c| c.label == "(?<name>...)"),
2443 "expected named capture when prefix is (?<"
2444 );
2445 }
2446
2447 #[test]
2448 fn test_regex_named_capture_prefix_lookbehind_only() {
2449 let code = r#"$x =~ /(?<="#;
2452 let mut parser = Parser::new(code);
2453 let ast = must(parser.parse());
2454 let provider = CompletionProvider::new(&ast);
2455 let completions = provider.get_completions(code, code.len());
2456 assert!(
2457 completions.iter().any(|c| c.label == "(?<=...)"),
2458 "expected lookbehind for prefix (?<="
2459 );
2460 assert!(
2461 !completions.iter().any(|c| c.label == "(?<name>...)"),
2462 "named capture should NOT appear for prefix (?<= (label doesn't start with (?<=)"
2463 );
2464 }
2465
2466 #[test]
2469 fn test_is_in_regex_flags_after_close_slash() {
2470 let code = "$x =~ /foo/";
2472 assert!(
2473 CompletionProvider::is_in_regex_flags(code, code.len()),
2474 "cursor right after closing / should be in regex-flags context"
2475 );
2476 }
2477
2478 #[test]
2479 fn test_is_in_regex_flags_after_partial_flag() {
2480 let code = "m/foo/i";
2482 assert!(
2483 CompletionProvider::is_in_regex_flags(code, code.len()),
2484 "cursor after /i should still be in regex-flags context"
2485 );
2486 }
2487
2488 #[test]
2489 fn test_is_in_regex_flags_s_operator() {
2490 let code = "s/foo/bar/g";
2491 assert!(
2492 CompletionProvider::is_in_regex_flags(code, code.len()),
2493 "s/// with /g flag should be in regex-flags context"
2494 );
2495 }
2496
2497 #[test]
2498 fn test_is_not_in_regex_flags_division() {
2499 let code = "my $x = $a / $b /";
2501 assert!(
2502 !CompletionProvider::is_in_regex_flags(code, code.len()),
2503 "division should not be detected as regex-flags context"
2504 );
2505 }
2506
2507 #[test]
2508 fn test_regex_flag_completions_after_close() {
2509 let code = "$x =~ /foo/";
2511 let mut parser = Parser::new(code);
2512 let ast = must(parser.parse());
2513 let provider = CompletionProvider::new(&ast);
2514 let completions = provider.get_completions(code, code.len());
2515 let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
2516 for flag in &["g", "i", "m", "s", "x", "e", "r", "a", "p"] {
2518 assert!(
2519 labels.contains(flag),
2520 "expected standard regex flag '{flag}' in completions; got: {labels:?}"
2521 );
2522 }
2523 }
2524
2525 #[test]
2526 fn test_regex_flag_completions_skip_already_typed() {
2527 let code = "$x =~ /foo/g";
2529 let mut parser = Parser::new(code);
2530 let ast = must(parser.parse());
2531 let provider = CompletionProvider::new(&ast);
2532 let completions = provider.get_completions(code, code.len());
2533 let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
2534 assert!(!labels.contains(&"g"), "already-typed flag 'g' should be excluded");
2535 assert!(labels.contains(&"i"), "flag 'i' should still be offered");
2536 }
2537
2538 #[test]
2539 fn test_regex_tr_flag_completions() {
2540 let code = "tr/a-z/A-Z/";
2542 let mut parser = Parser::new(code);
2543 let ast = must(parser.parse());
2544 let provider = CompletionProvider::new(&ast);
2545 let completions = provider.get_completions(code, code.len());
2546 let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
2547 for flag in &["c", "d", "s"] {
2548 assert!(
2549 labels.contains(flag),
2550 "tr/// flag '{flag}' should be offered; got: {labels:?}"
2551 );
2552 }
2553 for flag in &["g", "i", "e"] {
2554 assert!(!labels.contains(flag), "tr/// should NOT offer '{flag}'; got: {labels:?}");
2555 }
2556 }
2557
2558 #[test]
2559 fn test_regex_tr_binding_operator_flag_completions() {
2560 let code = "$x =~ tr/a-z/A-Z/";
2562 let mut parser = Parser::new(code);
2563 let ast = must(parser.parse());
2564 let provider = CompletionProvider::new(&ast);
2565 let completions = provider.get_completions(code, code.len());
2566 let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
2567 for flag in &["c", "d", "s"] {
2568 assert!(
2569 labels.contains(flag),
2570 "tr/// binding flag '{flag}' should be offered; got: {labels:?}"
2571 );
2572 }
2573 assert!(!labels.contains(&"g"), "tr/// should NOT offer 'g'; got: {labels:?}");
2574 }
2575
2576 #[test]
2579 fn test_regex_operator_snippets_present() {
2580 let code = "";
2581 let mut parser = Parser::new(code);
2582 let ast = must(parser.parse());
2583 let provider = CompletionProvider::new(&ast);
2584 let completions = provider.get_completions(code, 0);
2585 let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
2586 assert!(labels.contains(&"mregex"), "mregex snippet missing; got: {labels:?}");
2587 assert!(labels.contains(&"ssubst"), "ssubst snippet missing; got: {labels:?}");
2588 assert!(labels.contains(&"qrpat"), "qrpat snippet missing; got: {labels:?}");
2589 }
2590
2591 #[test]
2592 fn test_regex_operator_snippet_bodies() {
2593 let code = "mregex";
2595 let mut parser = Parser::new(code);
2596 let ast = must(parser.parse());
2597 let provider = CompletionProvider::new(&ast);
2598 let completions = provider.get_completions(code, code.len());
2599
2600 let mregex = must_some(completions.iter().find(|c| c.label == "mregex"));
2601 let insert = mregex.insert_text.as_deref().unwrap_or_default();
2602 assert!(insert.starts_with("m/"), "mregex body must start with m/; got: {insert:?}");
2603
2604 let code2 = "ssubst";
2606 let mut parser2 = Parser::new(code2);
2607 let ast2 = must(parser2.parse());
2608 let provider2 = CompletionProvider::new(&ast2);
2609 let completions2 = provider2.get_completions(code2, code2.len());
2610 let ssubst = must_some(completions2.iter().find(|c| c.label == "ssubst"));
2611 let insert2 = ssubst.insert_text.as_deref().unwrap_or_default();
2612 assert!(insert2.starts_with("s/"), "ssubst body must start with s/; got: {insert2:?}");
2613
2614 let code3 = "qrpat";
2615 let mut parser3 = Parser::new(code3);
2616 let ast3 = must(parser3.parse());
2617 let provider3 = CompletionProvider::new(&ast3);
2618 let completions3 = provider3.get_completions(code3, code3.len());
2619 let qrpat = must_some(completions3.iter().find(|c| c.label == "qrpat"));
2620 let insert3 = qrpat.insert_text.as_deref().unwrap_or_default();
2621 assert!(insert3.starts_with("qr/"), "qrpat body must start with qr/; got: {insert3:?}");
2622 }
2623
2624 #[test]
2629 fn test_dash_trigger_fires_method_completion_for_arrow()
2630 -> Result<(), Box<dyn std::error::Error>> {
2631 let code = r#"package MyService;
2637sub new { bless {}, shift }
2638sub process { }
2639sub validate { }
2640sub run {
2641 my $self = shift;
2642 $self-"#;
2643 let mut parser = Parser::new(code);
2644 let ast = must(parser.parse());
2645 let index = Arc::new(WorkspaceIndex::new());
2646 let module_uri = Url::parse("file:///workspace/MyService.pm")?;
2647 let module_code = "package MyService;\nsub process { }\nsub validate { }\n1;\n";
2648 index.index_file(module_uri, module_code.to_string())?;
2649 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2650 let completions = provider.get_completions(code, code.len());
2651 assert!(
2653 completions.iter().any(|c| c.label == "process" || c.label == "validate"),
2654 "dash trigger on `$self-` should produce method completions; got: {:?}",
2655 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2656 );
2657 assert!(
2661 !completions.iter().any(|c| c.label == "arrayref" || c.label == "hashref"),
2662 "dash trigger on `$self-` must not return generic snippets; got: {:?}",
2663 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2664 );
2665 Ok(())
2666 }
2667
2668 #[test]
2669 fn test_dash_trigger_suppressed_for_subtract_assign() {
2670 let code = "$x -";
2672 let mut parser = Parser::new(code);
2673 let ast = must(parser.parse());
2674 let provider = CompletionProvider::new(&ast);
2675 let completions = provider.get_completions(code, code.len());
2677 assert!(
2678 completions.is_empty(),
2679 "dash trigger on `$x -` (subtract context) should return no completions; got: {:?}",
2680 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2681 );
2682 }
2683
2684 #[test]
2685 fn test_dash_trigger_suppressed_for_decrement() {
2686 let code = "$x--";
2689 let mut parser = Parser::new(code);
2690 let ast = must(parser.parse());
2691 let provider = CompletionProvider::new(&ast);
2692 let completions = provider.get_completions(code, code.len());
2694 assert!(
2695 completions.is_empty(),
2696 "dash trigger on `$x--` (decrement context) should return no completions; got: {:?}",
2697 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2698 );
2699 }
2700
2701 #[test]
2702 fn test_dash_trigger_suppressed_for_unary_minus() {
2703 let code = "my $x = -";
2705 let mut parser = Parser::new(code);
2706 let ast = must(parser.parse());
2707 let provider = CompletionProvider::new(&ast);
2708 let completions = provider.get_completions(code, code.len());
2709 assert!(
2710 completions.is_empty(),
2711 "dash trigger on `my $x = -` (unary minus) should return no completions; got: {:?}",
2712 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2713 );
2714 }
2715
2716 #[test]
2717 fn test_dash_trigger_fires_for_hash_deref_arrow() -> Result<(), Box<dyn std::error::Error>> {
2718 let code = r#"package MyService;
2721sub new { bless {}, shift }
2722sub get_data { }
2723sub run {
2724 my $hash = {};
2725 $hash-"#;
2726 let mut parser = Parser::new(code);
2727 let ast = must(parser.parse());
2728 let index = Arc::new(WorkspaceIndex::new());
2729 let module_uri = Url::parse("file:///workspace/MyService.pm")?;
2730 let module_code = "package MyService;\nsub new { }\nsub get_data { }\n1;\n";
2731 index.index_file(module_uri, module_code.to_string())?;
2732 let provider = CompletionProvider::new_with_index(&ast, Some(index));
2733 let completions = provider.get_completions(code, code.len());
2734 assert!(
2735 completions.iter().any(|c| c.label == "get_data" || c.label == "new"),
2736 "dash trigger on `$hash-` should produce completions; got: {:?}",
2737 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2738 );
2739 assert!(
2741 !completions.iter().any(|c| c.label == "arrayref" || c.label == "hashref"),
2742 "dash trigger on `$hash-` must not return generic snippets; got: {:?}",
2743 completions.iter().map(|c| &c.label).collect::<Vec<_>>()
2744 );
2745 Ok(())
2746 }
2747}