Skip to main content

shape_lsp/
context.rs

1//! Context analysis for intelligent completions
2//!
3//! Determines the completion context based on cursor position and surrounding text.
4
5use 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/// Completion context based on cursor position
17#[derive(Debug, Clone, PartialEq)]
18pub enum CompletionContext {
19    /// General context - show all completions
20    General,
21    /// After a dot operator - show properties/methods
22    PropertyAccess {
23        /// The expression before the dot
24        object: String,
25    },
26    /// Inside a function call - show parameters
27    FunctionCall {
28        /// The function being called
29        function: String,
30        /// Detailed argument context
31        arg_context: ArgumentContext,
32    },
33    /// Inside a pattern definition
34    PatternBody,
35    /// Inside a query (find, scan, etc.)
36    Query {
37        /// The type of query (find, scan, analyze, backtest, alert)
38        query_type: String,
39    },
40    /// After "pattern" keyword - suggest pattern names
41    PatternReference,
42    /// Type annotation context
43    TypeAnnotation,
44    /// After typing "@" at function/pattern start
45    Annotation,
46    /// Inside annotation arguments @foo(|)
47    AnnotationArgs {
48        /// The annotation being used
49        annotation: String,
50    },
51    /// After "use " — suggest extension modules for namespace import
52    ImportModule,
53    /// After "from " — suggest importable modules for named import
54    FromModule,
55    /// After "from <module-prefix>." — suggest next namespace segments
56    FromModulePartial {
57        /// The prefix typed so far (e.g., "std.core", "mydep")
58        prefix: String,
59    },
60    /// Inside "from <module> use { " — suggest module exports
61    ImportItems {
62        /// The module being imported from
63        module: String,
64    },
65    /// After pipe operator `|>` — suggest functions/methods that accept the piped type
66    PipeTarget {
67        /// The inferred type of the expression before `|>`
68        pipe_input_type: Option<String>,
69    },
70    /// Inside an impl block body — suggest unimplemented trait methods
71    ImplBlock {
72        /// The trait name being implemented
73        trait_name: String,
74        /// The target type implementing the trait
75        target_type: String,
76        /// Methods already implemented in self impl block
77        existing_methods: Vec<String>,
78    },
79    /// Inside a type alias override: `type EUR = Currency { | }`
80    /// Suggests comptime field names from the base type
81    TypeAliasOverride {
82        /// The base type being aliased (e.g., "Currency")
83        base_type: String,
84    },
85    /// After `await join ` — suggest join strategies (all, race, any, settle)
86    JoinStrategy,
87    /// Inside a join block body — suggest labeled branch snippets
88    JoinBody {
89        /// The join strategy (all, race, any, settle)
90        strategy: String,
91    },
92    /// In a trait bound position: `fn foo<T: |>` — suggest trait names
93    TraitBound,
94    /// Inside a `comptime { }` block — suggest comptime builtins + normal expressions
95    ComptimeBlock,
96    /// After `@` in expression position — suggest annotations for expression-level decoration
97    ExprAnnotation,
98    /// After `@` inside a `///` doc comment.
99    DocTag {
100        /// The partial tag name typed so far.
101        prefix: String,
102    },
103    /// In a `@param` doc tag.
104    DocParamName {
105        /// The partial parameter name typed so far.
106        prefix: String,
107    },
108    /// In a `@typeparam` doc tag.
109    DocTypeParamName {
110        /// The partial type parameter name typed so far.
111        prefix: String,
112    },
113    /// In a `@see` or `@link` doc tag target.
114    DocLinkTarget {
115        /// The partial fully qualified symbol typed so far.
116        prefix: String,
117    },
118    /// Inside formatted interpolation spec after `expr:` in `f"{expr:...}"`.
119    InterpolationFormatSpec {
120        /// The currently typed prefix after `:`.
121        spec_prefix: String,
122    },
123}
124
125/// Detailed context about argument position
126#[derive(Debug, Clone, PartialEq)]
127pub enum ArgumentContext {
128    /// Cursor is on argument N of function call
129    FunctionArgument { function: String, arg_index: usize },
130    /// Inside object literal at property value position
131    ObjectLiteralValue {
132        containing_function: Option<String>,
133        property_name: String,
134    },
135    /// Inside object literal at property name position
136    ObjectLiteralPropertyName { containing_function: Option<String> },
137    /// Unknown/general argument context
138    General,
139}
140
141/// Analyze the text and position to determine completion context
142pub fn analyze_context(text: &str, position: Position) -> CompletionContext {
143    // Get the line where cursor is
144    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    // Get text before cursor on current line
153    let line_text_before_cursor = if char_pos <= current_line.len() {
154        &current_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                // Keep completion strict inside non-expression string content.
172                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        // Check if we're in an import statement
188        if let Some(import_ctx) = detect_import_context(&text_before_cursor) {
189            return import_ctx;
190        }
191    }
192
193    // Check if we're after a pipe operator `|>`
194    if !inside_interpolation {
195        if let Some(pipe_ctx) = detect_pipe_context(&text_before_cursor) {
196            return pipe_ctx;
197        }
198    }
199
200    // Check if we're after `await join ` — suggest join strategies
201    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    // Check if we're inside a join block body: `await join all { | }`
209    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    // Check if we're after a dot (property access)
218    // But NOT if the text after the dot contains '(' — that means
219    // we're inside a method call like `module.fn(`, which should be FunctionCall.
220    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    // Check if we're after "find" keyword (pattern reference)
233    if !inside_interpolation && text_before_cursor.trim_end().ends_with("find") {
234        return CompletionContext::PatternReference;
235    }
236
237    // Check if we're in a query context
238    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    // Check if we're inside a type alias override: `type EUR = Currency { | }`
249    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    // Check if we're inside an impl block
258    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    // Check if we're inside a comptime { } block
265    if !inside_interpolation && is_inside_comptime_block(text, position.line as usize) {
266        return CompletionContext::ComptimeBlock;
267    }
268
269    // Check if we're in a pattern body
270    // Look backwards through lines to see if we're inside a pattern definition
271    if !inside_interpolation && is_inside_pattern_body(text, position.line as usize) {
272        return CompletionContext::PatternBody;
273    }
274
275    // Check if we're in a trait bound position: `fn foo<T: |>`
276    if !inside_interpolation && is_in_trait_bound_position(&text_before_cursor) {
277        return CompletionContext::TraitBound;
278    }
279
280    // Check if we're in a type annotation position
281    if !inside_interpolation && is_in_type_annotation_position(&text_before_cursor) {
282        return CompletionContext::TypeAnnotation;
283    }
284
285    // Check if we're inside a function call
286    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    // Check if cursor is after "@" in expression position (not at statement start)
295    if !inside_interpolation && is_at_expr_annotation_position(&text_before_cursor) {
296        return CompletionContext::ExprAnnotation;
297    }
298
299    // Check if cursor is after "@" at statement start (item-level annotation)
300    if !inside_interpolation && is_at_annotation_position(&text_before_cursor) {
301        return CompletionContext::Annotation;
302    }
303
304    // Default to general context
305    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
356/// Returns true when cursor is inside an interpolation expression body,
357/// such as `f"{expr}"`, `f"${expr}"`, or `f#"{expr}"`.
358pub 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
780/// Detect if cursor is inside a join block body: `await join all { ... }`
781/// Returns JoinBody context with the strategy name if inside the block braces.
782fn 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    // Walk backwards from current line looking for unclosed `join <strategy> {`
791    let mut brace_depth: i32 = 0;
792    let mut i = current_line;
793    loop {
794        let line = lines.get(i)?;
795        // On cursor line, only count braces up to cursor position
796        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        // Found a net opening brace — check if self line has a join pattern
810        if brace_depth < 0 {
811            let trimmed = line.trim();
812            for strategy in &strategies {
813                // Match patterns like: `await join all {`, `join race {`
814                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
840/// Extract the object/expression before a dot
841fn extract_object_before_dot(text: &str) -> &str {
842    let trimmed = text.trim_end();
843
844    // Find the start of the identifier by looking backwards for whitespace or operators
845    // but we need to handle array indexing like data[0]
846    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
866/// Public API: check if cursor is inside a type alias override, returning the base type name
867pub 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
875/// Detect if cursor is inside a type alias override: `type EUR = Currency { | }`
876/// Returns the base type name (e.g., "Currency") if inside the override braces.
877/// `cursor_char` is the cursor's character offset on the current line, used to
878/// only count braces before the cursor when the override is on a single line.
879fn 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    // Walk backwards from current line to find `type X = Y {`
887    // This pattern is on a single line typically, but the braces content may span lines
888    let mut brace_depth: i32 = 0;
889    let mut i = current_line;
890    loop {
891        let line = lines.get(i)?;
892        // On the cursor line, only count braces up to the cursor position
893        // so that a closing brace after the cursor doesn't cancel the opening brace
894        let effective_line = if i == current_line {
895            let end = cursor_char.min(line.len());
896            &line[..end]
897        } else {
898            line
899        };
900        // Count braces in reverse order
901        for ch in effective_line.chars().rev() {
902            match ch {
903                '}' => brace_depth += 1,
904                '{' => brace_depth -= 1,
905                _ => {}
906            }
907        }
908        // If we found a net opening brace, check if self line has the type alias pattern
909        if brace_depth < 0 {
910            let trimmed = line.trim();
911            // Match pattern: `type NAME = BASE_TYPE {`
912            if trimmed.starts_with("type ") {
913                let rest = trimmed.strip_prefix("type ")?.trim();
914                // Skip the alias name
915                let after_name = rest
916                    .split(|c: char| c.is_whitespace() || c == '<')
917                    .next()
918                    .map(|name| &rest[name.len()..])?;
919                // Skip generic params if present
920                let after_generics = if after_name.trim_start().starts_with('<') {
921                    // Find closing '>'
922                    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                // Expect `= BASE_TYPE {`
942                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
962/// Detect if cursor is inside an impl block and return context with trait/type info
963fn 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        // Check for "impl TraitName for TypeName {" pattern
979        if trimmed.starts_with("impl ") && !in_impl {
980            // Parse: impl TraitName for TypeName {
981            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                // Extract type name (strip trailing `{` if present)
986                target_type = parts[2].trim_end_matches('{').trim().to_string();
987                in_impl = true;
988                existing_methods.clear();
989            }
990        }
991
992        // Collect method names inside the impl block
993        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
1027/// Check if cursor is inside a pattern body
1028fn 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
1054/// Extract function name if cursor is inside a function call.
1055/// Returns the full qualified name including module prefix (e.g., "csv.load").
1056fn extract_function_call(text: &str) -> Option<String> {
1057    // Find the last opening parenthesis
1058    if let Some(paren_pos) = text.rfind('(') {
1059        let before_paren = text[..paren_pos].trim_end();
1060
1061        // Find the start of the expression (go backwards through identifier chars and dots)
1062        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
1075/// Analyze detailed argument context (position, object literals, etc.)
1076fn analyze_argument_context(text_before_cursor: &str, function: &str) -> ArgumentContext {
1077    // Find the opening paren of the function call
1078    if let Some(paren_pos) = text_before_cursor.rfind('(') {
1079        let params_text = &text_before_cursor[paren_pos + 1..];
1080
1081        // Check if we're inside an object literal
1082        if is_inside_object_literal(params_text) {
1083            // Try to extract property name
1084            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        // Count commas to determine argument index
1097        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
1108/// Check if cursor is inside an object literal
1109fn 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
1115/// Extract property name from object literal context
1116fn extract_property_name(text: &str) -> Option<String> {
1117    // Find last '{' or ','
1118    let start = text.rfind(['{', ',']).map(|i| i + 1).unwrap_or(0);
1119    let fragment = text[start..].trim();
1120
1121    // Check if there's a ':' (we're past property name, at value position)
1122    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
1132/// Count commas outside of nested parens/braces to determine argument position
1133fn 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
1166/// Check if cursor is in a type annotation position
1167/// Returns true when:
1168/// - Cursor is right after a colon in a variable/parameter declaration
1169/// - Cursor is after a colon and user has started typing a type name
1170/// - Cursor is after a return type arrow "->"
1171fn is_in_type_annotation_position(text: &str) -> bool {
1172    let trimmed = text.trim_end();
1173
1174    // Case 1: Right after colon
1175    if trimmed.ends_with(':') {
1176        return true;
1177    }
1178
1179    // Case 2: After colon in variable/param declaration
1180    // Look for pattern: "let/const name:" or "(param:" with no "=" after
1181    if let Some(colon_idx) = find_unquoted_colon(trimmed) {
1182        let after_colon = &trimmed[colon_idx + 1..];
1183        // Not in type position if we've passed the type (hit '=' or '{')
1184        if after_colon.contains('=') || after_colon.contains('{') {
1185            return false;
1186        }
1187        let before_colon = &trimmed[..colon_idx];
1188        // Check for variable declaration pattern or parameter context
1189        if is_var_decl_context(before_colon) || is_param_context(before_colon) {
1190            return true;
1191        }
1192    }
1193
1194    // Case 3: After return type arrow "->"
1195    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
1205/// Find the last unquoted colon in the text
1206fn 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
1221/// Check if the text before colon indicates a variable declaration context
1222fn is_var_decl_context(before_colon: &str) -> bool {
1223    let trimmed = before_colon.trim();
1224    // Match "let x" or "const x" patterns
1225    trimmed.starts_with("let ") || trimmed.starts_with("const ")
1226}
1227
1228/// Check if we're in a parameter context (inside unclosed parentheses)
1229fn is_param_context(before_colon: &str) -> bool {
1230    // We're in param context if there's an unclosed "("
1231    let open = before_colon.matches('(').count();
1232    let close = before_colon.matches(')').count();
1233    open > close
1234}
1235
1236/// Detect import statement contexts:
1237///   "use "                 → ImportModule (extension modules for namespace import)
1238///   "from "                → FromModule (importable modules for named import)
1239///   "from std."            → FromModulePartial { prefix: "std" }
1240///   "from mydep.tools."    → FromModulePartial { prefix: "mydep.tools" }
1241///   "from std::core::csv use {"  → ImportItems { module: "std::core::csv" }
1242fn detect_import_context(text_before_cursor: &str) -> Option<CompletionContext> {
1243    let trimmed = text_before_cursor.trim();
1244
1245    // "from <module> use { <TAB>" or "from <module> use { a, <TAB>"
1246    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        // Check for partial module path with dots: "from std." or "from mydep.tools."
1254        let module_text = rest.trim();
1255        if module_text.contains('.') {
1256            // Extract prefix: "mydep.tools." → "mydep.tools", "mydep.tools.sub" → "mydep.tools"
1257            // If ends with dot, prefix is everything before the trailing dot
1258            // If not, prefix is everything up to and including the last dot segment minus the partial
1259            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        // "from <TAB>" — suggest importable modules
1269        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    // Bare keywords
1280    if trimmed == "use" {
1281        return Some(CompletionContext::ImportModule);
1282    }
1283    if trimmed == "from" {
1284        return Some(CompletionContext::FromModule);
1285    }
1286
1287    None
1288}
1289
1290/// Detect if cursor is after a pipe operator `|>`.
1291/// Returns PipeTarget context with the inferred input type (placeholder; type
1292/// is resolved later using the type_context in the completion handler).
1293fn detect_pipe_context(text_before_cursor: &str) -> Option<CompletionContext> {
1294    let trimmed = text_before_cursor.trim_end();
1295
1296    // Check for `|> ` or `|>` at end — the cursor is right after the pipe
1297    if trimmed.ends_with("|>") {
1298        return Some(CompletionContext::PipeTarget {
1299            pipe_input_type: None,
1300        });
1301    }
1302
1303    // Check for `|> partial_ident` — user is typing after pipe
1304    // Find last `|>` and check nothing complex follows (no dots, parens, etc.)
1305    if let Some(pipe_pos) = trimmed.rfind("|>") {
1306        let after_pipe = trimmed[pipe_pos + 2..].trim();
1307        // If what follows is just an identifier prefix (no dots, parens, braces)
1308        // then we're still in pipe target context
1309        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
1319/// Check if cursor is at a position where annotations can be written
1320fn is_at_annotation_position(text: &str) -> bool {
1321    let trimmed = text.trim_end();
1322
1323    // Check if we just typed "@"
1324    if trimmed.ends_with('@') {
1325        // Check if it's at the start of a line or after whitespace (valid annotation position)
1326        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
1333/// Check if cursor is in a trait bound position inside angle brackets.
1334/// Returns true for patterns like: `fn foo<T: |>`, `fn foo<T: Comparable + |>`, `trait Foo<T: |>`
1335fn is_in_trait_bound_position(text: &str) -> bool {
1336    let trimmed = text.trim_end();
1337
1338    // Must be inside unclosed `<` brackets (angle_depth > 0)
1339    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    // Extract text inside the last unclosed `<`
1356    let inside_angles = if let Some(start) = last_angle_open {
1357        &trimmed[start + 1..]
1358    } else {
1359        return false;
1360    };
1361
1362    // Look for a colon after a type param name: `T:` or `T: Comp + `
1363    // The last segment (after last comma) should contain a ':'
1364    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        // Before colon should be a simple identifier (type param name)
1373        if !before_colon.is_empty()
1374            && before_colon
1375                .chars()
1376                .all(|c| c.is_alphanumeric() || c == '_')
1377        {
1378            // Check that the text before `<` starts with fn, function, trait, or type
1379            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
1401/// Check if the cursor is inside a `comptime { }` block.
1402///
1403/// Scans backwards through lines to find an unmatched `comptime {` opening.
1404fn 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        // Check for `comptime {` pattern (item-level or expression-level)
1417        if (trimmed.starts_with("comptime {")
1418            || trimmed.starts_with("comptime{")
1419            || trimmed == "comptime")
1420            && !in_comptime
1421        {
1422            in_comptime = true;
1423        }
1424        // Also check for `= comptime {` or `let x = comptime {` (expression position)
1425        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
1440/// Check if cursor is after `@` in expression position (not at statement start).
1441///
1442/// Expression-level annotations: `let x = @timeout(5s) fetch()`
1443/// vs item-level annotations: `@strategy\nfn foo() { }`
1444fn 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    // Get text before the `@`
1452    let before_at = trimmed.trim_end_matches('@').trim_end();
1453
1454    // If it's at the start of a line (empty before_at or ends with newline),
1455    // that's an item-level annotation, not expression-level.
1456    if before_at.is_empty() || before_at.ends_with('\n') {
1457        return false;
1458    }
1459
1460    // Expression-level: after `=`, `(`, `,`, `return`, `=>`, or other expression starters
1461    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        // User has typed "let series: S" - should still be in type annotation context
1682        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        // User has typed "let table: Table" - should still be in type annotation context
1695        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        // After "=", we're no longer in type annotation context
1708        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        // Function parameter with type annotation
1721        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        // After return type arrow
1734        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        // Typing after return type arrow
1747        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        // The deprecated `from X import { }` syntax is removed;
1784        // LSP should fall back to FromModule context
1785        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        // "use ml" should still be ImportModule, not General
1815        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        // `a | b` should NOT be PipeTarget — that's bitwise OR, not pipe
1898        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        // User is typing an identifier after pipe: `data |> fi`
1915        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        // Cursor inside empty interpolation: f"hello {|}"
1932        let text = r#"let s = f"hello {}""#;
1933        let cursor = text.find("{}").unwrap() as u32 + 1; // after {
1934        let context = analyze_context(
1935            text,
1936            Position {
1937                line: 0,
1938                character: cursor,
1939            },
1940        );
1941        // Empty interpolation should still offer general completions (not literal text)
1942        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        // Cursor typing identifier in interpolation: f"hello {x|}"
1952        let text = r#"let s = f"hello {x}""#;
1953        let cursor = text.find("{x").unwrap() as u32 + 2; // after x
1954        let context = analyze_context(
1955            text,
1956            Position {
1957                line: 0,
1958                character: cursor,
1959            },
1960        );
1961        // Should be General context (variable name completion)
1962        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        // Cursor inside method call in interpolation: f"val: {obj.method(|)}"
1973        let text = r#"let s = f"val: {obj.method()}""#;
1974        let cursor = text.find("method(").unwrap() as u32 + 7; // after (
1975        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; // right after ':'
2001        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        // After closing brace, should NOT be in impl block
2079        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        // A normal struct type definition should NOT trigger TypeAliasOverride
2111        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        // `a < b` should NOT be trait bound
2210        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        // After comma in expression context, `@` should trigger ExprAnnotation
2299        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        // `@` at start of line is item-level, not expression-level
2311        let text = "@";
2312        let position = Position {
2313            line: 0,
2314            character: 1,
2315        };
2316        let context = analyze_context(text, position);
2317        // Should be Annotation (item-level), not ExprAnnotation
2318        assert!(
2319            !matches!(context, CompletionContext::ExprAnnotation),
2320            "Item-level @ should not be ExprAnnotation, got {:?}",
2321            context
2322        );
2323    }
2324}