Skip to main content

typr_cli/
lsp_parser.rs

1//! Token resolution and Markdown-highlighted type display for LSP hover.
2//!
3//! Given the full source text and a cursor position (line, character),
4//! this module:
5//!   1. Identifies the word (identifier / literal) under the cursor.
6//!   2. Parses and type-checks the whole document using the project's
7//!      pipeline (`parse` → `typing`) to build a fully-populated `Context`.
8//!   3. Looks up the identifier in that context and returns its type.
9//!   4. Renders the type string with Markdown syntax highlighting.
10
11use crate::metaprogramming::metaprogrammation;
12use nom_locate::LocatedSpan;
13use std::path::Path;
14use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, Position, Range};
15use typr_core::components::context::config::Environment;
16use typr_core::components::context::Context;
17use typr_core::components::language::var::Var;
18use typr_core::components::language::Lang;
19use typr_core::components::r#type::type_system::TypeSystem;
20use typr_core::components::r#type::Type;
21use typr_core::processes::parsing::parse;
22use typr_core::typing;
23use typr_core::utils::builder;
24
25type Span<'a> = LocatedSpan<&'a str, String>;
26
27/// A resolved hover result: the Markdown-highlighted type and the LSP range.
28#[derive(Debug, Clone)]
29pub struct HoverInfo {
30    /// Markdown string ready to be sent as hover contents.
31    pub type_display: String,
32    /// The source range of the token that was resolved.
33    pub range: Range,
34}
35
36/// A resolved definition result: the location where a symbol is defined.
37#[derive(Debug, Clone)]
38pub struct DefinitionInfo {
39    /// The source range where the symbol is defined.
40    pub range: Range,
41    /// The file path where the symbol is defined (None if same file or unknown).
42    pub file_path: Option<String>,
43}
44
45// ── public entry-point ─────────────────────────────────────────────────────
46
47/// Main entry-point called by the LSP hover handler.
48///
49/// Returns `None` when:
50///   - the cursor is not on an identifier/literal, or
51///   - parsing or type-checking fails (e.g. incomplete code).
52pub fn find_type_at(content: &str, line: u32, character: u32) -> Option<HoverInfo> {
53    // 1. Extract the word under the cursor.
54    let (word, word_range) = extract_word_at(content, line, character)?;
55
56    // 2. Parse the whole document.
57    let span: Span = LocatedSpan::new_extra(content, String::new());
58    let parse_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(span)));
59    let ast = parse_result.ok()?.ast;
60
61    // 3. Type-check the whole document to build the context.
62    let context = Context::default();
63    let type_context =
64        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| typing(&context, &ast)));
65    let type_context = type_context.ok()?;
66    let final_context = type_context.context;
67
68    // 4. Look up the word in the context.
69    let types = final_context.get_types_from_name(&word);
70
71    let typ = if types.is_empty() {
72        // Fallback: try to infer the type of the word as a literal.
73        infer_literal_type(&word)?
74    } else {
75        // Pick the most specific (last) type when there are multiple overloads.
76        types.last().unwrap().clone()
77    };
78
79    // 5. Render with Markdown highlighting.
80    let highlighted = highlight_type(&typ.pretty());
81    let markdown = format!(
82        "**`{}`** : {}\n\n```\n{}\n```",
83        word,         // variable name in bold code
84        highlighted,  // inline Markdown-highlighted type
85        typ.pretty()  // plain code-block fallback (always readable)
86    );
87
88    Some(HoverInfo {
89        type_display: markdown,
90        range: word_range,
91    })
92}
93
94// ── definition lookup ──────────────────────────────────────────────────────
95
96/// Detect the environment (Project or StandAlone) by looking for DESCRIPTION
97/// and NAMESPACE files in parent directories.
98fn detect_environment(file_path: &str) -> Environment {
99    let path = Path::new(file_path);
100    let mut dir = path.parent();
101
102    while let Some(d) = dir {
103        let description = d.join("DESCRIPTION");
104        let namespace = d.join("NAMESPACE");
105        if description.exists() && namespace.exists() {
106            return Environment::Project;
107        }
108        dir = d.parent();
109    }
110
111    Environment::StandAlone
112}
113
114/// Main entry-point called by the LSP goto_definition handler.
115pub fn find_definition_at(
116    content: &str,
117    line: u32,
118    character: u32,
119    file_path: &str,
120) -> Option<DefinitionInfo> {
121    // 1. Extract the word under the cursor.
122    let (word, _word_range) = extract_word_at(content, line, character)?;
123
124    // 2. Parse the whole document with the file path.
125    let span: Span = LocatedSpan::new_extra(content, file_path.to_string());
126    let parse_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(span)));
127    let ast = parse_result.ok()?.ast;
128
129    // 3. Detect environment and apply metaprogramming to resolve module imports.
130    let environment = detect_environment(file_path);
131    let ast = metaprogrammation(ast, environment);
132
133    // 4. Type-check the whole document to build the context.
134    let context = Context::default();
135    let type_context =
136        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| typing(&context, &ast)));
137    let type_context = type_context.ok()?;
138    let final_context = type_context.context;
139
140    // 5. Look up the variable in the context to find its definition.
141    let definition_var = final_context
142        .variables()
143        .find(|(var, _)| var.get_name() == word)
144        .map(|(var, _)| var.clone());
145
146    let definition_var = definition_var.or_else(|| {
147        final_context
148            .aliases()
149            .find(|(var, _)| var.get_name() == word)
150            .map(|(var, _)| var.clone())
151    });
152
153    let var = definition_var?;
154    let help_data = var.get_help_data();
155    let offset = help_data.get_offset();
156    let definition_file = help_data.get_file_name();
157
158    // 6. Determine if the definition is in a different file.
159    let (source_content, file_path_result) =
160        if definition_file.is_empty() || definition_file == file_path {
161            (content.to_string(), None)
162        } else {
163            match std::fs::read_to_string(&definition_file) {
164                Ok(external_content) => (external_content, Some(definition_file)),
165                Err(_) => (content.to_string(), None),
166            }
167        };
168
169    // 7. Convert offset to Position using the correct file content.
170    let pos = offset_to_position(offset, &source_content);
171    let end_col = pos.character + word.len() as u32;
172
173    Some(DefinitionInfo {
174        range: Range::new(pos, Position::new(pos.line, end_col)),
175        file_path: file_path_result,
176    })
177}
178
179/// Convert a character offset to a Position (line, column).
180fn offset_to_position(offset: usize, content: &str) -> Position {
181    let mut line = 0u32;
182    let mut col = 0u32;
183
184    for (i, ch) in content.chars().enumerate() {
185        if i >= offset {
186            break;
187        }
188        if ch == '\n' {
189            line += 1;
190            col = 0;
191        } else {
192            col += 1;
193        }
194    }
195
196    Position::new(line, col)
197}
198
199// ── word extraction ────────────────────────────────────────────────────────
200
201/// Extract the contiguous word (identifier or numeric literal) that contains
202/// the given cursor position.
203fn extract_word_at(content: &str, line: u32, character: u32) -> Option<(String, Range)> {
204    let source_line = content.lines().nth(line as usize)?;
205
206    if (character as usize) > source_line.len() {
207        return None;
208    }
209
210    let bytes = source_line.as_bytes();
211    let col = character as usize;
212
213    if col >= bytes.len() || !is_word_char(bytes[col]) {
214        if col == 0 {
215            return None;
216        }
217        if !is_word_char(bytes[col - 1]) {
218            return None;
219        }
220    }
221
222    let anchor = if col < bytes.len() && is_word_char(bytes[col]) {
223        col
224    } else {
225        col - 1
226    };
227
228    let start = {
229        let mut i = anchor;
230        while i > 0 && is_word_char(bytes[i - 1]) {
231            i -= 1;
232        }
233        i
234    };
235
236    let end = {
237        let mut i = anchor;
238        while i + 1 < bytes.len() && is_word_char(bytes[i + 1]) {
239            i += 1;
240        }
241        i + 1
242    };
243
244    let word = &source_line[start..end];
245    if word.is_empty() {
246        return None;
247    }
248
249    Some((
250        word.to_string(),
251        Range {
252            start: Position::new(line, start as u32),
253            end: Position::new(line, end as u32),
254        },
255    ))
256}
257
258/// A character is part of a word if it is alphanumeric, an underscore, or a dot.
259fn is_word_char(b: u8) -> bool {
260    b.is_ascii_alphanumeric() || b == b'_' || b == b'.'
261}
262
263// ── literal fallback ───────────────────────────────────────────────────────
264
265/// For numeric literals that are not in the context, return their literal type.
266fn infer_literal_type(word: &str) -> Option<Type> {
267    if let Ok(i) = word.parse::<i32>() {
268        return Some(builder::integer_type(i));
269    }
270    if let Ok(_f) = word.parse::<f32>() {
271        return Some(builder::number_type());
272    }
273    None
274}
275
276// ── Markdown type highlighter ──────────────────────────────────────────────
277
278/// Primitive type names that should be rendered in **bold**.
279const PRIMITIVE_TYPES: &[&str] = &["int", "num", "bool", "char", "any", "Empty"];
280
281/// Keywords rendered in ***bold italic***.
282const TYPE_KEYWORDS: &[&str] = &["fn", "Module", "interface", "class"];
283
284/// Highlight a TypR type string into inline Markdown.
285pub fn highlight_type(type_str: &str) -> String {
286    let mut out = String::with_capacity(type_str.len() * 2);
287    let chars: Vec<char> = type_str.chars().collect();
288    let len = chars.len();
289    let mut i = 0;
290
291    while i < len {
292        let ch = chars[i];
293
294        // ── generic prefixes: #identifier or %identifier ──────────────
295        if (ch == '#' || ch == '%')
296            && i + 1 < len
297            && (chars[i + 1].is_alphanumeric() || chars[i + 1] == '_')
298        {
299            let start = i;
300            i += 1;
301            while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
302                i += 1;
303            }
304            let token: String = chars[start..i].iter().collect();
305            out.push_str(&format!("*{}*", token));
306            continue;
307        }
308
309        // ── char-literal string values ─────────────────────────────────
310        if ch == '"' || ch == '\'' {
311            let delim = ch;
312            let start = i;
313            i += 1;
314            while i < len && chars[i] != delim {
315                i += 1;
316            }
317            if i < len {
318                i += 1;
319            }
320            let token: String = chars[start..i].iter().collect();
321            out.push_str(&format!("`{}`", token));
322            continue;
323        }
324
325        // ── word token ──────────────────────────────────────────────────
326        if ch.is_alphabetic() || ch == '_' {
327            let start = i;
328            while i < len && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.') {
329                i += 1;
330            }
331            let word: String = chars[start..i].iter().collect();
332            out.push_str(&colorize_word(&word));
333            continue;
334        }
335
336        // ── numeric literal ─────────────────────────────────────────────
337        if ch.is_ascii_digit() {
338            let start = i;
339            while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
340                i += 1;
341            }
342            let token: String = chars[start..i].iter().collect();
343            out.push_str(&format!("*{}*", token));
344            continue;
345        }
346
347        // ── tag dot-prefix: .TagName ────────────────────────────────────
348        if ch == '.' && i + 1 < len && chars[i + 1].is_alphabetic() {
349            out.push('.');
350            i += 1;
351            let start = i;
352            while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
353                i += 1;
354            }
355            let word: String = chars[start..i].iter().collect();
356            out.push_str(&format!("**{}**", word));
357            continue;
358        }
359
360        // ── arrow operator `->` ─────────────────────────────────────────
361        if ch == '-' && i + 1 < len && chars[i + 1] == '>' {
362            out.push_str(" → ");
363            i += 2;
364            continue;
365        }
366
367        // ── everything else ─────────────────────────────────────────────
368        out.push(ch);
369        i += 1;
370    }
371
372    out
373}
374
375/// Classify a word and wrap it in the appropriate Markdown formatting.
376fn colorize_word(word: &str) -> String {
377    if TYPE_KEYWORDS.contains(&word) {
378        format!("***{}***", word)
379    } else if PRIMITIVE_TYPES.contains(&word) {
380        format!("**{}**", word)
381    } else if is_generic_name(word) {
382        format!("*{}*", word)
383    } else if word.chars().next().map_or(false, |c| c.is_uppercase()) {
384        format!("**{}**", word)
385    } else {
386        word.to_string()
387    }
388}
389
390/// A generic name is a single uppercase ASCII letter, optionally followed by digits.
391fn is_generic_name(word: &str) -> bool {
392    let mut chars = word.chars();
393    match chars.next() {
394        Some(c) if c.is_ascii_uppercase() => chars.all(|c| c.is_ascii_digit()),
395        _ => false,
396    }
397}
398
399// ══════════════════════════════════════════════════════════════════════════
400// ── AUTOCOMPLETION ────────────────────────────────────────────────────────
401// ══════════════════════════════════════════════════════════════════════════
402
403/// Main entry point for LSP completion requests.
404pub fn get_completions_at(content: &str, line: u32, character: u32) -> Vec<CompletionItem> {
405    // 1. Parse + type-check the document WITHOUT the cursor line
406    let final_context = match parse_document_without_cursor_line(content, line) {
407        Some(ctx) => ctx,
408        None => {
409            let span: Span = LocatedSpan::new_extra(content, String::new());
410            let parse_result =
411                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(span)));
412            let context = Context::default();
413            match parse_result {
414                Ok(result) => {
415                    let ast = result.ast;
416                    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
417                        typing(&context, &ast)
418                    })) {
419                        Ok(tc) => tc.context,
420                        Err(_) => return get_fallback_completions(),
421                    }
422                }
423                Err(_) => return get_fallback_completions(),
424            }
425        }
426    };
427
428    // 2. Extract multi-line prefix up to the cursor.
429    let prefix = extract_multiline_prefix(content, line, character);
430
431    // 3. Detect the completion context.
432    let ctx = detect_completion_context(&prefix);
433
434    // 4. Generate completions based on context.
435    match ctx {
436        CompletionCtx::Type => get_type_completions(&final_context),
437        CompletionCtx::Module(name) => get_module_completions(&final_context, &name),
438        CompletionCtx::Pipe(expr) => get_pipe_completions(&final_context, &expr),
439        CompletionCtx::RecordField(expr) => get_record_field_completions(&final_context, &expr),
440        CompletionCtx::DotAccess(expr) => get_dot_completions(&final_context, &expr),
441        CompletionCtx::Expression => get_expression_completions(&final_context),
442    }
443}
444
445/// Parse the document excluding the line containing the cursor.
446fn parse_document_without_cursor_line(content: &str, cursor_line: u32) -> Option<Context> {
447    let lines: Vec<&str> = content.lines().collect();
448
449    let mut filtered_lines = Vec::new();
450    for (idx, line) in lines.iter().enumerate() {
451        if idx != cursor_line as usize {
452            filtered_lines.push(*line);
453        }
454    }
455
456    let filtered_content = filtered_lines.join("\n");
457    let span: Span = LocatedSpan::new_extra(&filtered_content, String::new());
458
459    let parse_result =
460        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(span))).ok()?;
461    let ast = parse_result.ast;
462
463    let context = Context::default();
464
465    let final_context = if let Lang::Lines(exprs, _) = &ast {
466        let mut ctx = context.clone();
467        for expr in exprs {
468            if let Ok(tc) =
469                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| typing(&ctx, expr)))
470            {
471                ctx = tc.context;
472            }
473        }
474        ctx
475    } else {
476        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| typing(&context, &ast)))
477            .ok()?
478            .context
479    };
480
481    Some(final_context)
482}
483
484// ── Context detection ──────────────────────────────────────────────────────
485
486#[derive(Debug, Clone)]
487enum CompletionCtx {
488    Type,
489    Module(String),
490    Pipe(String),
491    RecordField(String),
492    DotAccess(String),
493    Expression,
494}
495
496fn extract_multiline_prefix(content: &str, line: u32, character: u32) -> String {
497    let lines: Vec<&str> = content.lines().collect();
498    let current_line_idx = line as usize;
499
500    if current_line_idx >= lines.len() {
501        return String::new();
502    }
503
504    let current_line_part = lines[current_line_idx]
505        .get(..character as usize)
506        .unwrap_or("");
507
508    let lookback_lines = 10;
509    let start_line = current_line_idx.saturating_sub(lookback_lines);
510
511    let mut context_lines = Vec::new();
512    for i in start_line..current_line_idx {
513        context_lines.push(lines[i]);
514    }
515    context_lines.push(current_line_part);
516
517    context_lines.join("\n")
518}
519
520fn detect_completion_context(prefix: &str) -> CompletionCtx {
521    let trimmed = prefix.trim_end();
522
523    if trimmed.ends_with("|>") {
524        let before_pipe = trimmed[..trimmed.len() - 2].trim();
525        return CompletionCtx::Pipe(extract_expression_before(before_pipe));
526    }
527
528    if let Some(dollar_pos) = trimmed.rfind('$') {
529        let after_dollar = &trimmed[dollar_pos + 1..];
530        if after_dollar.is_empty() || after_dollar.chars().all(|c| c.is_whitespace()) {
531            let before_dollar = trimmed[..dollar_pos].trim_end();
532            if !before_dollar.is_empty() {
533                let expr = extract_last_expression(before_dollar);
534                return CompletionCtx::RecordField(expr);
535            }
536        }
537    }
538
539    if let Some(dot_pos) = trimmed.rfind('.') {
540        let after_dot = &trimmed[dot_pos + 1..];
541        if after_dot.is_empty() || after_dot.chars().all(|c| c.is_whitespace()) {
542            let before_dot = trimmed[..dot_pos].trim_end();
543            if !before_dot.is_empty() {
544                let expr = extract_last_expression(before_dot);
545
546                if expr.chars().next().map_or(false, |c| c.is_uppercase()) {
547                    return CompletionCtx::Module(expr);
548                } else {
549                    return CompletionCtx::DotAccess(expr);
550                }
551            }
552        }
553    }
554
555    if let Some(colon_pos) = trimmed.rfind(':') {
556        let after_colon = &trimmed[colon_pos + 1..];
557        if !after_colon.contains('=') && !after_colon.contains(';') {
558            return CompletionCtx::Type;
559        }
560    }
561
562    if trimmed.trim_start().starts_with("type ") && trimmed.contains('=') {
563        return CompletionCtx::Type;
564    }
565
566    CompletionCtx::Expression
567}
568
569fn extract_last_expression(s: &str) -> String {
570    let trimmed = s.trim_end();
571
572    let parts: Vec<&str> = trimmed
573        .split(|c| c == ';' || c == '\n')
574        .filter(|p| !p.trim().is_empty())
575        .collect();
576
577    let last_statement = parts.last().unwrap_or(&"").trim();
578
579    if last_statement.is_empty() {
580        return String::new();
581    }
582
583    let mut depth_paren = 0;
584    let mut depth_bracket = 0;
585    let mut depth_brace = 0;
586    let mut start = 0;
587
588    for (i, ch) in last_statement.char_indices().rev() {
589        match ch {
590            ')' => depth_paren += 1,
591            '(' => {
592                depth_paren -= 1;
593                if depth_paren < 0 {
594                    start = i + 1;
595                    break;
596                }
597            }
598            ']' => depth_bracket += 1,
599            '[' => {
600                depth_bracket -= 1;
601                if depth_bracket < 0 {
602                    start = i + 1;
603                    break;
604                }
605            }
606            '}' => depth_brace += 1,
607            '{' => {
608                depth_brace -= 1;
609                if depth_brace < 0 {
610                    start = i + 1;
611                    break;
612                }
613            }
614            ',' if depth_paren == 0 && depth_bracket == 0 && depth_brace == 0 => {
615                start = i + 1;
616                break;
617            }
618            '<' | '>' if depth_paren == 0 && depth_bracket == 0 && depth_brace == 0 => {
619                if i > 0 && last_statement.as_bytes().get(i - 1) == Some(&b'-') {
620                    continue;
621                }
622                start = i + 1;
623                break;
624            }
625            _ => {}
626        }
627    }
628
629    last_statement[start..].trim().to_string()
630}
631
632fn extract_expression_before(s: &str) -> String {
633    let trimmed = s.trim_end();
634
635    let mut depth = 0;
636    let mut start = trimmed.len();
637
638    for (i, ch) in trimmed.char_indices().rev() {
639        match ch {
640            ')' | ']' | '}' => depth += 1,
641            '(' | '[' | '{' => {
642                depth -= 1;
643                if depth < 0 {
644                    start = i + 1;
645                    break;
646                }
647            }
648            ';' | ',' if depth == 0 => {
649                start = i + 1;
650                break;
651            }
652            _ => {}
653        }
654    }
655
656    trimmed[start..].trim().to_string()
657}
658
659// ── Completion generators ──────────────────────────────────────────────────
660
661fn get_type_completions(context: &Context) -> Vec<CompletionItem> {
662    let mut items = Vec::new();
663
664    let primitives = [
665        ("int", builder::integer_type_default()),
666        ("num", builder::number_type()),
667        ("bool", builder::boolean_type()),
668        ("char", builder::character_type_default()),
669        ("any", builder::any_type()),
670    ];
671
672    for (name, typ) in &primitives {
673        items.push(CompletionItem {
674            label: name.to_string(),
675            insert_text: Some(format!(" {}", name)),
676            kind: Some(CompletionItemKind::KEYWORD),
677            detail: Some(typ.pretty()),
678            ..Default::default()
679        });
680    }
681
682    for (var, typ) in context.aliases() {
683        if var.is_alias() {
684            items.push(CompletionItem {
685                label: var.get_name(),
686                insert_text: Some(format!(" {}", var.get_name())),
687                kind: Some(CompletionItemKind::INTERFACE),
688                detail: Some(typ.pretty()),
689                ..Default::default()
690            });
691        }
692    }
693
694    for (var, typ) in context.module_aliases() {
695        items.push(CompletionItem {
696            label: var.get_name(),
697            insert_text: Some(format!(" {}", var.get_name())),
698            kind: Some(CompletionItemKind::INTERFACE),
699            detail: Some(typ.pretty()),
700            ..Default::default()
701        });
702    }
703
704    items
705}
706
707fn get_module_completions(context: &Context, module_name: &str) -> Vec<CompletionItem> {
708    let module_context = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
709        context.extract_module_as_vartype(module_name)
710    }));
711
712    let module_ctx = match module_context {
713        Ok(ctx) => ctx,
714        Err(_) => return Vec::new(),
715    };
716
717    let mut items = Vec::new();
718
719    for (var, typ) in module_ctx.variables() {
720        let kind = if typ.is_function() {
721            CompletionItemKind::FUNCTION
722        } else {
723            CompletionItemKind::VARIABLE
724        };
725        items.push(var_to_completion_item(var, typ, kind));
726    }
727
728    for (var, typ) in module_ctx.aliases() {
729        items.push(var_to_completion_item(
730            var,
731            typ,
732            CompletionItemKind::INTERFACE,
733        ));
734    }
735
736    items
737}
738
739fn get_pipe_completions(context: &Context, expr: &str) -> Vec<CompletionItem> {
740    let expr_type = infer_expression_type(context, expr);
741
742    let mut items = Vec::new();
743
744    let all_functions: Vec<_> = context
745        .get_all_generic_functions()
746        .into_iter()
747        .chain(
748            context
749                .typing_context
750                .standard_library()
751                .into_iter()
752                .filter(|(_, typ)| typ.is_function())
753                .map(|(v, t)| (v.clone(), t.clone())),
754        )
755        .collect();
756
757    for (var, typ) in all_functions {
758        if let Some(first_param_type) = get_first_parameter_type(&typ) {
759            if expr_type.is_subtype(&first_param_type, context).0 {
760                items.push(CompletionItem {
761                    label: var.get_name(),
762                    insert_text: Some(format!(" {}", var.get_name())),
763                    kind: Some(CompletionItemKind::FUNCTION),
764                    detail: Some(typ.pretty()),
765                    ..Default::default()
766                });
767            }
768        }
769    }
770
771    items
772}
773
774fn get_record_field_completions(context: &Context, expr: &str) -> Vec<CompletionItem> {
775    let record_type = infer_expression_type(context, expr);
776
777    match record_type {
778        Type::Record(fields, _) => fields
779            .iter()
780            .map(|arg_type| CompletionItem {
781                label: arg_type.get_argument_str(),
782                kind: Some(CompletionItemKind::FIELD),
783                detail: Some(arg_type.get_type().pretty()),
784                ..Default::default()
785            })
786            .collect(),
787        _ => Vec::new(),
788    }
789}
790
791fn get_dot_completions(context: &Context, expr: &str) -> Vec<CompletionItem> {
792    let mut items = Vec::new();
793
794    let expr_type = infer_expression_type(context, expr);
795
796    if let Type::Record(fields, _) = &expr_type {
797        for arg_type in fields {
798            items.push(CompletionItem {
799                label: arg_type.get_argument_str(),
800                kind: Some(CompletionItemKind::FIELD),
801                detail: Some(arg_type.get_type().pretty()),
802                ..Default::default()
803            });
804        }
805    }
806
807    let all_functions: Vec<_> = context
808        .get_all_generic_functions()
809        .into_iter()
810        .chain(
811            context
812                .typing_context
813                .standard_library()
814                .into_iter()
815                .filter(|(_, typ)| typ.is_function())
816                .map(|(v, t)| (v.clone(), t.clone())),
817        )
818        .collect();
819
820    for (var, typ) in all_functions {
821        if let Some(first_param_type) = get_first_parameter_type(&typ) {
822            if expr_type.is_subtype(&first_param_type, context).0 {
823                items.push(var_to_completion_item(
824                    &var,
825                    &typ,
826                    CompletionItemKind::FUNCTION,
827                ));
828            }
829        }
830    }
831
832    items
833}
834
835fn get_expression_completions(context: &Context) -> Vec<CompletionItem> {
836    let mut items = Vec::new();
837
838    for (var, typ) in context.variables() {
839        if !typ.is_function() && !var.is_alias() {
840            items.push(var_to_completion_item(
841                var,
842                typ,
843                CompletionItemKind::VARIABLE,
844            ));
845        }
846    }
847
848    for (var, typ) in context.get_all_generic_functions() {
849        items.push(var_to_completion_item(
850            &var,
851            &typ,
852            CompletionItemKind::FUNCTION,
853        ));
854    }
855
856    for (var, typ) in &context.typing_context.standard_library() {
857        if typ.is_function() {
858            items.push(var_to_completion_item(
859                var,
860                typ,
861                CompletionItemKind::FUNCTION,
862            ));
863        }
864    }
865
866    items
867}
868
869fn get_fallback_completions() -> Vec<CompletionItem> {
870    let mut items = Vec::new();
871
872    let primitives = [
873        ("int", builder::integer_type_default()),
874        ("num", builder::number_type()),
875        ("bool", builder::boolean_type()),
876        ("char", builder::character_type_default()),
877        ("any", builder::any_type()),
878    ];
879
880    for (name, typ) in &primitives {
881        items.push(CompletionItem {
882            label: name.to_string(),
883            kind: Some(CompletionItemKind::KEYWORD),
884            detail: Some(typ.pretty()),
885            ..Default::default()
886        });
887    }
888
889    items
890}
891
892// ── Type inference helpers ─────────────────────────────────────────────────
893
894fn infer_expression_type(context: &Context, expr: &str) -> Type {
895    let trimmed = expr.trim();
896
897    if trimmed.is_empty() {
898        return builder::any_type();
899    }
900
901    let types = context.get_types_from_name(trimmed);
902    if !types.is_empty() {
903        return types.last().unwrap().clone();
904    }
905
906    if let Some(typ) = infer_literal_type(trimmed) {
907        return typ;
908    }
909
910    let result = parse_and_infer_expression_type(context, trimmed);
911
912    result.unwrap_or_else(|| builder::any_type())
913}
914
915fn parse_and_infer_expression_type(context: &Context, expr: &str) -> Option<Type> {
916    let normalized_expr = normalize_dot_calls(context, expr);
917
918    let wrapped = format!("__temp <- {};", normalized_expr);
919    let span: Span = LocatedSpan::new_extra(&wrapped, "<lsp-inference>".to_string());
920
921    let ast = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(span)))
922        .ok()?
923        .ast;
924
925    let type_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
926        typr_core::typing_with_errors(context, &ast)
927    }))
928    .ok()?;
929
930    Some(type_result.type_context.value.clone())
931}
932
933fn normalize_dot_calls(context: &Context, expr: &str) -> String {
934    let trimmed = expr.trim();
935
936    let mut result = trimmed.to_string();
937    let mut changed = true;
938
939    while changed {
940        changed = false;
941        if let Some(transformed) = try_normalize_rightmost_dot_call(context, &result) {
942            result = transformed;
943            changed = true;
944        }
945    }
946
947    result
948}
949
950fn try_normalize_rightmost_dot_call(context: &Context, expr: &str) -> Option<String> {
951    let chars: Vec<char> = expr.chars().collect();
952    let len = chars.len();
953
954    let mut paren_depth = 0;
955    let mut bracket_depth = 0;
956    let mut i = len;
957
958    while i > 0 {
959        i -= 1;
960        match chars[i] {
961            ')' => paren_depth += 1,
962            '(' => {
963                if paren_depth > 0 {
964                    paren_depth -= 1;
965                }
966            }
967            ']' => bracket_depth += 1,
968            '[' => {
969                if bracket_depth > 0 {
970                    bracket_depth -= 1;
971                }
972            }
973            '.' if paren_depth == 0 && bracket_depth == 0 => {
974                if i + 1 < len && (chars[i + 1].is_alphabetic() || chars[i + 1] == '_') {
975                    let mut method_end = i + 1;
976                    while method_end < len
977                        && (chars[method_end].is_alphanumeric() || chars[method_end] == '_')
978                    {
979                        method_end += 1;
980                    }
981                    let method_name: String = chars[i + 1..method_end].iter().collect();
982
983                    let types = context.get_types_from_name(&method_name);
984                    let is_known_function = types.iter().any(|t| t.is_function());
985
986                    let is_std_function = context
987                        .typing_context
988                        .standard_library()
989                        .iter()
990                        .any(|(v, t)| v.get_name() == method_name && t.is_function());
991
992                    if is_known_function || is_std_function {
993                        let receiver: String = chars[..i].iter().collect();
994                        let after_method: String = chars[method_end..].iter().collect();
995
996                        if after_method.starts_with('(') {
997                            let after_chars: Vec<char> = after_method.chars().collect();
998                            let mut depth = 0;
999                            let mut close_idx = 0;
1000                            for (j, &c) in after_chars.iter().enumerate() {
1001                                match c {
1002                                    '(' => depth += 1,
1003                                    ')' => {
1004                                        depth -= 1;
1005                                        if depth == 0 {
1006                                            close_idx = j;
1007                                            break;
1008                                        }
1009                                    }
1010                                    _ => {}
1011                                }
1012                            }
1013
1014                            let args_content: String = after_chars[1..close_idx].iter().collect();
1015                            let rest: String = after_chars[close_idx + 1..].iter().collect();
1016
1017                            if args_content.trim().is_empty() {
1018                                return Some(format!(
1019                                    "{}({}){}",
1020                                    method_name,
1021                                    receiver.trim(),
1022                                    rest
1023                                ));
1024                            } else {
1025                                return Some(format!(
1026                                    "{}({}, {}){}",
1027                                    method_name,
1028                                    receiver.trim(),
1029                                    args_content.trim(),
1030                                    rest
1031                                ));
1032                            }
1033                        } else {
1034                            return None;
1035                        }
1036                    }
1037                }
1038            }
1039            _ => {}
1040        }
1041    }
1042
1043    None
1044}
1045
1046fn get_first_parameter_type(typ: &Type) -> Option<Type> {
1047    match typ {
1048        Type::Function(params, _, _) => params.first().cloned(),
1049        _ => None,
1050    }
1051}
1052
1053fn var_to_completion_item(var: &Var, typ: &Type, kind: CompletionItemKind) -> CompletionItem {
1054    CompletionItem {
1055        label: var.get_name(),
1056        kind: Some(kind),
1057        detail: Some(typ.pretty()),
1058        ..Default::default()
1059    }
1060}