1use crate::util::position_to_offset;
6use shape_ast::ast::InterpolationMode;
7use tower_lsp_server::ls_types::Position;
8
9#[derive(Debug, Clone, PartialEq)]
10enum FormattedCursorContext {
11 OutsideFormattedString,
12 InFormattedLiteral,
13 InInterpolationExpr { expr_prefix: String },
14}
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum CompletionContext {
19 General,
21 PropertyAccess {
23 object: String,
25 },
26 FunctionCall {
28 function: String,
30 arg_context: ArgumentContext,
32 },
33 PatternBody,
35 Query {
37 query_type: String,
39 },
40 PatternReference,
42 TypeAnnotation,
44 Annotation,
46 AnnotationArgs {
48 annotation: String,
50 },
51 ImportModule,
53 FromModule,
55 FromModulePartial {
57 prefix: String,
59 },
60 ImportItems {
62 module: String,
64 },
65 PipeTarget {
67 pipe_input_type: Option<String>,
69 },
70 ImplBlock {
72 trait_name: String,
74 target_type: String,
76 existing_methods: Vec<String>,
78 },
79 TypeAliasOverride {
82 base_type: String,
84 },
85 JoinStrategy,
87 JoinBody {
89 strategy: String,
91 },
92 TraitBound,
94 ComptimeBlock,
96 ExprAnnotation,
98 DocTag {
100 prefix: String,
102 },
103 DocParamName {
105 prefix: String,
107 },
108 DocTypeParamName {
110 prefix: String,
112 },
113 DocLinkTarget {
115 prefix: String,
117 },
118 InterpolationFormatSpec {
120 spec_prefix: String,
122 },
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub enum ArgumentContext {
128 FunctionArgument { function: String, arg_index: usize },
130 ObjectLiteralValue {
132 containing_function: Option<String>,
133 property_name: String,
134 },
135 ObjectLiteralPropertyName { containing_function: Option<String> },
137 General,
139}
140
141pub fn analyze_context(text: &str, position: Position) -> CompletionContext {
143 let lines: Vec<&str> = text.lines().collect();
145 if position.line as usize >= lines.len() {
146 return CompletionContext::General;
147 }
148
149 let current_line = lines[position.line as usize];
150 let char_pos = position.character as usize;
151
152 let line_text_before_cursor = if char_pos <= current_line.len() {
154 ¤t_line[..char_pos]
155 } else {
156 current_line
157 };
158
159 if let Some(doc_context) = detect_doc_comment_context(current_line, line_text_before_cursor) {
160 return doc_context;
161 }
162
163 let cursor_offset = match position_to_offset(text, position) {
164 Some(offset) => offset,
165 None => return CompletionContext::General,
166 };
167
168 let (text_before_cursor, inside_interpolation) =
169 match formatted_cursor_context(text, cursor_offset) {
170 FormattedCursorContext::InFormattedLiteral => {
171 return CompletionContext::General;
173 }
174 FormattedCursorContext::InInterpolationExpr { expr_prefix } => (expr_prefix, true),
175 FormattedCursorContext::OutsideFormattedString => {
176 (line_text_before_cursor.to_string(), false)
177 }
178 };
179
180 if inside_interpolation {
181 if let Some(spec_prefix) = interpolation_format_spec_prefix(&text_before_cursor) {
182 return CompletionContext::InterpolationFormatSpec { spec_prefix };
183 }
184 }
185
186 if !inside_interpolation {
187 if let Some(import_ctx) = detect_import_context(&text_before_cursor) {
189 return import_ctx;
190 }
191 }
192
193 if !inside_interpolation {
195 if let Some(pipe_ctx) = detect_pipe_context(&text_before_cursor) {
196 return pipe_ctx;
197 }
198 }
199
200 if !inside_interpolation {
202 let trimmed = text_before_cursor.trim_end();
203 if trimmed.ends_with("join") || trimmed.ends_with("await join") {
204 return CompletionContext::JoinStrategy;
205 }
206 }
207
208 if !inside_interpolation {
210 if let Some(join_body_ctx) =
211 detect_join_body_context(text, position.line as usize, char_pos)
212 {
213 return join_body_ctx;
214 }
215 }
216
217 if let Some(dot_pos) = text_before_cursor.rfind('.') {
221 let after_dot = &text_before_cursor[dot_pos + 1..];
222 if !after_dot.contains('(') {
223 let before_dot = &text_before_cursor[..dot_pos];
224 let object = extract_object_before_dot(before_dot);
225
226 return CompletionContext::PropertyAccess {
227 object: object.to_string(),
228 };
229 }
230 }
231
232 if !inside_interpolation && text_before_cursor.trim_end().ends_with("find") {
234 return CompletionContext::PatternReference;
235 }
236
237 if !inside_interpolation {
239 for query_type in &["find", "scan", "analyze", "backtest", "alert"] {
240 if text_before_cursor.contains(query_type) {
241 return CompletionContext::Query {
242 query_type: query_type.to_string(),
243 };
244 }
245 }
246 }
247
248 if !inside_interpolation {
250 if let Some(base_type) =
251 detect_type_alias_override_context(text, position.line as usize, char_pos)
252 {
253 return CompletionContext::TypeAliasOverride { base_type };
254 }
255 }
256
257 if !inside_interpolation {
259 if let Some(impl_ctx) = detect_impl_block_context(text, position.line as usize) {
260 return impl_ctx;
261 }
262 }
263
264 if !inside_interpolation && is_inside_comptime_block(text, position.line as usize) {
266 return CompletionContext::ComptimeBlock;
267 }
268
269 if !inside_interpolation && is_inside_pattern_body(text, position.line as usize) {
272 return CompletionContext::PatternBody;
273 }
274
275 if !inside_interpolation && is_in_trait_bound_position(&text_before_cursor) {
277 return CompletionContext::TraitBound;
278 }
279
280 if !inside_interpolation && is_in_type_annotation_position(&text_before_cursor) {
282 return CompletionContext::TypeAnnotation;
283 }
284
285 if let Some(func_name) = extract_function_call(&text_before_cursor) {
287 let arg_context = analyze_argument_context(&text_before_cursor, &func_name);
288 return CompletionContext::FunctionCall {
289 function: func_name,
290 arg_context,
291 };
292 }
293
294 if !inside_interpolation && is_at_expr_annotation_position(&text_before_cursor) {
296 return CompletionContext::ExprAnnotation;
297 }
298
299 if !inside_interpolation && is_at_annotation_position(&text_before_cursor) {
301 return CompletionContext::Annotation;
302 }
303
304 CompletionContext::General
306}
307
308fn detect_doc_comment_context(
309 current_line: &str,
310 line_text_before_cursor: &str,
311) -> Option<CompletionContext> {
312 let trimmed_line = current_line.trim_start();
313 let trimmed_before = line_text_before_cursor.trim_start();
314 if !trimmed_line.starts_with("///") || !trimmed_before.starts_with("///") {
315 return None;
316 }
317
318 let content_before_cursor = trimmed_before
319 .strip_prefix("///")
320 .unwrap_or("")
321 .strip_prefix(' ')
322 .unwrap_or_else(|| trimmed_before.strip_prefix("///").unwrap_or(""));
323 let doc_text = content_before_cursor.trim_start();
324 let rest = doc_text.strip_prefix('@')?;
325 let tag_name_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
326 let tag_name = &rest[..tag_name_end];
327
328 if tag_name_end == rest.len() {
329 return Some(CompletionContext::DocTag {
330 prefix: tag_name.to_string(),
331 });
332 }
333
334 let remainder = rest[tag_name_end..].trim_start();
335 let in_first_value_token = remainder.is_empty() || !remainder.contains(char::is_whitespace);
336 let value_prefix = if in_first_value_token {
337 remainder.to_string()
338 } else {
339 String::new()
340 };
341
342 match tag_name {
343 "param" if in_first_value_token => Some(CompletionContext::DocParamName {
344 prefix: value_prefix,
345 }),
346 "typeparam" if in_first_value_token => Some(CompletionContext::DocTypeParamName {
347 prefix: value_prefix,
348 }),
349 "see" | "link" if in_first_value_token => Some(CompletionContext::DocLinkTarget {
350 prefix: value_prefix,
351 }),
352 _ => None,
353 }
354}
355
356pub fn is_inside_interpolation_expression(text: &str, position: Position) -> bool {
359 let Some(cursor_offset) = position_to_offset(text, position) else {
360 return false;
361 };
362 matches!(
363 formatted_cursor_context(text, cursor_offset),
364 FormattedCursorContext::InInterpolationExpr { .. }
365 )
366}
367
368fn formatted_cursor_context(text: &str, cursor_offset: usize) -> FormattedCursorContext {
369 #[derive(Debug, Clone, Copy)]
370 enum State {
371 Normal,
372 String {
373 escaped: bool,
374 },
375 TripleString,
376 FormattedString {
377 mode: InterpolationMode,
378 escaped: bool,
379 interpolation_depth: usize,
380 interpolation_start: Option<usize>,
381 expr_quote: Option<char>,
382 expr_escaped: bool,
383 },
384 FormattedTripleString {
385 mode: InterpolationMode,
386 interpolation_depth: usize,
387 interpolation_start: Option<usize>,
388 expr_quote: Option<char>,
389 expr_escaped: bool,
390 },
391 }
392
393 fn formatted_prefix(rem: &str) -> Option<(InterpolationMode, bool, usize)> {
394 if rem.starts_with("f$\"\"\"") {
395 Some((InterpolationMode::Dollar, true, 5))
396 } else if rem.starts_with("f#\"\"\"") {
397 Some((InterpolationMode::Hash, true, 5))
398 } else if rem.starts_with("f\"\"\"") {
399 Some((InterpolationMode::Braces, true, 4))
400 } else if rem.starts_with("f$\"") {
401 Some((InterpolationMode::Dollar, false, 3))
402 } else if rem.starts_with("f#\"") {
403 Some((InterpolationMode::Hash, false, 3))
404 } else if rem.starts_with("f\"") {
405 Some((InterpolationMode::Braces, false, 2))
406 } else {
407 None
408 }
409 }
410
411 let mut state = State::Normal;
412 let mut i = 0usize;
413 let capped_offset = cursor_offset.min(text.len());
414
415 while i < capped_offset {
416 let rem = &text[i..];
417 state = match state {
418 State::Normal => {
419 if let Some((mode, true, prefix_len)) = formatted_prefix(rem) {
420 i += prefix_len;
421 State::FormattedTripleString {
422 mode,
423 interpolation_depth: 0,
424 interpolation_start: None,
425 expr_quote: None,
426 expr_escaped: false,
427 }
428 } else if rem.starts_with("\"\"\"") {
429 i += 3;
430 State::TripleString
431 } else if let Some((mode, false, prefix_len)) = formatted_prefix(rem) {
432 i += prefix_len;
433 State::FormattedString {
434 mode,
435 escaped: false,
436 interpolation_depth: 0,
437 interpolation_start: None,
438 expr_quote: None,
439 expr_escaped: false,
440 }
441 } else if rem.starts_with('"') {
442 i += 1;
443 State::String { escaped: false }
444 } else if let Some(ch) = rem.chars().next() {
445 i += ch.len_utf8();
446 State::Normal
447 } else {
448 break;
449 }
450 }
451 State::String { mut escaped } => {
452 if let Some(ch) = rem.chars().next() {
453 if escaped {
454 escaped = false;
455 i += ch.len_utf8();
456 State::String { escaped }
457 } else if ch == '\\' {
458 i += 1;
459 State::String { escaped: true }
460 } else if ch == '"' {
461 i += 1;
462 State::Normal
463 } else {
464 i += ch.len_utf8();
465 State::String { escaped }
466 }
467 } else {
468 break;
469 }
470 }
471 State::TripleString => {
472 if rem.starts_with("\"\"\"") {
473 i += 3;
474 State::Normal
475 } else if let Some(ch) = rem.chars().next() {
476 i += ch.len_utf8();
477 State::TripleString
478 } else {
479 break;
480 }
481 }
482 State::FormattedString {
483 mode,
484 mut escaped,
485 mut interpolation_depth,
486 mut interpolation_start,
487 mut expr_quote,
488 mut expr_escaped,
489 } => {
490 if interpolation_depth == 0 {
491 if mode == InterpolationMode::Braces
492 && (rem.starts_with("{{") || rem.starts_with("}}"))
493 {
494 i += 2;
495 State::FormattedString {
496 mode,
497 escaped,
498 interpolation_depth,
499 interpolation_start,
500 expr_quote,
501 expr_escaped,
502 }
503 } else if mode != InterpolationMode::Braces {
504 let sigil = mode.sigil().expect("sigil mode must provide sigil");
505 let mut esc = String::new();
506 esc.push(sigil);
507 esc.push(sigil);
508 esc.push('{');
509 let mut opener = String::new();
510 opener.push(sigil);
511 opener.push('{');
512
513 if rem.starts_with(&esc) {
514 i += esc.len();
515 State::FormattedString {
516 mode,
517 escaped,
518 interpolation_depth,
519 interpolation_start,
520 expr_quote,
521 expr_escaped,
522 }
523 } else if rem.starts_with(&opener) {
524 interpolation_depth = 1;
525 interpolation_start = Some(i + opener.len());
526 i += opener.len();
527 State::FormattedString {
528 mode,
529 escaped,
530 interpolation_depth,
531 interpolation_start,
532 expr_quote,
533 expr_escaped,
534 }
535 } else if let Some(ch) = rem.chars().next() {
536 if escaped {
537 escaped = false;
538 i += ch.len_utf8();
539 } else if ch == '\\' {
540 escaped = true;
541 i += 1;
542 } else if ch == '"' {
543 return FormattedCursorContext::OutsideFormattedString;
544 } else {
545 i += ch.len_utf8();
546 }
547 State::FormattedString {
548 mode,
549 escaped,
550 interpolation_depth,
551 interpolation_start,
552 expr_quote,
553 expr_escaped,
554 }
555 } else {
556 break;
557 }
558 } else if let Some(ch) = rem.chars().next() {
559 if escaped {
560 escaped = false;
561 i += ch.len_utf8();
562 } else if ch == '\\' {
563 escaped = true;
564 i += 1;
565 } else if ch == '"' {
566 return FormattedCursorContext::OutsideFormattedString;
567 } else if ch == '{' {
568 interpolation_depth = 1;
569 interpolation_start = Some(i + 1);
570 i += 1;
571 } else {
572 i += ch.len_utf8();
573 }
574 State::FormattedString {
575 mode,
576 escaped,
577 interpolation_depth,
578 interpolation_start,
579 expr_quote,
580 expr_escaped,
581 }
582 } else {
583 break;
584 }
585 } else if let Some(ch) = rem.chars().next() {
586 if let Some(quote) = expr_quote {
587 if expr_escaped {
588 expr_escaped = false;
589 i += ch.len_utf8();
590 } else if ch == '\\' {
591 expr_escaped = true;
592 i += 1;
593 } else if ch == quote {
594 expr_quote = None;
595 i += ch.len_utf8();
596 } else {
597 i += ch.len_utf8();
598 }
599 } else if ch == '"' || ch == '\'' {
600 expr_quote = Some(ch);
601 i += ch.len_utf8();
602 } else if ch == '{' {
603 interpolation_depth += 1;
604 i += 1;
605 } else if ch == '}' {
606 interpolation_depth = interpolation_depth.saturating_sub(1);
607 i += 1;
608 if interpolation_depth == 0 {
609 interpolation_start = None;
610 }
611 } else {
612 i += ch.len_utf8();
613 }
614 State::FormattedString {
615 mode,
616 escaped,
617 interpolation_depth,
618 interpolation_start,
619 expr_quote,
620 expr_escaped,
621 }
622 } else {
623 break;
624 }
625 }
626 State::FormattedTripleString {
627 mode,
628 mut interpolation_depth,
629 mut interpolation_start,
630 mut expr_quote,
631 mut expr_escaped,
632 } => {
633 if interpolation_depth == 0 {
634 if rem.starts_with("\"\"\"") {
635 i += 3;
636 State::Normal
637 } else if mode == InterpolationMode::Braces
638 && (rem.starts_with("{{") || rem.starts_with("}}"))
639 {
640 i += 2;
641 State::FormattedTripleString {
642 mode,
643 interpolation_depth,
644 interpolation_start,
645 expr_quote,
646 expr_escaped,
647 }
648 } else if mode != InterpolationMode::Braces {
649 let sigil = mode.sigil().expect("sigil mode must provide sigil");
650 let mut esc = String::new();
651 esc.push(sigil);
652 esc.push(sigil);
653 esc.push('{');
654 let mut opener = String::new();
655 opener.push(sigil);
656 opener.push('{');
657
658 if rem.starts_with(&esc) {
659 i += esc.len();
660 State::FormattedTripleString {
661 mode,
662 interpolation_depth,
663 interpolation_start,
664 expr_quote,
665 expr_escaped,
666 }
667 } else if rem.starts_with(&opener) {
668 interpolation_depth = 1;
669 interpolation_start = Some(i + opener.len());
670 i += opener.len();
671 State::FormattedTripleString {
672 mode,
673 interpolation_depth,
674 interpolation_start,
675 expr_quote,
676 expr_escaped,
677 }
678 } else if let Some(ch) = rem.chars().next() {
679 i += ch.len_utf8();
680 State::FormattedTripleString {
681 mode,
682 interpolation_depth,
683 interpolation_start,
684 expr_quote,
685 expr_escaped,
686 }
687 } else {
688 break;
689 }
690 } else if let Some(ch) = rem.chars().next() {
691 if ch == '{' {
692 interpolation_depth = 1;
693 interpolation_start = Some(i + 1);
694 i += 1;
695 } else {
696 i += ch.len_utf8();
697 }
698 State::FormattedTripleString {
699 mode,
700 interpolation_depth,
701 interpolation_start,
702 expr_quote,
703 expr_escaped,
704 }
705 } else {
706 break;
707 }
708 } else if let Some(ch) = rem.chars().next() {
709 if let Some(quote) = expr_quote {
710 if expr_escaped {
711 expr_escaped = false;
712 i += ch.len_utf8();
713 } else if ch == '\\' {
714 expr_escaped = true;
715 i += 1;
716 } else if ch == quote {
717 expr_quote = None;
718 i += ch.len_utf8();
719 } else {
720 i += ch.len_utf8();
721 }
722 } else if ch == '"' || ch == '\'' {
723 expr_quote = Some(ch);
724 i += ch.len_utf8();
725 } else if ch == '{' {
726 interpolation_depth += 1;
727 i += 1;
728 } else if ch == '}' {
729 interpolation_depth = interpolation_depth.saturating_sub(1);
730 i += 1;
731 if interpolation_depth == 0 {
732 interpolation_start = None;
733 }
734 } else {
735 i += ch.len_utf8();
736 }
737 State::FormattedTripleString {
738 mode,
739 interpolation_depth,
740 interpolation_start,
741 expr_quote,
742 expr_escaped,
743 }
744 } else {
745 break;
746 }
747 }
748 };
749 }
750
751 match state {
752 State::FormattedString {
753 interpolation_depth,
754 interpolation_start,
755 ..
756 }
757 | State::FormattedTripleString {
758 interpolation_depth,
759 interpolation_start,
760 ..
761 } => {
762 if interpolation_depth == 0 {
763 FormattedCursorContext::InFormattedLiteral
764 } else if let Some(start) = interpolation_start {
765 let prefix = text
766 .get(start..capped_offset)
767 .unwrap_or_default()
768 .to_string();
769 FormattedCursorContext::InInterpolationExpr {
770 expr_prefix: prefix,
771 }
772 } else {
773 FormattedCursorContext::InFormattedLiteral
774 }
775 }
776 _ => FormattedCursorContext::OutsideFormattedString,
777 }
778}
779
780fn detect_join_body_context(
783 text: &str,
784 current_line: usize,
785 cursor_char: usize,
786) -> Option<CompletionContext> {
787 let lines: Vec<&str> = text.lines().collect();
788 let strategies = ["all", "race", "any", "settle"];
789
790 let mut brace_depth: i32 = 0;
792 let mut i = current_line;
793 loop {
794 let line = lines.get(i)?;
795 let effective = if i == current_line {
797 let end = cursor_char.min(line.len());
798 &line[..end]
799 } else {
800 line
801 };
802 for ch in effective.chars().rev() {
803 match ch {
804 '}' => brace_depth += 1,
805 '{' => brace_depth -= 1,
806 _ => {}
807 }
808 }
809 if brace_depth < 0 {
811 let trimmed = line.trim();
812 for strategy in &strategies {
813 let join_pattern = format!("join {} {{", strategy);
815 let join_pattern_no_brace = format!("join {}", strategy);
816 if trimmed.contains(&join_pattern)
817 || (trimmed.ends_with('{') && trimmed.contains(&join_pattern_no_brace))
818 {
819 return Some(CompletionContext::JoinBody {
820 strategy: strategy.to_string(),
821 });
822 }
823 }
824 return None;
825 }
826 if i == 0 {
827 break;
828 }
829 i -= 1;
830 }
831 None
832}
833
834fn interpolation_format_spec_prefix(interpolation_expr_prefix: &str) -> Option<String> {
835 let idx = shape_ast::interpolation::find_top_level_format_colon(interpolation_expr_prefix)?;
836 let spec = interpolation_expr_prefix.get(idx + 1..)?.to_string();
837 Some(spec)
838}
839
840fn extract_object_before_dot(text: &str) -> &str {
842 let trimmed = text.trim_end();
843
844 let mut bracket_depth = 0;
847 let mut start = trimmed.len();
848
849 for (i, ch) in trimmed.char_indices().rev() {
850 if ch == ']' {
851 bracket_depth += 1;
852 } else if ch == '[' {
853 bracket_depth -= 1;
854 } else if bracket_depth == 0 && (ch.is_whitespace() || "(){}< >+-*/=!,;".contains(ch)) {
855 start = i + ch.len_utf8();
856 break;
857 }
858 if i == 0 {
859 start = 0;
860 }
861 }
862
863 &trimmed[start..]
864}
865
866pub fn detect_type_alias_override_context_pub(
868 text: &str,
869 line: usize,
870 cursor_char: usize,
871) -> Option<String> {
872 detect_type_alias_override_context(text, line, cursor_char)
873}
874
875fn detect_type_alias_override_context(
880 text: &str,
881 current_line: usize,
882 cursor_char: usize,
883) -> Option<String> {
884 let lines: Vec<&str> = text.lines().collect();
885
886 let mut brace_depth: i32 = 0;
889 let mut i = current_line;
890 loop {
891 let line = lines.get(i)?;
892 let effective_line = if i == current_line {
895 let end = cursor_char.min(line.len());
896 &line[..end]
897 } else {
898 line
899 };
900 for ch in effective_line.chars().rev() {
902 match ch {
903 '}' => brace_depth += 1,
904 '{' => brace_depth -= 1,
905 _ => {}
906 }
907 }
908 if brace_depth < 0 {
910 let trimmed = line.trim();
911 if trimmed.starts_with("type ") {
913 let rest = trimmed.strip_prefix("type ")?.trim();
914 let after_name = rest
916 .split(|c: char| c.is_whitespace() || c == '<')
917 .next()
918 .map(|name| &rest[name.len()..])?;
919 let after_generics = if after_name.trim_start().starts_with('<') {
921 let mut depth = 0;
923 let mut end = 0;
924 for (j, c) in after_name.trim_start().char_indices() {
925 match c {
926 '<' => depth += 1,
927 '>' => {
928 depth -= 1;
929 if depth == 0 {
930 end = j + 1;
931 break;
932 }
933 }
934 _ => {}
935 }
936 }
937 &after_name.trim_start()[end..]
938 } else {
939 after_name
940 };
941 let after_eq = after_generics.trim_start().strip_prefix('=')?;
943 let base_type = after_eq
944 .trim()
945 .split(|c: char| c == '{' || c.is_whitespace())
946 .next()?
947 .trim();
948 if !base_type.is_empty() {
949 return Some(base_type.to_string());
950 }
951 }
952 return None;
953 }
954 if i == 0 {
955 break;
956 }
957 i -= 1;
958 }
959 None
960}
961
962fn detect_impl_block_context(text: &str, current_line: usize) -> Option<CompletionContext> {
964 let lines: Vec<&str> = text.lines().collect();
965
966 let mut in_impl = false;
967 let mut trait_name = String::new();
968 let mut target_type = String::new();
969 let mut existing_methods = Vec::new();
970 let mut brace_count: i32 = 0;
971
972 for (i, line) in lines.iter().enumerate() {
973 if i > current_line {
974 break;
975 }
976
977 let trimmed = line.trim();
978 if trimmed.starts_with("impl ") && !in_impl {
980 let rest = trimmed.strip_prefix("impl ").unwrap().trim();
982 let parts: Vec<&str> = rest.splitn(4, ' ').collect();
983 if parts.len() >= 3 && parts[1] == "for" {
984 trait_name = parts[0].to_string();
985 target_type = parts[2].trim_end_matches('{').trim().to_string();
987 in_impl = true;
988 existing_methods.clear();
989 }
990 }
991
992 if in_impl && trimmed.starts_with("method ") {
994 let method_rest = trimmed.strip_prefix("method ").unwrap().trim();
995 if let Some(name) = method_rest
996 .split(|c: char| c == '(' || c.is_whitespace())
997 .next()
998 {
999 if !name.is_empty() {
1000 existing_methods.push(name.to_string());
1001 }
1002 }
1003 }
1004
1005 brace_count += line.matches('{').count() as i32;
1006 brace_count -= line.matches('}').count() as i32;
1007
1008 if in_impl && brace_count == 0 && line.contains('}') {
1009 in_impl = false;
1010 trait_name.clear();
1011 target_type.clear();
1012 existing_methods.clear();
1013 }
1014 }
1015
1016 if in_impl && brace_count > 0 && !trait_name.is_empty() {
1017 Some(CompletionContext::ImplBlock {
1018 trait_name,
1019 target_type,
1020 existing_methods,
1021 })
1022 } else {
1023 None
1024 }
1025}
1026
1027fn is_inside_pattern_body(text: &str, current_line: usize) -> bool {
1029 let lines: Vec<&str> = text.lines().collect();
1030
1031 let mut in_pattern = false;
1032 let mut brace_count = 0;
1033
1034 for (i, line) in lines.iter().enumerate() {
1035 if i > current_line {
1036 break;
1037 }
1038
1039 if line.trim().starts_with("pattern") {
1040 in_pattern = true;
1041 }
1042
1043 brace_count += line.matches('{').count() as i32;
1044 brace_count -= line.matches('}').count() as i32;
1045
1046 if in_pattern && brace_count == 0 && line.contains('}') {
1047 in_pattern = false;
1048 }
1049 }
1050
1051 in_pattern && brace_count > 0
1052}
1053
1054fn extract_function_call(text: &str) -> Option<String> {
1057 if let Some(paren_pos) = text.rfind('(') {
1059 let before_paren = text[..paren_pos].trim_end();
1060
1061 let start = before_paren
1063 .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
1064 .map(|i| i + 1)
1065 .unwrap_or(0);
1066 let func_name = &before_paren[start..];
1067
1068 if !func_name.is_empty() {
1069 return Some(func_name.to_string());
1070 }
1071 }
1072 None
1073}
1074
1075fn analyze_argument_context(text_before_cursor: &str, function: &str) -> ArgumentContext {
1077 if let Some(paren_pos) = text_before_cursor.rfind('(') {
1079 let params_text = &text_before_cursor[paren_pos + 1..];
1080
1081 if is_inside_object_literal(params_text) {
1083 if let Some(property_name) = extract_property_name(params_text) {
1085 return ArgumentContext::ObjectLiteralValue {
1086 containing_function: Some(function.to_string()),
1087 property_name,
1088 };
1089 } else {
1090 return ArgumentContext::ObjectLiteralPropertyName {
1091 containing_function: Some(function.to_string()),
1092 };
1093 }
1094 }
1095
1096 let arg_index = count_commas_outside_nested(params_text);
1098
1099 return ArgumentContext::FunctionArgument {
1100 function: function.to_string(),
1101 arg_index,
1102 };
1103 }
1104
1105 ArgumentContext::General
1106}
1107
1108fn is_inside_object_literal(text: &str) -> bool {
1110 let open_braces = text.matches('{').count();
1111 let close_braces = text.matches('}').count();
1112 open_braces > close_braces
1113}
1114
1115fn extract_property_name(text: &str) -> Option<String> {
1117 let start = text.rfind(['{', ',']).map(|i| i + 1).unwrap_or(0);
1119 let fragment = text[start..].trim();
1120
1121 if let Some(colon_pos) = fragment.find(':') {
1123 let prop = fragment[..colon_pos].trim();
1124 if !prop.is_empty() {
1125 return Some(prop.to_string());
1126 }
1127 }
1128
1129 None
1130}
1131
1132fn count_commas_outside_nested(text: &str) -> usize {
1134 let mut count: usize = 0;
1135 let mut paren_depth: i32 = 0;
1136 let mut brace_depth: i32 = 0;
1137 let mut bracket_depth: i32 = 0;
1138 let mut in_string = false;
1139 let mut escape_next = false;
1140
1141 for ch in text.chars() {
1142 if escape_next {
1143 escape_next = false;
1144 continue;
1145 }
1146
1147 match ch {
1148 '\\' if in_string => escape_next = true,
1149 '"' | '\'' => in_string = !in_string,
1150 '(' if !in_string => paren_depth += 1,
1151 ')' if !in_string => paren_depth = paren_depth.saturating_sub(1),
1152 '{' if !in_string => brace_depth += 1,
1153 '}' if !in_string => brace_depth = brace_depth.saturating_sub(1),
1154 '[' if !in_string => bracket_depth += 1,
1155 ']' if !in_string => bracket_depth = bracket_depth.saturating_sub(1),
1156 ',' if !in_string && paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 => {
1157 count += 1
1158 }
1159 _ => {}
1160 }
1161 }
1162
1163 count
1164}
1165
1166fn is_in_type_annotation_position(text: &str) -> bool {
1172 let trimmed = text.trim_end();
1173
1174 if trimmed.ends_with(':') {
1176 return true;
1177 }
1178
1179 if let Some(colon_idx) = find_unquoted_colon(trimmed) {
1182 let after_colon = &trimmed[colon_idx + 1..];
1183 if after_colon.contains('=') || after_colon.contains('{') {
1185 return false;
1186 }
1187 let before_colon = &trimmed[..colon_idx];
1188 if is_var_decl_context(before_colon) || is_param_context(before_colon) {
1190 return true;
1191 }
1192 }
1193
1194 if let Some(arrow_idx) = trimmed.rfind("->") {
1196 let after_arrow = &trimmed[arrow_idx + 2..];
1197 if !after_arrow.contains('=') && !after_arrow.contains('{') {
1198 return true;
1199 }
1200 }
1201
1202 false
1203}
1204
1205fn find_unquoted_colon(text: &str) -> Option<usize> {
1207 let mut in_string = false;
1208 let mut last_colon = None;
1209
1210 for (i, ch) in text.char_indices() {
1211 if ch == '"' {
1212 in_string = !in_string;
1213 }
1214 if ch == ':' && !in_string {
1215 last_colon = Some(i);
1216 }
1217 }
1218 last_colon
1219}
1220
1221fn is_var_decl_context(before_colon: &str) -> bool {
1223 let trimmed = before_colon.trim();
1224 trimmed.starts_with("let ") || trimmed.starts_with("const ")
1226}
1227
1228fn is_param_context(before_colon: &str) -> bool {
1230 let open = before_colon.matches('(').count();
1232 let close = before_colon.matches(')').count();
1233 open > close
1234}
1235
1236fn detect_import_context(text_before_cursor: &str) -> Option<CompletionContext> {
1243 let trimmed = text_before_cursor.trim();
1244
1245 if let Some(rest) = trimmed.strip_prefix("from ") {
1247 if let Some(use_pos) = rest.find(" use") {
1248 let module = rest[..use_pos].trim().to_string();
1249 if !module.is_empty() && rest[use_pos..].contains('{') {
1250 return Some(CompletionContext::ImportItems { module });
1251 }
1252 }
1253 let module_text = rest.trim();
1255 if module_text.contains('.') {
1256 let prefix = if module_text.ends_with('.') {
1260 module_text.trim_end_matches('.').to_string()
1261 } else if let Some(dot_pos) = module_text.rfind('.') {
1262 module_text[..dot_pos].to_string()
1263 } else {
1264 module_text.to_string()
1265 };
1266 return Some(CompletionContext::FromModulePartial { prefix });
1267 }
1268 return Some(CompletionContext::FromModule);
1270 }
1271
1272 if let Some(rest) = trimmed.strip_prefix("use ") {
1273 let rest = rest.trim();
1274 if !rest.starts_with('{') {
1275 return Some(CompletionContext::ImportModule);
1276 }
1277 }
1278
1279 if trimmed == "use" {
1281 return Some(CompletionContext::ImportModule);
1282 }
1283 if trimmed == "from" {
1284 return Some(CompletionContext::FromModule);
1285 }
1286
1287 None
1288}
1289
1290fn detect_pipe_context(text_before_cursor: &str) -> Option<CompletionContext> {
1294 let trimmed = text_before_cursor.trim_end();
1295
1296 if trimmed.ends_with("|>") {
1298 return Some(CompletionContext::PipeTarget {
1299 pipe_input_type: None,
1300 });
1301 }
1302
1303 if let Some(pipe_pos) = trimmed.rfind("|>") {
1306 let after_pipe = trimmed[pipe_pos + 2..].trim();
1307 if !after_pipe.is_empty() && after_pipe.chars().all(|c| c.is_alphanumeric() || c == '_') {
1310 return Some(CompletionContext::PipeTarget {
1311 pipe_input_type: None,
1312 });
1313 }
1314 }
1315
1316 None
1317}
1318
1319fn is_at_annotation_position(text: &str) -> bool {
1321 let trimmed = text.trim_end();
1322
1323 if trimmed.ends_with('@') {
1325 let before_at = trimmed.trim_end_matches('@').trim_end();
1327 return before_at.is_empty() || before_at.ends_with('\n');
1328 }
1329
1330 false
1331}
1332
1333fn is_in_trait_bound_position(text: &str) -> bool {
1336 let trimmed = text.trim_end();
1337
1338 let mut angle_depth: i32 = 0;
1340 let mut last_angle_open = None;
1341 for (i, ch) in trimmed.char_indices() {
1342 match ch {
1343 '<' => {
1344 angle_depth += 1;
1345 last_angle_open = Some(i);
1346 }
1347 '>' => angle_depth -= 1,
1348 _ => {}
1349 }
1350 }
1351 if angle_depth <= 0 {
1352 return false;
1353 }
1354
1355 let inside_angles = if let Some(start) = last_angle_open {
1357 &trimmed[start + 1..]
1358 } else {
1359 return false;
1360 };
1361
1362 let last_segment = inside_angles
1365 .rsplit(',')
1366 .next()
1367 .unwrap_or(inside_angles)
1368 .trim();
1369
1370 if let Some(colon_pos) = last_segment.find(':') {
1371 let before_colon = last_segment[..colon_pos].trim();
1372 if !before_colon.is_empty()
1374 && before_colon
1375 .chars()
1376 .all(|c| c.is_alphanumeric() || c == '_')
1377 {
1378 let before_angle = if let Some(start) = last_angle_open {
1380 trimmed[..start].trim()
1381 } else {
1382 ""
1383 };
1384 let is_type_param_context = before_angle.starts_with("fn ")
1385 || before_angle.starts_with("function ")
1386 || before_angle.starts_with("trait ")
1387 || before_angle.starts_with("type ")
1388 || before_angle.contains(" fn ")
1389 || before_angle.contains(" function ")
1390 || before_angle.ends_with("fn")
1391 || before_angle.ends_with("function")
1392 || before_angle.ends_with("trait")
1393 || before_angle.ends_with("type");
1394 return is_type_param_context;
1395 }
1396 }
1397
1398 false
1399}
1400
1401fn is_inside_comptime_block(text: &str, current_line: usize) -> bool {
1405 let lines: Vec<&str> = text.lines().collect();
1406
1407 let mut in_comptime = false;
1408 let mut brace_count: i32 = 0;
1409
1410 for (i, line) in lines.iter().enumerate() {
1411 if i > current_line {
1412 break;
1413 }
1414
1415 let trimmed = line.trim();
1416 if (trimmed.starts_with("comptime {")
1418 || trimmed.starts_with("comptime{")
1419 || trimmed == "comptime")
1420 && !in_comptime
1421 {
1422 in_comptime = true;
1423 }
1424 if !in_comptime && trimmed.contains("comptime {") {
1426 in_comptime = true;
1427 }
1428
1429 brace_count += line.matches('{').count() as i32;
1430 brace_count -= line.matches('}').count() as i32;
1431
1432 if in_comptime && brace_count == 0 && line.contains('}') {
1433 in_comptime = false;
1434 }
1435 }
1436
1437 in_comptime && brace_count > 0
1438}
1439
1440fn is_at_expr_annotation_position(text: &str) -> bool {
1445 let trimmed = text.trim_end();
1446
1447 if !trimmed.ends_with('@') {
1448 return false;
1449 }
1450
1451 let before_at = trimmed.trim_end_matches('@').trim_end();
1453
1454 if before_at.is_empty() || before_at.ends_with('\n') {
1457 return false;
1458 }
1459
1460 let last_char = before_at.chars().last().unwrap_or(' ');
1462 matches!(last_char, '=' | '(' | ',' | '>' | '{' | '[' | ';' | '|')
1463 || before_at.ends_with("return")
1464 || before_at.ends_with("return ")
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469 use super::*;
1470
1471 #[test]
1472 fn test_general_context() {
1473 let text = "let x = ";
1474 let position = Position {
1475 line: 0,
1476 character: 8,
1477 };
1478
1479 let context = analyze_context(text, position);
1480 assert_eq!(context, CompletionContext::General);
1481 }
1482
1483 #[test]
1484 fn test_property_access_context() {
1485 let text = "data[0].";
1486 let position = Position {
1487 line: 0,
1488 character: 8,
1489 };
1490
1491 let context = analyze_context(text, position);
1492 match context {
1493 CompletionContext::PropertyAccess { object } => {
1494 assert_eq!(object, "data[0]");
1495 }
1496 _ => panic!("Expected PropertyAccess context"),
1497 }
1498 }
1499
1500 #[test]
1501 fn test_property_access_context_inside_formatted_string_expression() {
1502 let text = r#"let msg = f"value: {user.}";"#;
1503 let position = Position {
1504 line: 0,
1505 character: text.find("user.").unwrap() as u32 + 5,
1506 };
1507
1508 let context = analyze_context(text, position);
1509 match context {
1510 CompletionContext::PropertyAccess { object } => {
1511 assert_eq!(object, "user");
1512 }
1513 _ => panic!("Expected PropertyAccess context inside interpolation"),
1514 }
1515 }
1516
1517 #[test]
1518 fn test_no_property_context_inside_formatted_string_literal_text() {
1519 let text = r#"let msg = f"price.path {user}";"#;
1520 let position = Position {
1521 line: 0,
1522 character: text.find("path").unwrap() as u32 + 2,
1523 };
1524
1525 let context = analyze_context(text, position);
1526 assert_eq!(context, CompletionContext::General);
1527 }
1528
1529 #[test]
1530 fn test_function_call_context_inside_formatted_string_expression() {
1531 let text = r#"let msg = f"value: {sma(}";"#;
1532 let position = Position {
1533 line: 0,
1534 character: text.find("sma(").unwrap() as u32 + 4,
1535 };
1536
1537 let context = analyze_context(text, position);
1538 match context {
1539 CompletionContext::FunctionCall { function, .. } => {
1540 assert_eq!(function, "sma");
1541 }
1542 _ => panic!("Expected FunctionCall context inside interpolation"),
1543 }
1544 }
1545
1546 #[test]
1547 fn test_doc_tag_context() {
1548 let text = "/// @pa\nfn add(x: number) -> number { x }\n";
1549 let position = Position {
1550 line: 0,
1551 character: 7,
1552 };
1553
1554 let context = analyze_context(text, position);
1555 assert_eq!(
1556 context,
1557 CompletionContext::DocTag {
1558 prefix: "pa".to_string()
1559 }
1560 );
1561 }
1562
1563 #[test]
1564 fn test_doc_param_context() {
1565 let text = "/// @param va\nfn add(value: number) -> number { value }\n";
1566 let position = Position {
1567 line: 0,
1568 character: 13,
1569 };
1570
1571 let context = analyze_context(text, position);
1572 assert_eq!(
1573 context,
1574 CompletionContext::DocParamName {
1575 prefix: "va".to_string()
1576 }
1577 );
1578 }
1579
1580 #[test]
1581 fn test_doc_link_context() {
1582 let text = "/// @see std::co\nfn add(x: number) -> number { x }\n";
1583 let position = Position {
1584 line: 0,
1585 character: 16,
1586 };
1587
1588 let context = analyze_context(text, position);
1589 assert_eq!(
1590 context,
1591 CompletionContext::DocLinkTarget {
1592 prefix: "std::co".to_string()
1593 }
1594 );
1595 }
1596
1597 #[test]
1598 fn test_property_access_context_inside_dollar_formatted_string_expression() {
1599 let text = r#"let msg = f$"value: ${user.}";"#;
1600 let position = Position {
1601 line: 0,
1602 character: text.find("user.").unwrap() as u32 + 5,
1603 };
1604
1605 let context = analyze_context(text, position);
1606 match context {
1607 CompletionContext::PropertyAccess { object } => {
1608 assert_eq!(object, "user");
1609 }
1610 _ => panic!("Expected PropertyAccess context inside dollar interpolation"),
1611 }
1612 }
1613
1614 #[test]
1615 fn test_function_call_context_inside_hash_formatted_string_expression() {
1616 let text = "let cmd = f#\"run #{build(}\"";
1617 let position = Position {
1618 line: 0,
1619 character: text.find("build(").unwrap() as u32 + 6,
1620 };
1621
1622 let context = analyze_context(text, position);
1623 match context {
1624 CompletionContext::FunctionCall { function, .. } => {
1625 assert_eq!(function, "build");
1626 }
1627 _ => panic!("Expected FunctionCall context inside hash interpolation"),
1628 }
1629 }
1630
1631 #[test]
1632 fn test_pattern_reference_context() {
1633 let text = "find ";
1634 let position = Position {
1635 line: 0,
1636 character: 5,
1637 };
1638
1639 let context = analyze_context(text, position);
1640 assert_eq!(context, CompletionContext::PatternReference);
1641 }
1642
1643 #[test]
1644 fn test_function_call_context() {
1645 let text = "sma(";
1646 let position = Position {
1647 line: 0,
1648 character: 4,
1649 };
1650
1651 let context = analyze_context(text, position);
1652 match context {
1653 CompletionContext::FunctionCall { function, .. } => {
1654 assert_eq!(function, "sma");
1655 }
1656 _ => panic!("Expected FunctionCall context"),
1657 }
1658 }
1659
1660 #[test]
1661 fn test_extract_object_before_dot() {
1662 assert_eq!(extract_object_before_dot("data[0]"), "data[0]");
1663 assert_eq!(extract_object_before_dot("let x = myvar"), "myvar");
1664 assert_eq!(extract_object_before_dot("data"), "data");
1665 }
1666
1667 #[test]
1668 fn test_type_annotation_context_after_colon() {
1669 let text = "let series: ";
1670 let position = Position {
1671 line: 0,
1672 character: 12,
1673 };
1674
1675 let context = analyze_context(text, position);
1676 assert_eq!(context, CompletionContext::TypeAnnotation);
1677 }
1678
1679 #[test]
1680 fn test_type_annotation_context_typing_type() {
1681 let text = "let series: S";
1683 let position = Position {
1684 line: 0,
1685 character: 13,
1686 };
1687
1688 let context = analyze_context(text, position);
1689 assert_eq!(context, CompletionContext::TypeAnnotation);
1690 }
1691
1692 #[test]
1693 fn test_type_annotation_context_typing_full_type() {
1694 let text = "let table: Table";
1696 let position = Position {
1697 line: 0,
1698 character: 16,
1699 };
1700
1701 let context = analyze_context(text, position);
1702 assert_eq!(context, CompletionContext::TypeAnnotation);
1703 }
1704
1705 #[test]
1706 fn test_type_annotation_context_after_equals() {
1707 let text = "let table: Table = ";
1709 let position = Position {
1710 line: 0,
1711 character: 19,
1712 };
1713
1714 let context = analyze_context(text, position);
1715 assert_ne!(context, CompletionContext::TypeAnnotation);
1716 }
1717
1718 #[test]
1719 fn test_type_annotation_context_function_param() {
1720 let text = "function foo(x: ";
1722 let position = Position {
1723 line: 0,
1724 character: 16,
1725 };
1726
1727 let context = analyze_context(text, position);
1728 assert_eq!(context, CompletionContext::TypeAnnotation);
1729 }
1730
1731 #[test]
1732 fn test_type_annotation_context_return_type() {
1733 let text = "function foo() -> ";
1735 let position = Position {
1736 line: 0,
1737 character: 18,
1738 };
1739
1740 let context = analyze_context(text, position);
1741 assert_eq!(context, CompletionContext::TypeAnnotation);
1742 }
1743
1744 #[test]
1745 fn test_type_annotation_context_return_type_typing() {
1746 let text = "function foo() -> Res";
1748 let position = Position {
1749 line: 0,
1750 character: 21,
1751 };
1752
1753 let context = analyze_context(text, position);
1754 assert_eq!(context, CompletionContext::TypeAnnotation);
1755 }
1756
1757 #[test]
1758 fn test_use_module_context() {
1759 let context = analyze_context(
1760 "use ",
1761 Position {
1762 line: 0,
1763 character: 4,
1764 },
1765 );
1766 assert_eq!(context, CompletionContext::ImportModule);
1767 }
1768
1769 #[test]
1770 fn test_from_module_context() {
1771 let context = analyze_context(
1772 "from ",
1773 Position {
1774 line: 0,
1775 character: 5,
1776 },
1777 );
1778 assert_eq!(context, CompletionContext::FromModule);
1779 }
1780
1781 #[test]
1782 fn test_from_import_no_longer_triggers_items() {
1783 let context = analyze_context(
1786 "from std::core::csv import { ",
1787 Position {
1788 line: 0,
1789 character: 29,
1790 },
1791 );
1792 assert_eq!(context, CompletionContext::FromModule);
1793 }
1794
1795 #[test]
1796 fn test_from_use_items_context() {
1797 let context = analyze_context(
1798 "from std::core::csv use { ",
1799 Position {
1800 line: 0,
1801 character: 26,
1802 },
1803 );
1804 assert_eq!(
1805 context,
1806 CompletionContext::ImportItems {
1807 module: "std::core::csv".to_string()
1808 }
1809 );
1810 }
1811
1812 #[test]
1813 fn test_use_not_object_literal() {
1814 let context = analyze_context(
1816 "use ml",
1817 Position {
1818 line: 0,
1819 character: 6,
1820 },
1821 );
1822 assert_eq!(context, CompletionContext::ImportModule);
1823 }
1824
1825 #[test]
1826 fn test_module_dot_access() {
1827 let context = analyze_context(
1828 "csv.",
1829 Position {
1830 line: 0,
1831 character: 4,
1832 },
1833 );
1834 assert_eq!(
1835 context,
1836 CompletionContext::PropertyAccess {
1837 object: "csv".to_string()
1838 }
1839 );
1840 }
1841
1842 #[test]
1843 fn test_module_method_call_context() {
1844 let context = analyze_context(
1845 "duckdb.query(",
1846 Position {
1847 line: 0,
1848 character: 13,
1849 },
1850 );
1851 match context {
1852 CompletionContext::FunctionCall { function, .. } => {
1853 assert!(
1854 function.contains("duckdb.query"),
1855 "Expected function to contain 'duckdb.query', got '{}'",
1856 function
1857 );
1858 }
1859 _ => panic!("Expected FunctionCall context, got {:?}", context),
1860 }
1861 }
1862
1863 #[test]
1864 fn test_pipe_context_detection() {
1865 let context = analyze_context(
1866 "data |> ",
1867 Position {
1868 line: 0,
1869 character: 8,
1870 },
1871 );
1872 assert!(
1873 matches!(context, CompletionContext::PipeTarget { .. }),
1874 "Expected PipeTarget context, got {:?}",
1875 context
1876 );
1877 }
1878
1879 #[test]
1880 fn test_pipe_context_with_chain() {
1881 let context = analyze_context(
1882 "data |> filter(p) |> ",
1883 Position {
1884 line: 0,
1885 character: 21,
1886 },
1887 );
1888 assert!(
1889 matches!(context, CompletionContext::PipeTarget { .. }),
1890 "Expected PipeTarget context after chained pipe, got {:?}",
1891 context
1892 );
1893 }
1894
1895 #[test]
1896 fn test_pipe_not_detected_in_bitwise_or() {
1897 let context = analyze_context(
1899 "a | b",
1900 Position {
1901 line: 0,
1902 character: 5,
1903 },
1904 );
1905 assert!(
1906 !matches!(context, CompletionContext::PipeTarget { .. }),
1907 "Bitwise OR should NOT be PipeTarget, got {:?}",
1908 context
1909 );
1910 }
1911
1912 #[test]
1913 fn test_pipe_context_typing_identifier() {
1914 let context = analyze_context(
1916 "data |> fi",
1917 Position {
1918 line: 0,
1919 character: 10,
1920 },
1921 );
1922 assert!(
1923 matches!(context, CompletionContext::PipeTarget { .. }),
1924 "Expected PipeTarget while typing after pipe, got {:?}",
1925 context
1926 );
1927 }
1928
1929 #[test]
1930 fn test_fstring_empty_interpolation() {
1931 let text = r#"let s = f"hello {}""#;
1933 let cursor = text.find("{}").unwrap() as u32 + 1; let context = analyze_context(
1935 text,
1936 Position {
1937 line: 0,
1938 character: cursor,
1939 },
1940 );
1941 assert_eq!(
1943 context,
1944 CompletionContext::General,
1945 "Empty f-string interpolation should give General context for variable completions"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_fstring_identifier_completion() {
1951 let text = r#"let s = f"hello {x}""#;
1953 let cursor = text.find("{x").unwrap() as u32 + 2; let context = analyze_context(
1955 text,
1956 Position {
1957 line: 0,
1958 character: cursor,
1959 },
1960 );
1961 assert_eq!(
1963 context,
1964 CompletionContext::General,
1965 "f-string identifier should give General context, got {:?}",
1966 context
1967 );
1968 }
1969
1970 #[test]
1971 fn test_fstring_method_call() {
1972 let text = r#"let s = f"val: {obj.method()}""#;
1974 let cursor = text.find("method(").unwrap() as u32 + 7; let context = analyze_context(
1976 text,
1977 Position {
1978 line: 0,
1979 character: cursor,
1980 },
1981 );
1982 match context {
1983 CompletionContext::FunctionCall { function, .. } => {
1984 assert!(
1985 function.contains("method"),
1986 "Expected function to contain 'method', got '{}'",
1987 function
1988 );
1989 }
1990 _ => panic!(
1991 "Expected FunctionCall context in f-string interpolation, got {:?}",
1992 context
1993 ),
1994 }
1995 }
1996
1997 #[test]
1998 fn test_fstring_format_spec_context_after_colon() {
1999 let text = r#"let s = f"value: {price:}""#;
2000 let cursor = text.find("price:").unwrap() as u32 + 6; let context = analyze_context(
2002 text,
2003 Position {
2004 line: 0,
2005 character: cursor,
2006 },
2007 );
2008 assert!(
2009 matches!(context, CompletionContext::InterpolationFormatSpec { .. }),
2010 "Expected InterpolationFormatSpec context, got {:?}",
2011 context
2012 );
2013 }
2014
2015 #[test]
2016 fn test_fstring_table_format_spec_context() {
2017 let text = r#"let s = f"{rows:table(align=)}""#;
2018 let cursor = text.find("align=").unwrap() as u32 + 6;
2019 let context = analyze_context(
2020 text,
2021 Position {
2022 line: 0,
2023 character: cursor,
2024 },
2025 );
2026 assert!(
2027 matches!(context, CompletionContext::InterpolationFormatSpec { .. }),
2028 "Expected InterpolationFormatSpec context, got {:?}",
2029 context
2030 );
2031 }
2032
2033 #[test]
2034 fn test_impl_block_context() {
2035 let text = "trait Q {\n filter(p): any\n}\nimpl Q for T {\n \n}\n";
2036 let position = Position {
2037 line: 4,
2038 character: 4,
2039 };
2040 let context = analyze_context(text, position);
2041 match context {
2042 CompletionContext::ImplBlock {
2043 trait_name,
2044 target_type,
2045 existing_methods,
2046 } => {
2047 assert_eq!(trait_name, "Q");
2048 assert_eq!(target_type, "T");
2049 assert!(existing_methods.is_empty());
2050 }
2051 _ => panic!("Expected ImplBlock context, got {:?}", context),
2052 }
2053 }
2054
2055 #[test]
2056 fn test_impl_block_context_with_existing_methods() {
2057 let text = "impl Queryable for MyTable {\n method filter(pred) { self }\n \n}\n";
2058 let position = Position {
2059 line: 2,
2060 character: 4,
2061 };
2062 let context = analyze_context(text, position);
2063 match context {
2064 CompletionContext::ImplBlock {
2065 trait_name,
2066 existing_methods,
2067 ..
2068 } => {
2069 assert_eq!(trait_name, "Queryable");
2070 assert_eq!(existing_methods, vec!["filter".to_string()]);
2071 }
2072 _ => panic!("Expected ImplBlock context, got {:?}", context),
2073 }
2074 }
2075
2076 #[test]
2077 fn test_impl_block_context_after_close() {
2078 let text = "impl Q for T {\n method foo() { self }\n}\nlet x = ";
2080 let position = Position {
2081 line: 3,
2082 character: 8,
2083 };
2084 let context = analyze_context(text, position);
2085 assert!(
2086 !matches!(context, CompletionContext::ImplBlock { .. }),
2087 "Should not be ImplBlock after closing brace, got {:?}",
2088 context
2089 );
2090 }
2091
2092 #[test]
2093 fn test_type_alias_override_context() {
2094 let text = "type Currency { comptime symbol: string = \"$\", amount: number }\ntype EUR = Currency { ";
2095 let position = Position {
2096 line: 1,
2097 character: 22,
2098 };
2099 let context = analyze_context(text, position);
2100 match context {
2101 CompletionContext::TypeAliasOverride { base_type } => {
2102 assert_eq!(base_type, "Currency");
2103 }
2104 _ => panic!("Expected TypeAliasOverride context, got {:?}", context),
2105 }
2106 }
2107
2108 #[test]
2109 fn test_type_alias_override_context_not_struct_def() {
2110 let text = "type Currency { ";
2112 let position = Position {
2113 line: 0,
2114 character: 16,
2115 };
2116 let context = analyze_context(text, position);
2117 assert!(
2118 !matches!(context, CompletionContext::TypeAliasOverride { .. }),
2119 "Struct def should not be TypeAliasOverride, got {:?}",
2120 context
2121 );
2122 }
2123
2124 #[test]
2125 fn test_join_body_context() {
2126 let text = "async fn foo() {\n await join all {\n ";
2127 let position = Position {
2128 line: 2,
2129 character: 4,
2130 };
2131 let context = analyze_context(text, position);
2132 match context {
2133 CompletionContext::JoinBody { strategy } => {
2134 assert_eq!(strategy, "all");
2135 }
2136 _ => panic!("Expected JoinBody context, got {:?}", context),
2137 }
2138 }
2139
2140 #[test]
2141 fn test_join_body_context_race() {
2142 let text = "async fn foo() {\n await join race {\n branch1,\n ";
2143 let position = Position {
2144 line: 3,
2145 character: 4,
2146 };
2147 let context = analyze_context(text, position);
2148 match context {
2149 CompletionContext::JoinBody { strategy } => {
2150 assert_eq!(strategy, "race");
2151 }
2152 _ => panic!("Expected JoinBody context with race, got {:?}", context),
2153 }
2154 }
2155
2156 #[test]
2157 fn test_join_body_not_after_close() {
2158 let text = "async fn foo() {\n await join all {\n 1, 2\n }\n ";
2159 let position = Position {
2160 line: 4,
2161 character: 2,
2162 };
2163 let context = analyze_context(text, position);
2164 assert!(
2165 !matches!(context, CompletionContext::JoinBody { .. }),
2166 "Should not be JoinBody after closing brace, got {:?}",
2167 context
2168 );
2169 }
2170
2171 #[test]
2172 fn test_trait_bound_context_after_colon() {
2173 let context = analyze_context(
2174 "fn foo<T: ",
2175 Position {
2176 line: 0,
2177 character: 10,
2178 },
2179 );
2180 assert_eq!(context, CompletionContext::TraitBound);
2181 }
2182
2183 #[test]
2184 fn test_trait_bound_context_typing_trait_name() {
2185 let context = analyze_context(
2186 "fn foo<T: Comp",
2187 Position {
2188 line: 0,
2189 character: 14,
2190 },
2191 );
2192 assert_eq!(context, CompletionContext::TraitBound);
2193 }
2194
2195 #[test]
2196 fn test_trait_bound_context_after_plus() {
2197 let context = analyze_context(
2198 "fn foo<T: Comparable + ",
2199 Position {
2200 line: 0,
2201 character: 23,
2202 },
2203 );
2204 assert_eq!(context, CompletionContext::TraitBound);
2205 }
2206
2207 #[test]
2208 fn test_trait_bound_not_in_comparison() {
2209 let context = analyze_context(
2211 "let x = a < b",
2212 Position {
2213 line: 0,
2214 character: 14,
2215 },
2216 );
2217 assert!(
2218 !matches!(context, CompletionContext::TraitBound),
2219 "Comparison should not be TraitBound, got {:?}",
2220 context
2221 );
2222 }
2223
2224 #[test]
2225 fn test_trait_bound_function_keyword() {
2226 let context = analyze_context(
2227 "function sort<T: ",
2228 Position {
2229 line: 0,
2230 character: 17,
2231 },
2232 );
2233 assert_eq!(context, CompletionContext::TraitBound);
2234 }
2235
2236 #[test]
2237 fn test_trait_bound_trait_keyword() {
2238 let context = analyze_context(
2239 "trait Sortable<T: ",
2240 Position {
2241 line: 0,
2242 character: 18,
2243 },
2244 );
2245 assert_eq!(context, CompletionContext::TraitBound);
2246 }
2247
2248 #[test]
2249 fn test_comptime_block_context() {
2250 let text = "comptime {\n ";
2251 let position = Position {
2252 line: 1,
2253 character: 4,
2254 };
2255 let context = analyze_context(text, position);
2256 assert_eq!(context, CompletionContext::ComptimeBlock);
2257 }
2258
2259 #[test]
2260 fn test_comptime_block_context_expression() {
2261 let text = "let x = comptime {\n ";
2262 let position = Position {
2263 line: 1,
2264 character: 4,
2265 };
2266 let context = analyze_context(text, position);
2267 assert_eq!(context, CompletionContext::ComptimeBlock);
2268 }
2269
2270 #[test]
2271 fn test_comptime_block_not_after_close() {
2272 let text = "comptime {\n implements(\"Foo\", \"Display\")\n}\nlet x = ";
2273 let position = Position {
2274 line: 3,
2275 character: 8,
2276 };
2277 let context = analyze_context(text, position);
2278 assert!(
2279 !matches!(context, CompletionContext::ComptimeBlock),
2280 "Should not be ComptimeBlock after closing brace, got {:?}",
2281 context
2282 );
2283 }
2284
2285 #[test]
2286 fn test_expr_annotation_after_equals() {
2287 let text = "let x = @";
2288 let position = Position {
2289 line: 0,
2290 character: 9,
2291 };
2292 let context = analyze_context(text, position);
2293 assert_eq!(context, CompletionContext::ExprAnnotation);
2294 }
2295
2296 #[test]
2297 fn test_expr_annotation_after_comma() {
2298 let text = "let x = [a, @";
2300 let position = Position {
2301 line: 0,
2302 character: 13,
2303 };
2304 let context = analyze_context(text, position);
2305 assert_eq!(context, CompletionContext::ExprAnnotation);
2306 }
2307
2308 #[test]
2309 fn test_item_annotation_not_expr_annotation() {
2310 let text = "@";
2312 let position = Position {
2313 line: 0,
2314 character: 1,
2315 };
2316 let context = analyze_context(text, position);
2317 assert!(
2319 !matches!(context, CompletionContext::ExprAnnotation),
2320 "Item-level @ should not be ExprAnnotation, got {:?}",
2321 context
2322 );
2323 }
2324}