1use perl_position_tracking::utf16_line_col_to_offset;
8use serde::{Deserialize, Serialize};
9
10const MAX_INLINE_COMPLETION_ITEMS: usize = 5;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct PreparedInlineCompletionContext {
16 pub prefix: String,
18 pub current_line: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub previous_non_empty_line: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub current_function: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub current_package: Option<String>,
29 pub variables: Vec<String>,
31 pub imports: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct InlineCompletionItem {
39 pub insert_text: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub filter_text: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub range: Option<lsp_types::Range>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub command: Option<lsp_types::Command>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct InlineCompletionList {
56 pub items: Vec<InlineCompletionItem>,
58}
59
60#[derive(Debug)]
64pub enum BackendError {
65 Transport(String),
67 Auth(String),
69 Provider(String),
71 Timeout,
73 RateLimited,
75 Cancelled,
77}
78
79impl std::fmt::Display for BackendError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::Transport(msg) => write!(f, "transport error: {}", msg),
83 Self::Auth(msg) => write!(f, "auth error: {}", msg),
84 Self::Provider(msg) => write!(f, "provider error: {}", msg),
85 Self::Timeout => write!(f, "request timed out"),
86 Self::RateLimited => write!(f, "rate limit exceeded"),
87 Self::Cancelled => write!(f, "request cancelled"),
88 }
89 }
90}
91
92impl std::error::Error for BackendError {}
93
94#[derive(Debug, Clone)]
96pub struct BackendRequest {
97 pub context: PreparedInlineCompletionContext,
99 pub max_output_tokens: u32,
101 pub timeout_ms: u64,
103}
104
105#[derive(Debug, Clone)]
107pub struct StreamChunk {
108 pub text: String,
110 pub is_final: bool,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum StreamControl {
117 Continue,
119 Stop,
121}
122
123pub trait InlineCompletionBackend: Send + Sync {
132 fn complete(&self, req: &BackendRequest) -> Result<Vec<String>, BackendError> {
136 let mut final_text = String::new();
137 self.stream(req, &mut |chunk| {
138 final_text = chunk.text.clone();
139 if chunk.is_final { StreamControl::Stop } else { StreamControl::Continue }
140 })?;
141 Ok(if final_text.is_empty() { vec![] } else { vec![final_text] })
142 }
143
144 fn stream(
149 &self,
150 req: &BackendRequest,
151 sink: &mut dyn FnMut(StreamChunk) -> StreamControl,
152 ) -> Result<(), BackendError>;
153}
154
155#[derive(Debug)]
156struct RankedCompletionItem {
157 priority: u8,
158 order: usize,
159 item: InlineCompletionItem,
160}
161
162pub struct InlineCompletionProvider;
164
165impl Default for InlineCompletionProvider {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl InlineCompletionProvider {
172 pub fn new() -> Self {
174 Self
175 }
176
177 pub fn get_inline_completions(
179 &self,
180 text: &str,
181 line: u32,
182 character: u32,
183 ) -> InlineCompletionList {
184 if let Some(context) = self.prepare_context(text, line, character) {
185 let items = self.get_completions_for_context(&context);
186 return InlineCompletionList { items };
187 }
188
189 InlineCompletionList { items: vec![] }
190 }
191
192 pub fn prepare_context(
195 &self,
196 text: &str,
197 line: u32,
198 character: u32,
199 ) -> Option<PreparedInlineCompletionContext> {
200 let line_context = self.line_context_at_position(text, line, character)?;
201 let lines = self.normalized_lines(text);
202 let line_index = usize::try_from(line).ok()?;
203 let (current_function, function_start_line) =
204 self.current_function_context(&lines, line_index);
205 let visible_text = self.visible_text_until_cursor(&lines, line_index, line_context.prefix);
206 let variable_scan_text = self.visible_text_since_line(
207 &lines,
208 function_start_line.unwrap_or(0),
209 line_index,
210 line_context.prefix,
211 );
212
213 Some(PreparedInlineCompletionContext {
214 prefix: line_context.prefix.to_string(),
215 current_line: line_context.current_line.to_string(),
216 previous_non_empty_line: self
217 .previous_non_empty_line(&lines, line_index)
218 .map(str::to_string),
219 current_function,
220 current_package: self.current_package(&lines, line_index),
221 variables: self.collect_variables(&variable_scan_text),
222 imports: self.collect_imports(&visible_text),
223 })
224 }
225
226 fn line_context_at_position<'a>(
227 &self,
228 text: &'a str,
229 line: u32,
230 character: u32,
231 ) -> Option<LineContext<'a>> {
232 let lines = self.normalized_lines(text);
233 let line_index = usize::try_from(line).ok()?;
234 let current_line = *lines.get(line_index)?;
235 let prefix_end = utf16_line_col_to_offset(current_line, 0, character);
236
237 Some(LineContext { prefix: ¤t_line[..prefix_end], current_line })
238 }
239
240 fn normalized_lines<'a>(&self, text: &'a str) -> Vec<&'a str> {
241 if text.is_empty() {
242 return vec![""];
243 }
244
245 text.split('\n').map(|line| line.strip_suffix('\r').unwrap_or(line)).collect()
246 }
247
248 fn get_completions_for_context(
249 &self,
250 context: &PreparedInlineCompletionContext,
251 ) -> Vec<InlineCompletionItem> {
252 let prefix = context.prefix.as_str();
253 let full_line = context.current_line.as_str();
254 let mut items = Vec::<RankedCompletionItem>::new();
255 let mut sequence = 0usize;
256
257 let mut push_item = |priority: u8, item: InlineCompletionItem| {
258 items.push(RankedCompletionItem { priority, order: sequence, item });
259 sequence += 1;
260 };
261
262 if prefix.ends_with("->") {
264 push_item(
265 0,
266 InlineCompletionItem {
267 insert_text: "new()".into(),
268 filter_text: Some("new".into()),
269 range: None,
270 command: None,
271 },
272 );
273 }
274
275 if prefix.trim_end() == "use" || prefix.ends_with("use ") {
277 push_item(
279 0,
280 InlineCompletionItem {
281 insert_text: "strict;".into(),
282 filter_text: Some("strict".into()),
283 range: None,
284 command: None,
285 },
286 );
287
288 push_item(
289 1,
290 InlineCompletionItem {
291 insert_text: "warnings;".into(),
292 filter_text: Some("warnings".into()),
293 range: None,
294 command: None,
295 },
296 );
297
298 push_item(
299 2,
300 InlineCompletionItem {
301 insert_text: "feature ':5.36';".into(),
302 filter_text: Some("feature".into()),
303 range: None,
304 command: None,
305 },
306 );
307 }
308
309 if let Some(sub_name) = self.match_sub_declaration(prefix) {
311 if !full_line.contains('{') {
312 let body = self.generate_smart_body(&sub_name);
313 push_item(
314 0,
315 InlineCompletionItem {
316 insert_text: format!(" {{\n{}\n}}", body),
317 filter_text: Some("{".into()),
318 range: None,
319 command: None,
320 },
321 );
322 }
323 }
324
325 if prefix.ends_with("my $") {
327 push_item(
328 0,
329 InlineCompletionItem {
330 insert_text: "self = shift;".into(),
331 filter_text: Some("self".into()),
332 range: None,
333 command: None,
334 },
335 );
336 }
337
338 if prefix.ends_with("package ") {
340 push_item(
341 0,
342 InlineCompletionItem {
343 insert_text: "MyPackage;\n\nuse strict;\nuse warnings;".into(),
344 filter_text: Some("MyPackage".into()),
345 range: None,
346 command: None,
347 },
348 );
349 }
350
351 if prefix.ends_with("bless ") {
353 push_item(
354 0,
355 InlineCompletionItem {
356 insert_text: "$self, $class;".into(),
357 filter_text: Some("$self".into()),
358 range: None,
359 command: None,
360 },
361 );
362 }
363
364 if prefix.ends_with("return ") {
366 if let Some(variable) = self.preferred_return_variable(context) {
367 push_item(
368 0,
369 InlineCompletionItem {
370 insert_text: format!("{variable};"),
371 filter_text: Some(variable),
372 range: None,
373 command: None,
374 },
375 );
376 } else if self.is_in_constructor_context(context.current_function.as_deref(), prefix) {
377 push_item(
378 1,
379 InlineCompletionItem {
380 insert_text: "$self;".into(),
381 filter_text: Some("$self".into()),
382 range: None,
383 command: None,
384 },
385 );
386 }
387 }
388
389 if prefix.ends_with("for ") {
391 push_item(
392 0,
393 InlineCompletionItem {
394 insert_text: "my $item (@items) {\n \n}".into(),
395 filter_text: Some("my".into()),
396 range: None,
397 command: None,
398 },
399 );
400 }
401
402 if prefix.ends_with("foreach ") {
403 push_item(
404 0,
405 InlineCompletionItem {
406 insert_text: "my $item (@items) {\n \n}".into(),
407 filter_text: Some("my".into()),
408 range: None,
409 command: None,
410 },
411 );
412 }
413
414 if prefix.ends_with("ok(") {
416 push_item(
417 0,
418 InlineCompletionItem {
419 insert_text: "$result, 'test description');".into(),
420 filter_text: Some("$result".into()),
421 range: None,
422 command: None,
423 },
424 );
425 }
426
427 if prefix.ends_with("is(") {
428 push_item(
429 0,
430 InlineCompletionItem {
431 insert_text: "$got, $expected, 'test description');".into(),
432 filter_text: Some("$got".into()),
433 range: None,
434 command: None,
435 },
436 );
437 }
438
439 if prefix == "#!" || prefix == "#!/" {
441 push_item(
442 0,
443 InlineCompletionItem {
444 insert_text: "/usr/bin/env perl".into(),
445 filter_text: Some("perl".into()),
446 range: None,
447 command: None,
448 },
449 );
450 }
451
452 self.add_contextual_fallbacks(context, &mut items, &mut sequence);
453 self.normalize_items(items)
454 }
455
456 fn match_sub_declaration(&self, prefix: &str) -> Option<String> {
458 if let Some(idx) = prefix.rfind("sub ") {
460 let after_sub = &prefix[idx + 4..];
461 if !after_sub.is_empty() && !after_sub.contains('{') && !after_sub.contains('(') {
463 let name = after_sub.trim();
465 if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
466 return Some(name.to_string());
467 }
468 }
469 }
470 None
471 }
472
473 fn is_in_constructor_context(&self, current_function: Option<&str>, prefix: &str) -> bool {
475 matches!(current_function, Some("new" | "BUILD"))
476 || prefix.contains("sub new")
477 || prefix.contains("sub BUILD")
478 }
479
480 fn generate_smart_body(&self, sub_name: &str) -> String {
491 if sub_name == "new" || sub_name == "BUILD" {
493 return " my $class = shift;\n my $self = bless {}, $class;\n return $self;"
494 .to_string();
495 }
496
497 if let Some(field) = sub_name.strip_prefix("get_") {
499 return format!(" my $self = shift;\n return $self->{{{}}};", field);
501 }
502
503 if let Some(field) = sub_name.strip_prefix("set_") {
505 return format!(
507 " my ($self, $value) = @_;\n $self->{{{}}} = $value;\n return $self;",
508 field
509 );
510 }
511
512 if sub_name.starts_with("is_")
514 || sub_name.starts_with("has_")
515 || sub_name.starts_with("can_")
516 {
517 let prefix_len = if sub_name.starts_with("is_") { 3 } else { 4 };
518 let field = &sub_name[prefix_len..];
519 return format!(" my $self = shift;\n return $self->{{{}}} ? 1 : 0;", field);
520 }
521
522 if sub_name.starts_with('_') {
524 return " my $self = shift;\n ...".to_string();
525 }
526
527 " my $self = shift;\n ...".to_string()
529 }
530
531 fn current_function_context(
532 &self,
533 lines: &[&str],
534 line_index: usize,
535 ) -> (Option<String>, Option<usize>) {
536 lines.iter().take(line_index + 1).enumerate().fold(
537 (None, None),
538 |mut state, (idx, line)| {
539 if let Some(name) = self.parse_sub_name(line) {
540 state = (Some(name), Some(idx));
541 }
542 state
543 },
544 )
545 }
546
547 fn current_package(&self, lines: &[&str], line_index: usize) -> Option<String> {
548 lines
549 .iter()
550 .take(line_index + 1)
551 .filter_map(|line| self.parse_package_name(line))
552 .next_back()
553 }
554
555 fn previous_non_empty_line<'a>(
556 &self,
557 lines: &'a [&'a str],
558 line_index: usize,
559 ) -> Option<&'a str> {
560 lines
561 .get(..line_index)
562 .and_then(|slice| slice.iter().rev().find(|line| !line.trim().is_empty()).copied())
563 }
564
565 fn visible_text_until_cursor(&self, lines: &[&str], line_index: usize, prefix: &str) -> String {
566 self.visible_text_since_line(lines, 0, line_index, prefix)
567 }
568
569 fn visible_text_since_line(
570 &self,
571 lines: &[&str],
572 start_line: usize,
573 line_index: usize,
574 prefix: &str,
575 ) -> String {
576 let mut visible_text = String::new();
577
578 for (idx, line) in
579 lines.iter().enumerate().skip(start_line).take(line_index.saturating_sub(start_line))
580 {
581 if idx > start_line {
582 visible_text.push('\n');
583 }
584 visible_text.push_str(line);
585 }
586
587 if line_index > start_line || !visible_text.is_empty() {
588 visible_text.push('\n');
589 }
590 visible_text.push_str(prefix);
591 visible_text
592 }
593
594 fn collect_imports(&self, visible_text: &str) -> Vec<String> {
595 let mut imports = Vec::new();
596
597 for line in visible_text.lines() {
598 if let Some(import_name) = self.parse_use_name(line) {
599 self.push_unique(&mut imports, import_name);
600 }
601 }
602
603 imports
604 }
605
606 fn collect_variables(&self, visible_text: &str) -> Vec<String> {
607 let mut matches = Vec::new();
608 let bytes = visible_text.as_bytes();
609 let mut index = 0usize;
610
611 while index < bytes.len() {
612 let byte = bytes[index];
613 if byte == b'$' || byte == b'@' || byte == b'%' {
614 let start = index;
615 index += 1;
616
617 if index >= bytes.len() {
618 break;
619 }
620
621 let first = bytes[index] as char;
622 if !(first.is_ascii_alphabetic() || first == '_') {
623 continue;
624 }
625
626 index += 1;
627 while index < bytes.len() {
628 let next = bytes[index] as char;
629 if next.is_ascii_alphanumeric() || next == '_' {
630 index += 1;
631 } else {
632 break;
633 }
634 }
635
636 matches.push(visible_text[start..index].to_string());
637 continue;
638 }
639
640 index += 1;
641 }
642
643 let mut variables = Vec::new();
644 for variable in matches.into_iter().rev() {
645 self.push_unique(&mut variables, variable);
646 if variables.len() >= 8 {
647 break;
648 }
649 }
650
651 variables
652 }
653
654 fn parse_use_name(&self, line: &str) -> Option<String> {
655 let trimmed = line.trim_start();
656 let rest = trimmed.strip_prefix("use ")?;
657 let name: String = rest
658 .chars()
659 .take_while(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_'))
660 .collect();
661
662 (!name.is_empty()).then_some(name)
663 }
664
665 fn parse_sub_name(&self, line: &str) -> Option<String> {
666 let trimmed = line.trim_start();
667 let rest = trimmed.strip_prefix("sub ")?;
668 let name: String = rest
669 .chars()
670 .skip_while(|ch| ch.is_whitespace())
671 .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
672 .collect();
673
674 (!name.is_empty()).then_some(name)
675 }
676
677 fn parse_package_name(&self, line: &str) -> Option<String> {
678 let trimmed = line.trim_start();
679 let rest = trimmed.strip_prefix("package ")?;
680 let name: String = rest
681 .chars()
682 .take_while(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ':' | '_'))
683 .collect();
684
685 (!name.is_empty()).then_some(name)
686 }
687
688 fn add_contextual_fallbacks(
689 &self,
690 context: &PreparedInlineCompletionContext,
691 items: &mut Vec<RankedCompletionItem>,
692 sequence: &mut usize,
693 ) {
694 let prefix = context.prefix.trim();
695 let comment_context = context
696 .previous_non_empty_line
697 .as_deref()
698 .map(|line| line.trim_start().starts_with('#'))
699 .unwrap_or(false);
700
701 if context.current_line.is_empty()
702 && context.current_function.is_none()
703 && context.imports.is_empty()
704 && context.variables.is_empty()
705 {
706 items.push(RankedCompletionItem {
707 priority: 8,
708 order: *sequence,
709 item: InlineCompletionItem {
710 insert_text: "#!/usr/bin/env perl\nuse strict;\nuse warnings;\n\n".into(),
711 filter_text: Some("perl".into()),
712 range: None,
713 command: None,
714 },
715 });
716 *sequence += 1;
717 items.push(RankedCompletionItem {
718 priority: 9,
719 order: *sequence,
720 item: InlineCompletionItem {
721 insert_text: "use strict;\nuse warnings;\n\n".into(),
722 filter_text: Some("strict".into()),
723 range: None,
724 command: None,
725 },
726 });
727 *sequence += 1;
728 }
729
730 if prefix.is_empty() {
731 if let Some(variable) = self.preferred_return_variable(context) {
732 items.push(RankedCompletionItem {
733 priority: 0,
734 order: *sequence,
735 item: InlineCompletionItem {
736 insert_text: format!("return {variable};"),
737 filter_text: Some(variable),
738 range: None,
739 command: None,
740 },
741 });
742 *sequence += 1;
743 }
744
745 if self.imports_include(context, "Test::More")
746 || self.imports_include(context, "Test2::V0")
747 {
748 items.push(RankedCompletionItem {
749 priority: 1,
750 order: *sequence,
751 item: InlineCompletionItem {
752 insert_text: "done_testing();".into(),
753 filter_text: Some("done_testing".into()),
754 range: None,
755 command: None,
756 },
757 });
758 *sequence += 1;
759 }
760
761 if comment_context && let Some(variable) = self.preferred_assignment_variable(context) {
762 items.push(RankedCompletionItem {
763 priority: 2,
764 order: *sequence,
765 item: InlineCompletionItem {
766 insert_text: format!("my {variable} = shift;"),
767 filter_text: Some(variable),
768 range: None,
769 command: None,
770 },
771 });
772 *sequence += 1;
773 }
774 }
775 }
776
777 fn normalize_items(&self, mut items: Vec<RankedCompletionItem>) -> Vec<InlineCompletionItem> {
778 items.sort_by(|left, right| {
779 left.priority.cmp(&right.priority).then_with(|| left.order.cmp(&right.order))
780 });
781
782 let mut deduped = Vec::new();
783 let mut seen = Vec::<String>::new();
784 for candidate in items.into_iter() {
785 if seen.iter().any(|existing| existing == &candidate.item.insert_text) {
786 continue;
787 }
788
789 seen.push(candidate.item.insert_text.clone());
790 deduped.push(candidate.item);
791 if deduped.len() >= MAX_INLINE_COMPLETION_ITEMS {
792 break;
793 }
794 }
795
796 deduped
797 }
798
799 fn preferred_return_variable(
800 &self,
801 context: &PreparedInlineCompletionContext,
802 ) -> Option<String> {
803 context
804 .variables
805 .iter()
806 .find(|variable| variable.as_str() == "$self")
807 .cloned()
808 .or_else(|| context.variables.first().cloned())
809 }
810
811 fn preferred_assignment_variable(
812 &self,
813 context: &PreparedInlineCompletionContext,
814 ) -> Option<String> {
815 context
816 .variables
817 .iter()
818 .find(|variable| variable.starts_with('$') && variable.as_str() != "$self")
819 .cloned()
820 }
821
822 fn imports_include(&self, context: &PreparedInlineCompletionContext, expected: &str) -> bool {
823 context.imports.iter().any(|import_name| import_name == expected)
824 }
825
826 fn push_unique(&self, values: &mut Vec<String>, value: String) {
827 if values.iter().any(|existing| existing == &value) {
828 return;
829 }
830 values.push(value);
831 }
832}
833
834struct LineContext<'a> {
835 prefix: &'a str,
836 current_line: &'a str,
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[test]
844 fn test_after_arrow() {
845 let provider = InlineCompletionProvider::new();
846 let completions = provider.get_inline_completions("$obj->", 0, 6);
847 assert!(!completions.items.is_empty());
848 assert_eq!(completions.items[0].insert_text, "new()");
849 }
850
851 #[test]
852 fn test_after_use() {
853 let provider = InlineCompletionProvider::new();
854 let completions = provider.get_inline_completions("use ", 0, 4);
855 assert!(!completions.items.is_empty());
856 assert!(completions.items.iter().any(|i| i.insert_text == "strict;"));
857 }
858
859 #[test]
860 fn test_after_sub() {
861 let provider = InlineCompletionProvider::new();
862 let completions = provider.get_inline_completions("sub hello", 0, 9);
863 assert!(!completions.items.is_empty());
864 assert!(completions.items[0].insert_text.contains("my $self = shift"));
866 }
867
868 #[test]
869 fn test_sub_new_constructor() {
870 let provider = InlineCompletionProvider::new();
871 let completions = provider.get_inline_completions("sub new", 0, 7);
872 assert!(!completions.items.is_empty());
873 assert!(completions.items[0].insert_text.contains("bless"));
875 assert!(completions.items[0].insert_text.contains("my $class = shift"));
876 }
877
878 #[test]
879 fn test_sub_getter() {
880 let provider = InlineCompletionProvider::new();
881 let completions = provider.get_inline_completions("sub get_name", 0, 12);
882 assert!(!completions.items.is_empty());
883 assert!(completions.items[0].insert_text.contains("return $self->{name}"));
885 }
886
887 #[test]
888 fn test_sub_setter() {
889 let provider = InlineCompletionProvider::new();
890 let completions = provider.get_inline_completions("sub set_name", 0, 12);
891 assert!(!completions.items.is_empty());
892 assert!(completions.items[0].insert_text.contains("$self->{name} = $value"));
894 }
895
896 #[test]
897 fn test_sub_is_predicate() {
898 let provider = InlineCompletionProvider::new();
899 let completions = provider.get_inline_completions("sub is_active", 0, 13);
900 assert!(!completions.items.is_empty());
901 assert!(completions.items[0].insert_text.contains("? 1 : 0"));
903 }
904
905 #[test]
906 fn test_sub_has_predicate() {
907 let provider = InlineCompletionProvider::new();
908 let completions = provider.get_inline_completions("sub has_items", 0, 13);
909 assert!(!completions.items.is_empty());
910 assert!(completions.items[0].insert_text.contains("? 1 : 0"));
912 }
913
914 #[test]
915 fn test_no_completion_when_brace_exists() {
916 let provider = InlineCompletionProvider::new();
917 let completions = provider.get_inline_completions("sub hello {", 0, 9);
918 assert!(completions.items.is_empty() || !completions.items[0].insert_text.contains('{'));
920 }
921
922 #[test]
923 fn test_shebang_completion() {
924 let provider = InlineCompletionProvider::new();
925 let completions = provider.get_inline_completions("#!/", 0, 3);
926 assert!(!completions.items.is_empty());
927 assert_eq!(completions.items[0].insert_text, "/usr/bin/env perl");
928 }
929
930 #[test]
931 fn test_after_arrow_with_unicode_prefix_uses_utf16_position() {
932 let provider = InlineCompletionProvider::new();
933 let source = "my $emoji = \"😀\"; my $obj = Package->";
934 let character = source.encode_utf16().count() as u32;
935 let completions = provider.get_inline_completions(source, 0, character);
936
937 assert!(!completions.items.is_empty());
938 assert_eq!(completions.items[0].insert_text, "new()");
939 }
940
941 #[test]
942 fn test_prepare_context_collects_function_variables_and_imports()
943 -> Result<(), Box<dyn std::error::Error>> {
944 let provider = InlineCompletionProvider::new();
945 let source = "use Test::More;\npackage Demo;\n\nsub helper {\n my $result = 1;\n my $status = $result;\n \n}\n";
946 let line = 6;
947 let character = 4;
948 let context =
949 provider.prepare_context(source, line, character).ok_or("expected prepared context")?;
950
951 assert_eq!(context.current_function.as_deref(), Some("helper"));
952 assert_eq!(context.current_package.as_deref(), Some("Demo"));
953 assert_eq!(context.previous_non_empty_line.as_deref(), Some(" my $status = $result;"));
954 assert!(context.imports.iter().any(|import_name| import_name == "Test::More"));
955 assert!(context.variables.iter().any(|variable| variable == "$status"));
956 assert!(context.variables.iter().any(|variable| variable == "$result"));
957 Ok(())
958 }
959
960 #[test]
961 fn test_empty_file_gets_scaffold_suggestions() {
962 let provider = InlineCompletionProvider::new();
963 let completions = provider.get_inline_completions("", 0, 0);
964
965 assert!(!completions.items.is_empty());
966 assert!(completions.items.iter().any(|item| item.insert_text.contains("use strict;")));
967 }
968
969 #[test]
970 fn test_blank_line_in_function_prefers_nearby_variable() {
971 let provider = InlineCompletionProvider::new();
972 let source = "sub helper {\n my $result = compute();\n \n}\n";
973 let completions = provider.get_inline_completions(source, 2, 4);
974
975 assert!(!completions.items.is_empty());
976 assert!(completions.items.iter().any(|item| item.insert_text == "return $result;"));
977 }
978
979 #[test]
980 fn test_blank_line_after_comment_still_has_contextual_suggestions() {
981 let provider = InlineCompletionProvider::new();
982 let source = "use Test::More;\n\nsub helper {\n my $result = 1;\n # explain next step\n \n}\n";
983 let completions = provider.get_inline_completions(source, 5, 4);
984
985 assert!(!completions.items.is_empty());
986 assert!(completions.items.iter().any(|item| item.insert_text == "return $result;"));
987 assert!(completions.items.iter().any(|item| item.insert_text == "done_testing();"));
988 }
989
990 #[test]
991 fn test_normalize_items_orders_deduplicates_and_limits() {
992 let provider = InlineCompletionProvider::new();
993 let items = vec![
994 RankedCompletionItem {
995 priority: 2,
996 order: 0,
997 item: InlineCompletionItem {
998 insert_text: "late".into(),
999 filter_text: None,
1000 range: None,
1001 command: None,
1002 },
1003 },
1004 RankedCompletionItem {
1005 priority: 0,
1006 order: 1,
1007 item: InlineCompletionItem {
1008 insert_text: "first".into(),
1009 filter_text: None,
1010 range: None,
1011 command: None,
1012 },
1013 },
1014 RankedCompletionItem {
1015 priority: 0,
1016 order: 2,
1017 item: InlineCompletionItem {
1018 insert_text: "first".into(),
1019 filter_text: Some("duplicate".into()),
1020 range: None,
1021 command: None,
1022 },
1023 },
1024 RankedCompletionItem {
1025 priority: 1,
1026 order: 3,
1027 item: InlineCompletionItem {
1028 insert_text: "second".into(),
1029 filter_text: None,
1030 range: None,
1031 command: None,
1032 },
1033 },
1034 RankedCompletionItem {
1035 priority: 3,
1036 order: 4,
1037 item: InlineCompletionItem {
1038 insert_text: "third".into(),
1039 filter_text: None,
1040 range: None,
1041 command: None,
1042 },
1043 },
1044 RankedCompletionItem {
1045 priority: 4,
1046 order: 5,
1047 item: InlineCompletionItem {
1048 insert_text: "fourth".into(),
1049 filter_text: None,
1050 range: None,
1051 command: None,
1052 },
1053 },
1054 RankedCompletionItem {
1055 priority: 5,
1056 order: 6,
1057 item: InlineCompletionItem {
1058 insert_text: "fifth".into(),
1059 filter_text: None,
1060 range: None,
1061 command: None,
1062 },
1063 },
1064 ];
1065
1066 let normalized = provider.normalize_items(items);
1067
1068 assert_eq!(normalized.len(), MAX_INLINE_COMPLETION_ITEMS);
1069 assert_eq!(normalized[0].insert_text, "first");
1070 assert_eq!(normalized[1].insert_text, "second");
1071 assert_eq!(normalized[2].insert_text, "late");
1072 assert_eq!(normalized[3].insert_text, "third");
1073 assert_eq!(normalized[4].insert_text, "fourth");
1074 }
1075}