Skip to main content

shape_lsp/
hover.rs

1//! Hover information provider for Shape
2//!
3//! Provides type information and documentation when hovering over symbols.
4
5use crate::annotation_discovery::{AnnotationDiscovery, render_annotation_documentation};
6use crate::context::{CompletionContext, analyze_context, is_inside_interpolation_expression};
7use crate::doc_render::render_doc_comment;
8use crate::module_cache::ModuleCache;
9use crate::scope::ScopeTree;
10use crate::symbols::{SymbolKind, extract_symbols};
11use crate::trait_lookup::resolve_trait_definition;
12use crate::type_inference::{
13    FunctionTypeInfo, ParamReferenceMode, extract_struct_fields,
14    infer_block_return_type_via_engine, infer_function_signatures, infer_program_types,
15    infer_variable_type, infer_variable_type_for_display, infer_variable_visible_type_at_offset,
16    parse_object_shape_fields, resolve_struct_field_type, type_annotation_to_string,
17    unified_metadata,
18};
19use crate::util::{get_word_at_position, parser_source, position_to_offset};
20use shape_ast::ast::{Expr, Item, JoinKind, Pattern, Program, Span, Statement, TypeName};
21use shape_ast::parser::parse_program;
22use shape_runtime::metadata::LanguageMetadata;
23use shape_runtime::visitor::{Visitor, walk_program};
24use std::path::Path;
25use tower_lsp_server::ls_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position};
26
27// Thread-local storage for the cached program fallback.
28// This avoids threading the parameter through every internal helper.
29std::thread_local! {
30    static CACHED_PROGRAM: std::cell::RefCell<Option<Program>> = const { std::cell::RefCell::new(None) };
31}
32
33/// Try to parse text, falling back to the thread-local cached program.
34fn parse_with_fallback(text: &str) -> Option<Program> {
35    let parse_src = parser_source(text);
36    let parse_src = parse_src.as_ref();
37
38    match parse_program(parse_src) {
39        Ok(p) => Some(p),
40        Err(_) => {
41            // Try cached program first
42            let cached = CACHED_PROGRAM.with(|c| c.borrow().clone());
43            if cached.is_some() {
44                return cached;
45            }
46            // Fall back to resilient parser — always succeeds with partial results
47            let partial = shape_ast::parser::resilient::parse_program_resilient(parse_src);
48            if !partial.items.is_empty() {
49                Some(partial.into_program())
50            } else {
51                None
52            }
53        }
54    }
55}
56
57/// Get hover information for a position in the document.
58///
59/// When `cached_program` is provided, it is used as a fallback AST when
60/// the current source text fails to parse (e.g., user is mid-edit).
61pub fn get_hover(
62    text: &str,
63    position: Position,
64    module_cache: Option<&ModuleCache>,
65    current_file: Option<&Path>,
66    cached_program: Option<&Program>,
67) -> Option<Hover> {
68    // Set the cached program as fallback for internal helpers
69    CACHED_PROGRAM.with(|c| {
70        *c.borrow_mut() = cached_program.cloned();
71    });
72
73    let result = get_hover_inner(text, position, module_cache, current_file);
74
75    // Clear the cache
76    CACHED_PROGRAM.with(|c| {
77        *c.borrow_mut() = None;
78    });
79
80    result
81}
82
83fn get_hover_inner(
84    text: &str,
85    position: Position,
86    module_cache: Option<&ModuleCache>,
87    current_file: Option<&Path>,
88) -> Option<Hover> {
89    // Get the word at the cursor position
90    let word = get_word_at_position(text, position)?;
91
92    // First, check if we're hovering on a property access (e.g., instr.symbol)
93    if let Some(hover) = get_property_access_hover(text, &word, position) {
94        return Some(hover);
95    }
96    if let Some(hover) = get_interpolation_self_property_hover(text, &word, position) {
97        return Some(hover);
98    }
99
100    // Try to find hover information for self word
101    if let Some(hover) = get_hover_for_word(text, &word, position, module_cache, current_file) {
102        return Some(hover);
103    }
104
105    // Check imported symbols via module cache
106    if let (Some(cache), Some(file_path)) = (module_cache, current_file) {
107        if let Some(hover) = get_imported_symbol_hover(text, &word, cache, file_path) {
108            return Some(hover);
109        }
110    }
111
112    None
113}
114
115/// Get hover information for a specific word
116fn get_hover_for_word(
117    text: &str,
118    word: &str,
119    position: Position,
120    module_cache: Option<&ModuleCache>,
121    current_file: Option<&Path>,
122) -> Option<Hover> {
123    // Check interpolation format-spec docs first when inside `f"{expr:spec}"`.
124    if let Some(hover) = get_interpolation_format_spec_hover(text, word, position) {
125        return Some(hover);
126    }
127
128    // Check annotations (local and imported via module resolution).
129    if let Some(hover) = get_annotation_hover(text, word, position, module_cache, current_file) {
130        return Some(hover);
131    }
132
133    // Check if we're hovering on a join strategy keyword — show resolved return type
134    if matches!(word, "all" | "race" | "any" | "settle") {
135        if let Some(hover) = get_join_expression_hover(text, word, position) {
136            return Some(hover);
137        }
138    }
139
140    // Check if hovering on `async` in `async let` or `async scope` context
141    if word == "async" {
142        if let Some(hover) = get_async_structured_hover(text, position) {
143            return Some(hover);
144        }
145    }
146
147    // Check if hovering on `scope` in `async scope` context
148    if word == "scope" {
149        if let Some(hover) = get_async_scope_keyword_hover(text, position) {
150            return Some(hover);
151        }
152    }
153
154    // Check if hovering on `comptime` as a block/expression keyword
155    if word == "comptime" {
156        if let Some(hover) = get_comptime_block_hover(text, position) {
157            return Some(hover);
158        }
159    }
160
161    // Check if hovering on a comptime builtin.
162    if let Some(hover) = get_comptime_builtin_hover(word) {
163        return Some(hover);
164    }
165
166    // `self` inside impl/extend method bodies is an implicit receiver binding.
167    if let Some(hover) = get_self_receiver_hover(text, word, position) {
168        return Some(hover);
169    }
170
171    // Hovering trait name in `impl Trait for Type` should show trait context,
172    // even when the trait is not defined in the current file.
173    if let Some(hover) =
174        get_impl_header_trait_hover(text, word, position, module_cache, current_file)
175    {
176        return Some(hover);
177    }
178
179    // Check Content API namespaces (Content, Color, Border, ChartType, Align)
180    if let Some(hover) = get_content_api_hover(word) {
181        return Some(hover);
182    }
183
184    // Check DateTime / io / time namespaces
185    if let Some(hover) = get_namespace_api_hover(word) {
186        return Some(hover);
187    }
188
189    // Check if it's a keyword
190    if let Some(hover) = get_keyword_hover(word) {
191        return Some(hover);
192    }
193
194    // Check if it's a method name inside an impl block — show trait method signature
195    // (checked before builtins so impl context takes priority over coincidentally-named builtins)
196    if let Some(hover) = get_impl_method_hover(text, word, position, module_cache, current_file) {
197        return Some(hover);
198    }
199
200    if let Some(hover) = get_extend_method_hover(text, word, position, module_cache, current_file) {
201        return Some(hover);
202    }
203
204    // Check if it's a built-in function
205    if let Some(hover) = get_builtin_function_hover(word) {
206        return Some(hover);
207    }
208
209    // Check if it's a module namespace (extension or local `mod`)
210    if let Some(hover) = get_module_hover(text, word) {
211        return Some(hover);
212    }
213
214    // Check if it's a comptime field (in struct def or type alias override)
215    if let Some(hover) = get_comptime_field_hover(text, word, position) {
216        return Some(hover);
217    }
218
219    // Check if it's a bounded type parameter — show required traits
220    if let Some(hover) = get_type_param_hover(text, word, position) {
221        return Some(hover);
222    }
223
224    // Check user-defined symbols BEFORE builtin types — prevents false matches
225    // from type aliases (e.g., "double" → "float", "record" → "object")
226    if let Some(hover) = get_typed_match_pattern_hover(text, word, position) {
227        return Some(hover);
228    }
229
230    if let Some(hover) = get_user_symbol_hover_at(text, word, position) {
231        return Some(hover);
232    }
233
234    // Check if it's a built-in type (after user symbols, to avoid alias collisions)
235    if let Some(hover) = get_type_hover(word) {
236        return Some(hover);
237    }
238
239    None
240}
241
242fn get_interpolation_format_spec_hover(
243    text: &str,
244    word: &str,
245    position: Position,
246) -> Option<Hover> {
247    if !matches!(
248        analyze_context(text, position),
249        CompletionContext::InterpolationFormatSpec { .. }
250    ) {
251        return None;
252    }
253
254    let doc = match word {
255        "fixed" => {
256            "**Interpolation Spec**: `fixed(precision)`\n\n\
257             Formats numeric values using fixed decimal precision.\n\n\
258             Example: `f\"price={p:fixed(2)}\"`"
259        }
260        "table" => {
261            "**Interpolation Spec**: `table(...)`\n\n\
262             Renders table values with typed configuration.\n\n\
263             Supported keys: `max_rows`, `align`, `precision`, `color`, `border`.\n\n\
264             Example: `f\"{rows:table(max_rows=20, align=right, precision=2, border=on)}\"`"
265        }
266        "max_rows" => {
267            "**Table Format Key**: `max_rows`\n\n\
268             Maximum number of rendered rows.\n\n\
269             Example: `table(max_rows=10)`"
270        }
271        "align" => {
272            "**Table Format Key**: `align`\n\n\
273             Global cell alignment (`left`, `center`, `right`)."
274        }
275        "precision" => {
276            "**Table Format Key**: `precision`\n\n\
277             Numeric precision for floating-point columns."
278        }
279        "color" => {
280            "**Table Format Key**: `color`\n\n\
281             Optional color hint (`default`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`)."
282        }
283        "border" => {
284            "**Table Format Key**: `border`\n\n\
285             Border mode (`on` or `off`)."
286        }
287        "left" | "center" | "right" => {
288            "**Table Align Enum**\n\n\
289             Alignment enum value used by `align=`."
290        }
291        "default" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" => {
292            "**Table Color Enum**\n\n\
293             Color enum value used by `color=`."
294        }
295        "on" | "off" => {
296            "**Table Border Enum**\n\n\
297             Border toggle value used by `border=`."
298        }
299        _ => return None,
300    };
301
302    Some(Hover {
303        contents: HoverContents::Markup(MarkupContent {
304            kind: MarkupKind::Markdown,
305            value: doc.to_string(),
306        }),
307        range: None,
308    })
309}
310
311fn span_contains_offset(span: Span, offset: usize) -> bool {
312    if span.is_dummy() || span.is_empty() {
313        return false;
314    }
315    offset >= span.start && offset < span.end
316}
317
318#[derive(Debug, Clone)]
319struct TypedMatchPatternInfo {
320    name: String,
321    def_span: (usize, usize),
322    type_name: String,
323}
324
325struct TypedMatchPatternCollector {
326    patterns: Vec<TypedMatchPatternInfo>,
327}
328
329impl Visitor for TypedMatchPatternCollector {
330    fn visit_expr(&mut self, expr: &Expr) -> bool {
331        if let Expr::Match(match_expr, _) = expr {
332            for arm in &match_expr.arms {
333                let Pattern::Typed {
334                    name,
335                    type_annotation,
336                } = &arm.pattern
337                else {
338                    continue;
339                };
340                let Some(pattern_span) = arm.pattern_span else {
341                    continue;
342                };
343                if pattern_span.is_dummy() {
344                    continue;
345                }
346                let Some(type_name) = type_annotation_to_string(type_annotation) else {
347                    continue;
348                };
349                let start = pattern_span.start;
350                let end = start.saturating_add(name.len());
351                self.patterns.push(TypedMatchPatternInfo {
352                    name: name.clone(),
353                    def_span: (start, end),
354                    type_name,
355                });
356            }
357        }
358        true
359    }
360}
361
362fn collect_typed_match_patterns(program: &Program) -> Vec<TypedMatchPatternInfo> {
363    let mut collector = TypedMatchPatternCollector {
364        patterns: Vec::new(),
365    };
366    walk_program(&mut collector, program);
367    collector.patterns
368}
369
370fn get_typed_match_pattern_hover(text: &str, word: &str, position: Position) -> Option<Hover> {
371    let mut program = parse_with_fallback(text)?;
372    shape_ast::transform::desugar_program(&mut program);
373
374    let patterns = collect_typed_match_patterns(&program);
375    if patterns.is_empty() {
376        return None;
377    }
378
379    let offset = position_to_offset(text, position)?;
380
381    if let Some(info) = patterns
382        .iter()
383        .find(|p| p.name == word && offset >= p.def_span.0 && offset < p.def_span.1)
384    {
385        return Some(build_typed_match_pattern_hover(info));
386    }
387
388    // For references inside match-arm bodies, resolve lexical binding first.
389    let scope_tree = ScopeTree::build(&program, text);
390    let binding = scope_tree.binding_at(offset)?;
391    if binding.name != word {
392        return None;
393    }
394
395    let info = patterns.iter().find(|p| p.def_span == binding.def_span)?;
396    Some(build_typed_match_pattern_hover(info))
397}
398
399fn build_typed_match_pattern_hover(info: &TypedMatchPatternInfo) -> Hover {
400    let content = format!(
401        "**Variable**: `{}`\n\n**Type:** `{}`",
402        info.name, info.type_name
403    );
404
405    Hover {
406        contents: HoverContents::Markup(MarkupContent {
407            kind: MarkupKind::Markdown,
408            value: content,
409        }),
410        range: None,
411    }
412}
413
414/// Get hover information for annotation names (`@name`) from local/imported definitions.
415fn get_annotation_hover(
416    text: &str,
417    word: &str,
418    position: Position,
419    module_cache: Option<&ModuleCache>,
420    current_file: Option<&Path>,
421) -> Option<Hover> {
422    let offset = position_to_offset(text, position)?;
423    let program = parse_with_fallback(text)?;
424
425    let is_definition_name = program.items.iter().any(|item| match item {
426        Item::AnnotationDef(annotation_def, _) => {
427            annotation_def.name == word && span_contains_offset(annotation_def.name_span, offset)
428        }
429        _ => false,
430    });
431
432    let is_usage_name = is_annotation_word_at_position(text, position);
433    if !is_definition_name && !is_usage_name {
434        return None;
435    }
436
437    let mut discovery = AnnotationDiscovery::new();
438    discovery.discover_from_program(&program);
439    if let (Some(cache), Some(file_path)) = (module_cache, current_file) {
440        discovery.discover_from_imports_with_cache(&program, file_path, cache, None);
441    } else {
442        discovery.discover_from_imports(&program);
443    }
444
445    let info = discovery.get(word)?;
446    let signature = if info.params.is_empty() {
447        format!("@{}", info.name)
448    } else {
449        format!("@{}({})", info.name, info.params.join(", "))
450    };
451    let mut sections = vec![format!("**Annotation**: `{signature}`")];
452    if let Some(documentation) =
453        render_annotation_documentation(info, Some(&program), module_cache, current_file, None)
454    {
455        sections.push(documentation);
456    }
457    if let Some(source_file) = &info.source_file {
458        sections.push(format!("**Defined in:** `{}`", source_file.display()));
459    } else {
460        sections.push("**Defined in:** current file".to_string());
461    }
462    let content = sections.join("\n\n");
463
464    Some(Hover {
465        contents: HoverContents::Markup(MarkupContent {
466            kind: MarkupKind::Markdown,
467            value: content,
468        }),
469        range: None,
470    })
471}
472
473fn is_annotation_word_at_position(text: &str, position: Position) -> bool {
474    let Some(offset) = position_to_offset(text, position) else {
475        return false;
476    };
477    let mut start = offset.min(text.len());
478
479    while start > 0 {
480        let ch = text[..start]
481            .chars()
482            .next_back()
483            .expect("slice is non-empty when start > 0");
484        if ch.is_ascii_alphanumeric() || ch == '_' {
485            start -= ch.len_utf8();
486        } else {
487            break;
488        }
489    }
490
491    text[..start].chars().next_back() == Some('@')
492}
493
494/// Get hover for Content API namespaces (Content, Color, Border, ChartType, Align)
495fn get_content_api_hover(word: &str) -> Option<Hover> {
496    let doc = match word {
497        "Content" => {
498            "**Content API**\n\n\
499             Static constructors for building rich content nodes.\n\n\
500             **Methods:**\n\
501             - `Content.text(string)` — Create a plain text content node\n\
502             - `Content.table(data)` — Create a table from a collection\n\
503             - `Content.chart(type, data)` — Create a chart\n\
504             - `Content.fragment(parts)` — Compose multiple content nodes\n\
505             - `Content.code(language, source)` — Create a code block\n\
506             - `Content.kv(pairs)` — Create key-value content\n\n\
507             Content strings (`c\"...\"`) produce `ContentNode` values that can be \
508             styled and composed using the Content API."
509        }
510        "Color" => {
511            "**Color Enum**\n\n\
512             Terminal color values for styling content strings.\n\n\
513             **Values:**\n\
514             - `Color.red`, `Color.green`, `Color.blue`, `Color.yellow`\n\
515             - `Color.magenta`, `Color.cyan`, `Color.white`, `Color.default`\n\
516             - `Color.rgb(r, g, b)` — Custom RGB color (0-255 per channel)"
517        }
518        "Border" => {
519            "**Border Enum**\n\n\
520             Border styles for content tables and panels.\n\n\
521             **Values:**\n\
522             - `Border.rounded` — Rounded corners (default)\n\
523             - `Border.sharp` — Sharp 90-degree corners\n\
524             - `Border.heavy` — Thick border lines\n\
525             - `Border.double` — Double-line border\n\
526             - `Border.minimal` — Minimal separator lines\n\
527             - `Border.none` — No border"
528        }
529        "ChartType" => {
530            "**ChartType Enum**\n\n\
531             Chart type selectors for `Content.chart()`.\n\n\
532             **Values:**\n\
533             - `ChartType.line` — Line chart\n\
534             - `ChartType.bar` — Bar chart\n\
535             - `ChartType.scatter` — Scatter plot\n\
536             - `ChartType.area` — Area chart\n\
537             - `ChartType.candlestick` — Candlestick chart\n\
538             - `ChartType.histogram` — Histogram"
539        }
540        "Align" => {
541            "**Align Enum**\n\n\
542             Text alignment for content layout.\n\n\
543             **Values:**\n\
544             - `Align.left` — Left-aligned (default)\n\
545             - `Align.center` — Center-aligned\n\
546             - `Align.right` — Right-aligned"
547        }
548        _ => return None,
549    };
550
551    Some(Hover {
552        contents: HoverContents::Markup(MarkupContent {
553            kind: MarkupKind::Markdown,
554            value: doc.to_string(),
555        }),
556        range: None,
557    })
558}
559
560/// Get hover for Content API member access (e.g., Content.text, Color.red, Border.rounded)
561fn get_content_member_hover(object: &str, member: &str) -> Option<Hover> {
562    let doc = match (object, member) {
563        // Content constructors
564        ("Content", "text") => {
565            "**Content.text**(string): ContentNode\n\nCreate a plain text content node.\n\n```shape\nContent.text(\"Hello world\")\n```"
566        }
567        ("Content", "table") => {
568            "**Content.table**(data): ContentNode\n\nCreate a table from a collection or array of objects.\n\n```shape\nContent.table(my_data)\n```"
569        }
570        ("Content", "chart") => {
571            "**Content.chart**(type, data): ContentNode\n\nCreate a chart visualization.\n\n```shape\nContent.chart(ChartType.line, series)\n```"
572        }
573        ("Content", "fragment") => {
574            "**Content.fragment**(parts): ContentNode\n\nCompose multiple content nodes into a single fragment.\n\n```shape\nContent.fragment([header, body, footer])\n```"
575        }
576        ("Content", "code") => {
577            "**Content.code**(language, source): ContentNode\n\nCreate a syntax-highlighted code block.\n\n```shape\nContent.code(\"shape\", \"let x = 42\")\n```"
578        }
579        ("Content", "kv") => {
580            "**Content.kv**(pairs): ContentNode\n\nCreate a key-value display from an object.\n\n```shape\nContent.kv({ name: \"test\", value: 42 })\n```"
581        }
582
583        // Color values
584        ("Color", "red") => "**Color.red**: Color\n\nRed terminal color.",
585        ("Color", "green") => "**Color.green**: Color\n\nGreen terminal color.",
586        ("Color", "blue") => "**Color.blue**: Color\n\nBlue terminal color.",
587        ("Color", "yellow") => "**Color.yellow**: Color\n\nYellow terminal color.",
588        ("Color", "magenta") => "**Color.magenta**: Color\n\nMagenta terminal color.",
589        ("Color", "cyan") => "**Color.cyan**: Color\n\nCyan terminal color.",
590        ("Color", "white") => "**Color.white**: Color\n\nWhite terminal color.",
591        ("Color", "default") => {
592            "**Color.default**: Color\n\nDefault terminal color (inherits from parent)."
593        }
594        ("Color", "rgb") => {
595            "**Color.rgb**(r, g, b): Color\n\nCustom RGB color. Each component must be 0-255.\n\n```shape\nColor.rgb(255, 128, 0)\n```"
596        }
597
598        // Border styles
599        ("Border", "rounded") => {
600            "**Border.rounded**: Border\n\nRounded corners border style (default).\n```\n\u{256d}\u{2500}\u{2500}\u{2500}\u{256e}\n\u{2502}   \u{2502}\n\u{2570}\u{2500}\u{2500}\u{2500}\u{256f}\n```"
601        }
602        ("Border", "sharp") => {
603            "**Border.sharp**: Border\n\nSharp 90-degree corners.\n```\n\u{250c}\u{2500}\u{2500}\u{2500}\u{2510}\n\u{2502}   \u{2502}\n\u{2514}\u{2500}\u{2500}\u{2500}\u{2518}\n```"
604        }
605        ("Border", "heavy") => {
606            "**Border.heavy**: Border\n\nThick border lines.\n```\n\u{250f}\u{2501}\u{2501}\u{2501}\u{2513}\n\u{2503}   \u{2503}\n\u{2517}\u{2501}\u{2501}\u{2501}\u{251b}\n```"
607        }
608        ("Border", "double") => {
609            "**Border.double**: Border\n\nDouble-line border.\n```\n\u{2554}\u{2550}\u{2550}\u{2550}\u{2557}\n\u{2551}   \u{2551}\n\u{255a}\u{2550}\u{2550}\u{2550}\u{255d}\n```"
610        }
611        ("Border", "minimal") => "**Border.minimal**: Border\n\nMinimal separator lines only.",
612        ("Border", "none") => "**Border.none**: Border\n\nNo border.",
613
614        // ChartType values
615        ("ChartType", "line") => {
616            "**ChartType.line**: ChartType\n\nLine chart — connects data points with lines."
617        }
618        ("ChartType", "bar") => {
619            "**ChartType.bar**: ChartType\n\nBar chart — vertical bars for each data point."
620        }
621        ("ChartType", "scatter") => {
622            "**ChartType.scatter**: ChartType\n\nScatter plot — individual data points."
623        }
624        ("ChartType", "area") => {
625            "**ChartType.area**: ChartType\n\nArea chart — filled area under a line."
626        }
627        ("ChartType", "candlestick") => {
628            "**ChartType.candlestick**: ChartType\n\nCandlestick chart — OHLC financial data."
629        }
630        ("ChartType", "histogram") => {
631            "**ChartType.histogram**: ChartType\n\nHistogram — frequency distribution of values."
632        }
633
634        // Align values
635        ("Align", "left") => "**Align.left**: Align\n\nLeft-aligned text (default).",
636        ("Align", "center") => "**Align.center**: Align\n\nCenter-aligned text.",
637        ("Align", "right") => "**Align.right**: Align\n\nRight-aligned text.",
638
639        _ => return None,
640    };
641
642    Some(Hover {
643        contents: HoverContents::Markup(MarkupContent {
644            kind: MarkupKind::Markdown,
645            value: doc.to_string(),
646        }),
647        range: None,
648    })
649}
650
651/// Get hover for keywords
652fn get_keyword_hover(word: &str) -> Option<Hover> {
653    let keywords = LanguageMetadata::keywords();
654    let keyword = keywords.iter().find(|k| k.keyword == word)?;
655
656    let content = format!(
657        "**Keyword**: `{}`\n\n{}",
658        keyword.keyword, keyword.description
659    );
660
661    Some(Hover {
662        contents: HoverContents::Markup(MarkupContent {
663            kind: MarkupKind::Markdown,
664            value: content,
665        }),
666        range: None,
667    })
668}
669
670/// Get hover for built-in functions (using unified metadata)
671fn get_builtin_function_hover(word: &str) -> Option<Hover> {
672    let function = unified_metadata().get_function(word)?;
673
674    let mut content = format!(
675        "**Function**: `{}`\n\n{}\n\n**Signature:**\n```shape\n{}\n```",
676        function.name, function.description, function.signature
677    );
678
679    if !function.parameters.is_empty() {
680        content.push_str("\n\n**Parameters:**\n");
681        for param in &function.parameters {
682            content.push_str(&format!(
683                "- `{}`: `{}` - {}\n",
684                param.name, param.param_type, param.description
685            ));
686        }
687    }
688
689    content.push_str(&format!("\n**Returns:** `{}`", function.return_type));
690
691    if let Some(example) = &function.example {
692        content.push_str(&format!("\n\n**Example:**\n```shape\n{}\n```", example));
693    }
694
695    Some(Hover {
696        contents: HoverContents::Markup(MarkupContent {
697            kind: MarkupKind::Markdown,
698            value: content,
699        }),
700        range: None,
701    })
702}
703
704/// Get hover for types
705fn get_type_hover(word: &str) -> Option<Hover> {
706    let word = word.trim();
707    let types = LanguageMetadata::builtin_types();
708    let type_info = types
709        .iter()
710        .find(|t| t.name == word)
711        .or_else(|| types.iter().find(|t| t.name.eq_ignore_ascii_case(word)));
712
713    let (type_name, type_description) = if let Some(info) = type_info {
714        (info.name.clone(), info.description.clone())
715    } else {
716        let (name, description) = fallback_builtin_type_hover(word)?;
717        (name.to_string(), description.to_string())
718    };
719
720    let content = format!("**Type**: `{}`\n\n{}", type_name, type_description);
721
722    Some(Hover {
723        contents: HoverContents::Markup(MarkupContent {
724            kind: MarkupKind::Markdown,
725            value: content,
726        }),
727        range: None,
728    })
729}
730
731fn fallback_builtin_type_hover(word: &str) -> Option<(&'static str, &'static str)> {
732    match word.to_ascii_lowercase().as_str() {
733        "int" | "integer" => Some(("int", "Integer numeric type")),
734        "float" | "double" => Some(("float", "Floating-point numeric type")),
735        "number" => Some(("number", "Numeric type (integer or floating-point)")),
736        "string" | "str" => Some(("string", "String type")),
737        "bool" | "boolean" => Some(("bool", "Boolean type (true or false)")),
738        "array" => Some(("Array", "Array type")),
739        "table" => Some((
740            "Table",
741            "Typed table container for row-oriented and relational operations",
742        )),
743        "object" | "record" => Some(("object", "Object type")),
744        "datetime" => Some(("DateTime", "Date/time value")),
745        "result" => Some(("Result", "Result type - Ok(value) or Err(AnyError)")),
746        "option" => Some(("Option", "Option type - Some(value) or None")),
747        "anyerror" => Some(("AnyError", "Universal runtime error type used by Result<T>")),
748        _ => None,
749    }
750}
751
752/// Get hover for `async` when used in `async let` or `async scope` context.
753fn get_async_structured_hover(text: &str, position: Position) -> Option<Hover> {
754    let offset = position_to_offset(text, position)?;
755    let program = parse_with_fallback(text)?;
756
757    #[derive(Clone, Copy)]
758    enum AsyncHoverKind {
759        AsyncLet,
760        AsyncScope,
761    }
762
763    struct AsyncContextFinder {
764        offset: usize,
765        best: Option<(usize, AsyncHoverKind)>,
766    }
767
768    impl Visitor for AsyncContextFinder {
769        fn visit_expr(&mut self, expr: &Expr) -> bool {
770            let (kind, span) = match expr {
771                Expr::AsyncLet(_, span) => (Some(AsyncHoverKind::AsyncLet), *span),
772                Expr::AsyncScope(_, span) => (Some(AsyncHoverKind::AsyncScope), *span),
773                _ => (None, Span::DUMMY),
774            };
775
776            if let Some(kind) = kind {
777                if span_contains_offset(span, self.offset) {
778                    let len = span.len();
779                    if self
780                        .best
781                        .map(|(best_len, _)| len < best_len)
782                        .unwrap_or(true)
783                    {
784                        self.best = Some((len, kind));
785                    }
786                }
787            }
788
789            true
790        }
791    }
792
793    let mut finder = AsyncContextFinder { offset, best: None };
794    walk_program(&mut finder, &program);
795
796    match finder.best.map(|(_, kind)| kind) {
797        Some(AsyncHoverKind::AsyncLet) => {
798            let content = "**Async Let**: `async let name = expr`\n\n\
799                Spawns an asynchronous task and binds a future handle to a local variable.\n\n\
800                The task begins executing immediately. Use `await name` to retrieve the result.\n\n\
801                **Requirements:** Must be used inside an `async` function.\n\n\
802                **Example:**\n\
803                ```shape\nasync fn fetch_data() {\n  async let a = fetch(\"url1\")\n  async let b = fetch(\"url2\")\n  let results = (await a, await b)\n}\n```";
804            Some(Hover {
805                contents: HoverContents::Markup(MarkupContent {
806                    kind: MarkupKind::Markdown,
807                    value: content.to_string(),
808                }),
809                range: None,
810            })
811        }
812        Some(AsyncHoverKind::AsyncScope) => {
813            let content = "**Async Scope**: `async scope { ... }`\n\n\
814                Creates a structured concurrency boundary. All tasks spawned inside the scope \
815                are automatically cancelled (in LIFO order) when the scope exits.\n\n\
816                **Requirements:** Must be used inside an `async` function.\n\n\
817                **Example:**\n\
818                ```shape\nasync fn process() {\n  async scope {\n    async let a = task1()\n    async let b = task2()\n    await a + await b\n  }\n  // a and b are guaranteed complete or cancelled here\n}\n```";
819            Some(Hover {
820                contents: HoverContents::Markup(MarkupContent {
821                    kind: MarkupKind::Markdown,
822                    value: content.to_string(),
823                }),
824                range: None,
825            })
826        }
827        None => None,
828    }
829}
830
831/// Get hover for `scope` keyword when used in `async scope` context.
832fn get_async_scope_keyword_hover(text: &str, position: Position) -> Option<Hover> {
833    let offset = position_to_offset(text, position)?;
834    let program = parse_with_fallback(text)?;
835
836    struct AsyncScopeFinder {
837        offset: usize,
838        found: bool,
839    }
840
841    impl Visitor for AsyncScopeFinder {
842        fn visit_expr(&mut self, expr: &Expr) -> bool {
843            if let Expr::AsyncScope(_, span) = expr {
844                if span_contains_offset(*span, self.offset) {
845                    self.found = true;
846                }
847            }
848            true
849        }
850    }
851
852    let mut finder = AsyncScopeFinder {
853        offset,
854        found: false,
855    };
856    walk_program(&mut finder, &program);
857
858    if !finder.found {
859        return None;
860    }
861
862    let content = "**Scope** (structured concurrency)\n\n\
863        The `scope` keyword after `async` creates a structured concurrency boundary.\n\
864        All spawned tasks within the scope are tracked and automatically cancelled \
865        when the scope exits, ensuring no dangling tasks.";
866    Some(Hover {
867        contents: HoverContents::Markup(MarkupContent {
868            kind: MarkupKind::Markdown,
869            value: content.to_string(),
870        }),
871        range: None,
872    })
873}
874
875/// Get hover for `comptime` when used as a block or expression keyword.
876///
877/// Shows compile-time block info with available builtins when hovering on `comptime`
878/// followed by `{` (block context), as opposed to struct field context.
879fn get_comptime_block_hover(text: &str, position: Position) -> Option<Hover> {
880    let offset = position_to_offset(text, position)?;
881    let program = parse_with_fallback(text)?;
882
883    struct ComptimeContextFinder {
884        offset: usize,
885        found: bool,
886    }
887
888    impl Visitor for ComptimeContextFinder {
889        fn visit_expr(&mut self, expr: &Expr) -> bool {
890            if let Expr::Comptime(_, span) = expr {
891                if span_contains_offset(*span, self.offset) {
892                    self.found = true;
893                }
894            }
895            true
896        }
897
898        fn visit_item(&mut self, item: &Item) -> bool {
899            if let Item::Comptime(_, span) = item {
900                if span_contains_offset(*span, self.offset) {
901                    self.found = true;
902                }
903            }
904            true
905        }
906    }
907
908    let mut finder = ComptimeContextFinder {
909        offset,
910        found: false,
911    };
912    walk_program(&mut finder, &program);
913
914    if !finder.found {
915        return None;
916    }
917
918    let comptime_builtins: Vec<_> = unified_metadata()
919        .all_functions()
920        .into_iter()
921        .filter(|f| f.comptime_only)
922        .collect();
923    let builtins_list = if comptime_builtins.is_empty() {
924        "- (no comptime intrinsics discovered)".to_string()
925    } else {
926        comptime_builtins
927            .iter()
928            .map(|f| format!("- `{}`", f.signature))
929            .collect::<Vec<_>>()
930            .join("\n")
931    };
932    let example = comptime_builtins
933        .iter()
934        .find_map(|f| f.example.as_deref())
935        .unwrap_or("let version = comptime { build_config().version }");
936
937    let content = "**Compile-Time Block**: `comptime { }`\n\n\
938        Evaluates the enclosed expression at compile time. The result is \
939        embedded as a constant in the compiled output.\n\n\
940        **Available builtins:**\n\
941"
942    .to_string()
943        + &builtins_list
944        + "\n\n\
945        **Example:**\n\
946        ```shape\n"
947        + example
948        + "\n```";
949
950    Some(Hover {
951        contents: HoverContents::Markup(MarkupContent {
952            kind: MarkupKind::Markdown,
953            value: content,
954        }),
955        range: None,
956    })
957}
958
959/// Get hover for comptime builtin functions.
960fn get_comptime_builtin_hover(word: &str) -> Option<Hover> {
961    let function = unified_metadata()
962        .all_functions()
963        .into_iter()
964        .find(|f| f.comptime_only && f.name == word)?;
965    let mut doc = format!(
966        "**`{}`**\n\n{}\n\n*Only available inside `comptime {{ }}` blocks.*",
967        function.signature, function.description
968    );
969    if !function.parameters.is_empty() {
970        doc.push_str("\n\n**Parameters:**\n");
971        for param in &function.parameters {
972            doc.push_str(&format!(
973                "- `{}`: `{}` - {}\n",
974                param.name, param.param_type, param.description
975            ));
976        }
977    }
978    if let Some(example) = &function.example {
979        doc.push_str(&format!("\n**Example:**\n```shape\n{}\n```", example));
980    }
981
982    Some(Hover {
983        contents: HoverContents::Markup(MarkupContent {
984            kind: MarkupKind::Markdown,
985            value: doc,
986        }),
987        range: None,
988    })
989}
990
991/// Get hover for a comptime field name.
992///
993/// Shows the comptime field's type, default value, and resolved value when inside a type alias
994/// override (e.g., `type EUR = Currency { symbol: "EUR" }`).
995fn get_comptime_field_hover(text: &str, word: &str, position: Position) -> Option<Hover> {
996    let program = parse_with_fallback(text)?;
997    let offset = position_to_offset(text, position)?;
998
999    // Check if cursor is inside a type alias override:
1000    // `type EUR = Currency { symbol: ... }`
1001    for item in &program.items {
1002        let Item::TypeAlias(alias_def, alias_span) = item else {
1003            continue;
1004        };
1005        if !span_contains_offset(*alias_span, offset) {
1006            continue;
1007        }
1008        let shape_ast::ast::TypeAnnotation::Basic(base_type) = &alias_def.type_annotation else {
1009            continue;
1010        };
1011
1012        for item in &program.items {
1013            if let Item::StructType(struct_def, _) = item {
1014                if struct_def.name == *base_type {
1015                    for field in &struct_def.fields {
1016                        if field.name == word && field.is_comptime {
1017                            let type_str = type_annotation_to_string(&field.type_annotation)
1018                                .unwrap_or_else(|| "unknown".to_string());
1019                            let default_str = field
1020                                .default_value
1021                                .as_ref()
1022                                .map(format_expr_short)
1023                                .unwrap_or_else(|| "none".to_string());
1024
1025                            let content = format!(
1026                                "**Comptime Field**: `{}`\n\n**Type:** `{}`\n**Default:** `{}`\n\nCompile-time constant field of type `{}`",
1027                                word, type_str, default_str, base_type
1028                            );
1029                            return Some(Hover {
1030                                contents: HoverContents::Markup(MarkupContent {
1031                                    kind: MarkupKind::Markdown,
1032                                    value: content,
1033                                }),
1034                                range: None,
1035                            });
1036                        }
1037                    }
1038                }
1039            }
1040        }
1041    }
1042
1043    // Check if cursor is on a comptime field inside a struct type definition
1044    for item in &program.items {
1045        if let Item::StructType(struct_def, span) = item {
1046            if span_contains_offset(*span, offset) {
1047                for field in &struct_def.fields {
1048                    if field.name == word && field.is_comptime {
1049                        let type_str = type_annotation_to_string(&field.type_annotation)
1050                            .unwrap_or_else(|| "unknown".to_string());
1051                        let default_str = field
1052                            .default_value
1053                            .as_ref()
1054                            .map(format_expr_short)
1055                            .unwrap_or_else(|| "none".to_string());
1056
1057                        let content = format!(
1058                            "**Comptime Field**: `{}`\n\n**Type:** `{}`\n**Default:** `{}`\n\nCompile-time constant field of type `{}`. Resolved at compile time — zero runtime cost.",
1059                            word, type_str, default_str, struct_def.name
1060                        );
1061                        return Some(Hover {
1062                            contents: HoverContents::Markup(MarkupContent {
1063                                kind: MarkupKind::Markdown,
1064                                value: content,
1065                            }),
1066                            range: None,
1067                        });
1068                    }
1069                }
1070            }
1071        }
1072    }
1073
1074    None
1075}
1076
1077/// Format an expression as a short string for display in hover
1078fn format_expr_short(expr: &Expr) -> String {
1079    match expr {
1080        Expr::Literal(lit, _) => match lit {
1081            shape_ast::ast::Literal::String(s) => format!("\"{}\"", s),
1082            shape_ast::ast::Literal::Number(n) => format!("{}", n),
1083            shape_ast::ast::Literal::Int(n) => format!("{}", n),
1084            shape_ast::ast::Literal::Decimal(d) => format!("{}D", d),
1085            shape_ast::ast::Literal::Bool(b) => format!("{}", b),
1086            shape_ast::ast::Literal::None => "None".to_string(),
1087            _ => "...".to_string(),
1088        },
1089        _ => "...".to_string(),
1090    }
1091}
1092
1093/// Get hover for a bounded type parameter.
1094///
1095/// When the cursor is on a type parameter name (e.g., `T` in `fn foo<T: Comparable>`),
1096/// shows the required trait bounds.
1097fn get_type_param_hover(text: &str, word: &str, position: Position) -> Option<Hover> {
1098    let offset = position_to_offset(text, position)?;
1099    let program = parse_with_fallback(text)?;
1100
1101    for item in &program.items {
1102        let (type_params, span) = match item {
1103            Item::Function(func, span) => (func.type_params.as_ref(), *span),
1104            Item::Trait(trait_def, span) => (trait_def.type_params.as_ref(), *span),
1105            _ => (None, Span::DUMMY),
1106        };
1107
1108        if !span_contains_offset(span, offset) {
1109            continue;
1110        }
1111
1112        if let Some(params) = type_params {
1113            for tp in params {
1114                if tp.name == word && !tp.trait_bounds.is_empty() {
1115                    let bounds_str = tp.trait_bounds.iter().map(|t| t.as_str()).collect::<Vec<_>>().join(" + ");
1116                    let content = format!(
1117                        "**Type Parameter**: `{}`\n\n**Bounds:** `{}: {}`\n\nMust implement: {}",
1118                        word,
1119                        word,
1120                        bounds_str,
1121                        tp.trait_bounds
1122                            .iter()
1123                            .map(|b| format!("`{}`", b))
1124                            .collect::<Vec<_>>()
1125                            .join(", ")
1126                    );
1127                    return Some(Hover {
1128                        contents: HoverContents::Markup(MarkupContent {
1129                            kind: MarkupKind::Markdown,
1130                            value: content,
1131                        }),
1132                        range: None,
1133                    });
1134                }
1135            }
1136        }
1137    }
1138
1139    None
1140}
1141
1142/// Get hover for a method name inside an impl block.
1143///
1144/// When the cursor is on a method name within `impl Trait for Type { method foo(...) { ... } }`,
1145/// self shows the trait method signature.
1146fn get_impl_method_hover(
1147    text: &str,
1148    word: &str,
1149    position: Position,
1150    module_cache: Option<&ModuleCache>,
1151    current_file: Option<&Path>,
1152) -> Option<Hover> {
1153    use crate::type_inference::type_annotation_to_string;
1154
1155    let offset = position_to_offset(text, position)?;
1156    let program = parse_with_fallback(text)?;
1157
1158    let mut selected_impl: Option<(&shape_ast::ast::ImplBlock, Span)> = None;
1159    for item in &program.items {
1160        let Item::Impl(impl_block, span) = item else {
1161            continue;
1162        };
1163        if !span_contains_offset(*span, offset) {
1164            continue;
1165        }
1166        let is_method_name = impl_block.methods.iter().any(|method| method.name == word);
1167        if !is_method_name {
1168            continue;
1169        }
1170
1171        if selected_impl
1172            .map(|(_, current_span)| span.len() < current_span.len())
1173            .unwrap_or(true)
1174        {
1175            selected_impl = Some((impl_block, *span));
1176        }
1177    }
1178
1179    let (impl_block, _) = selected_impl?;
1180    let trait_name = type_name_base_name(&impl_block.trait_name);
1181    let target_type = type_name_base_name(&impl_block.target_type);
1182    if trait_name.is_empty() {
1183        return None;
1184    }
1185
1186    if let Some(resolved_trait) =
1187        resolve_trait_definition(&program, &trait_name, module_cache, current_file, None)
1188    {
1189        for member in &resolved_trait.trait_def.members {
1190            match member {
1191                shape_ast::ast::TraitMember::Required(
1192                    shape_ast::ast::InterfaceMember::Method {
1193                        name,
1194                        params,
1195                        return_type,
1196                        doc_comment,
1197                        ..
1198                    },
1199                ) if name == word => {
1200                    let param_names: Vec<String> = params
1201                        .iter()
1202                        .map(|p| {
1203                            let pname = p.name.clone().unwrap_or_else(|| "_".to_string());
1204                            let ptype = type_annotation_to_string(&p.type_annotation)
1205                                .unwrap_or_else(|| "_".to_string());
1206                            format!("{}: {}", pname, ptype)
1207                        })
1208                        .collect();
1209                    let return_type_str =
1210                        type_annotation_to_string(return_type).unwrap_or_else(|| "_".to_string());
1211                    let signature =
1212                        format!("{}({}): {}", name, param_names.join(", "), return_type_str);
1213                    let mut content = format!(
1214                        "**Trait Method**: `{}`\n\n**Trait:** `{}`\n**Target:** `{}`\n\n**Signature:**\n```shape\n{}\n```",
1215                        name, trait_name, target_type, signature
1216                    );
1217                    if let Some(comment) = doc_comment.as_ref() {
1218                        content.push_str(&format!(
1219                            "\n\n{}",
1220                            render_doc_comment(&program, comment, module_cache, current_file, None,)
1221                        ));
1222                    }
1223                    if let Some(impl_name) = &impl_block.impl_name {
1224                        content.push_str(&format!("\n\n**Implementation:** `{}`", impl_name));
1225                    }
1226                    return Some(Hover {
1227                        contents: HoverContents::Markup(MarkupContent {
1228                            kind: MarkupKind::Markdown,
1229                            value: content,
1230                        }),
1231                        range: None,
1232                    });
1233                }
1234                shape_ast::ast::TraitMember::Default(method_def) if method_def.name == word => {
1235                    let param_names: Vec<String> = method_def
1236                        .params
1237                        .iter()
1238                        .map(|p| p.simple_name().unwrap_or("_").to_string())
1239                        .collect();
1240
1241                    let return_type_str = method_def
1242                        .return_type
1243                        .as_ref()
1244                        .and_then(type_annotation_to_string)
1245                        .unwrap_or_else(|| "_".to_string());
1246
1247                    let signature = format!(
1248                        "{}({}): {}",
1249                        method_def.name,
1250                        param_names.join(", "),
1251                        return_type_str
1252                    );
1253
1254                    let mut content = format!(
1255                        "**Trait Method** (default): `{}`\n\n**Trait:** `{}`\n**Target:** `{}`\n\nThis method has a default implementation and does not need to be overridden.\n\n**Signature:**\n```shape\n{}\n```",
1256                        method_def.name, trait_name, target_type, signature
1257                    );
1258                    if let Some(comment) = program.docs.comment_for_span(method_def.span) {
1259                        content.push_str(&format!(
1260                            "\n\n{}",
1261                            render_doc_comment(&program, comment, module_cache, current_file, None,)
1262                        ));
1263                    }
1264                    if let Some(impl_name) = &impl_block.impl_name {
1265                        content.push_str(&format!("\n\n**Implementation:** `{}`", impl_name));
1266                    }
1267
1268                    return Some(Hover {
1269                        contents: HoverContents::Markup(MarkupContent {
1270                            kind: MarkupKind::Markdown,
1271                            value: content,
1272                        }),
1273                        range: None,
1274                    });
1275                }
1276                _ => {}
1277            }
1278        }
1279    }
1280
1281    // Fallback: trait definition may live in another module; still provide
1282    // method-level hover from the impl body itself.
1283    if let Some(method_def) = impl_block.methods.iter().find(|method| method.name == word) {
1284        let param_names: Vec<String> = method_def
1285            .params
1286            .iter()
1287            .map(|p| {
1288                let pname = p.simple_name().unwrap_or("_").to_string();
1289                let ptype = p
1290                    .type_annotation
1291                    .as_ref()
1292                    .and_then(type_annotation_to_string);
1293                match ptype {
1294                    Some(t) => format!("{}: {}", pname, t),
1295                    None => pname,
1296                }
1297            })
1298            .collect();
1299
1300        let return_type_str = method_def
1301            .return_type
1302            .as_ref()
1303            .and_then(type_annotation_to_string)
1304            .or_else(|| infer_block_return_type_via_engine(&method_def.body))
1305            .unwrap_or_else(|| "unknown".to_string());
1306
1307        let signature = format!(
1308            "{}({}): {}",
1309            method_def.name,
1310            param_names.join(", "),
1311            return_type_str
1312        );
1313
1314        let mut content = format!(
1315            "**Method**: `{}`\n\n**Trait:** `{}`\n**Target:** `{}`\n\n**Signature:**\n```shape\n{}\n```",
1316            method_def.name, trait_name, target_type, signature
1317        );
1318        if let Some(comment) = program.docs.comment_for_span(method_def.span) {
1319            content.push_str(&format!(
1320                "\n\n{}",
1321                render_doc_comment(&program, comment, module_cache, current_file, None)
1322            ));
1323        }
1324        if let Some(impl_name) = &impl_block.impl_name {
1325            content.push_str(&format!("\n\n**Implementation:** `{}`", impl_name));
1326        }
1327
1328        return Some(Hover {
1329            contents: HoverContents::Markup(MarkupContent {
1330                kind: MarkupKind::Markdown,
1331                value: content,
1332            }),
1333            range: None,
1334        });
1335    }
1336
1337    None
1338}
1339
1340fn get_extend_method_hover(
1341    text: &str,
1342    word: &str,
1343    position: Position,
1344    module_cache: Option<&ModuleCache>,
1345    current_file: Option<&Path>,
1346) -> Option<Hover> {
1347    use crate::type_inference::type_annotation_to_string;
1348
1349    let offset = position_to_offset(text, position)?;
1350    let program = parse_with_fallback(text)?;
1351
1352    let mut selected_extend: Option<&shape_ast::ast::ExtendStatement> = None;
1353    for item in &program.items {
1354        let Item::Extend(extend, span) = item else {
1355            continue;
1356        };
1357        if !span_contains_offset(*span, offset) {
1358            continue;
1359        }
1360        if !extend.methods.iter().any(|method| method.name == word) {
1361            continue;
1362        }
1363        selected_extend = Some(extend);
1364        break;
1365    }
1366
1367    let extend = selected_extend?;
1368    let target_type = type_name_base_name(&extend.type_name);
1369    let method = extend.methods.iter().find(|method| method.name == word)?;
1370    let param_names: Vec<String> = method
1371        .params
1372        .iter()
1373        .map(|p| {
1374            let pname = p.simple_name().unwrap_or("_").to_string();
1375            let ptype = p
1376                .type_annotation
1377                .as_ref()
1378                .and_then(type_annotation_to_string);
1379            match ptype {
1380                Some(t) => format!("{pname}: {t}"),
1381                None => pname,
1382            }
1383        })
1384        .collect();
1385    let return_type = method
1386        .return_type
1387        .as_ref()
1388        .and_then(type_annotation_to_string)
1389        .or_else(|| infer_block_return_type_via_engine(&method.body))
1390        .unwrap_or_else(|| "unknown".to_string());
1391    let signature = format!(
1392        "{}({}): {}",
1393        method.name,
1394        param_names.join(", "),
1395        return_type
1396    );
1397
1398    let mut content = format!(
1399        "**Method**: `{}`\n\n**Target:** `{}`\n\n**Signature:**\n```shape\n{}\n```",
1400        method.name, target_type, signature
1401    );
1402    if let Some(comment) = program.docs.comment_for_span(method.span) {
1403        content.push_str(&format!(
1404            "\n\n{}",
1405            render_doc_comment(&program, comment, module_cache, current_file, None)
1406        ));
1407    }
1408
1409    Some(Hover {
1410        contents: HoverContents::Markup(MarkupContent {
1411            kind: MarkupKind::Markdown,
1412            value: content,
1413        }),
1414        range: None,
1415    })
1416}
1417
1418fn method_body_contains_offset(method: &shape_ast::ast::MethodDef, offset: usize) -> bool {
1419    method
1420        .body
1421        .iter()
1422        .any(|stmt| statement_contains_offset(stmt, offset))
1423}
1424
1425fn statement_contains_offset(stmt: &Statement, offset: usize) -> bool {
1426    match stmt {
1427        Statement::Return(_, span)
1428        | Statement::Break(span)
1429        | Statement::Continue(span)
1430        | Statement::VariableDecl(_, span)
1431        | Statement::Assignment(_, span)
1432        | Statement::Expression(_, span)
1433        | Statement::Extend(_, span)
1434        | Statement::RemoveTarget(span)
1435        | Statement::SetParamType { span, .. }
1436        | Statement::SetParamValue { span, .. }
1437        | Statement::SetReturnType { span, .. } => span_contains_offset(*span, offset),
1438        Statement::SetReturnExpr { span, .. } => span_contains_offset(*span, offset),
1439        Statement::ReplaceModuleExpr { span, .. } => span_contains_offset(*span, offset),
1440        Statement::ReplaceBodyExpr { span, .. } => span_contains_offset(*span, offset),
1441        Statement::ReplaceBody { body, span } => {
1442            span_contains_offset(*span, offset)
1443                || body
1444                    .iter()
1445                    .any(|nested| statement_contains_offset(nested, offset))
1446        }
1447        Statement::For(for_stmt, span) => {
1448            span_contains_offset(*span, offset)
1449                || for_stmt
1450                    .body
1451                    .iter()
1452                    .any(|nested| statement_contains_offset(nested, offset))
1453        }
1454        Statement::While(while_stmt, span) => {
1455            span_contains_offset(*span, offset)
1456                || while_stmt
1457                    .body
1458                    .iter()
1459                    .any(|nested| statement_contains_offset(nested, offset))
1460        }
1461        Statement::If(if_stmt, span) => {
1462            span_contains_offset(*span, offset)
1463                || if_stmt
1464                    .then_body
1465                    .iter()
1466                    .any(|nested| statement_contains_offset(nested, offset))
1467                || if_stmt.else_body.as_ref().is_some_and(|else_body| {
1468                    else_body
1469                        .iter()
1470                        .any(|nested| statement_contains_offset(nested, offset))
1471                })
1472        }
1473    }
1474}
1475
1476fn receiver_type_at_offset(program: &Program, offset: usize) -> Option<String> {
1477    let mut best: Option<(usize, String)> = None;
1478
1479    for item in &program.items {
1480        match item {
1481            Item::Impl(impl_block, span) if span_contains_offset(*span, offset) => {
1482                if !impl_block
1483                    .methods
1484                    .iter()
1485                    .any(|method| method_body_contains_offset(method, offset))
1486                {
1487                    continue;
1488                }
1489                let target_type = type_name_base_name(&impl_block.target_type);
1490                if target_type.is_empty() {
1491                    continue;
1492                }
1493                let len = span.len();
1494                if best
1495                    .as_ref()
1496                    .map(|(best_len, _)| len < *best_len)
1497                    .unwrap_or(true)
1498                {
1499                    best = Some((len, target_type));
1500                }
1501            }
1502            Item::Extend(extend_stmt, span) if span_contains_offset(*span, offset) => {
1503                if !extend_stmt
1504                    .methods
1505                    .iter()
1506                    .any(|method| method_body_contains_offset(method, offset))
1507                {
1508                    continue;
1509                }
1510                let target_type = type_name_base_name(&extend_stmt.type_name);
1511                if target_type.is_empty() {
1512                    continue;
1513                }
1514                let len = span.len();
1515                if best
1516                    .as_ref()
1517                    .map(|(best_len, _)| len < *best_len)
1518                    .unwrap_or(true)
1519                {
1520                    best = Some((len, target_type));
1521                }
1522            }
1523            _ => {}
1524        }
1525    }
1526
1527    best.map(|(_, ty)| ty)
1528}
1529
1530fn get_self_receiver_hover(text: &str, word: &str, position: Position) -> Option<Hover> {
1531    if word != "self" {
1532        return None;
1533    }
1534
1535    let offset = position_to_offset(text, position)?;
1536    let mut program = parse_with_fallback(text)?;
1537    shape_ast::transform::desugar_program(&mut program);
1538
1539    struct SelfUseFinder {
1540        offset: usize,
1541        found: bool,
1542    }
1543
1544    impl Visitor for SelfUseFinder {
1545        fn visit_expr(&mut self, expr: &Expr) -> bool {
1546            if let Expr::Identifier(name, span) = expr {
1547                if name == "self" && span_contains_offset(*span, self.offset) {
1548                    self.found = true;
1549                }
1550            }
1551            true
1552        }
1553    }
1554
1555    let mut finder = SelfUseFinder {
1556        offset,
1557        found: false,
1558    };
1559    walk_program(&mut finder, &program);
1560    if !finder.found && !is_inside_interpolation_expression(text, position) {
1561        return None;
1562    }
1563
1564    let receiver_type = receiver_type_at_offset(&program, offset)?;
1565    let content = format!(
1566        "**Variable**: `self`\n\n**Type:** `{}`\n\nImplicit method receiver.",
1567        receiver_type
1568    );
1569
1570    Some(Hover {
1571        contents: HoverContents::Markup(MarkupContent {
1572            kind: MarkupKind::Markdown,
1573            value: content,
1574        }),
1575        range: None,
1576    })
1577}
1578
1579fn get_interpolation_self_property_hover(
1580    text: &str,
1581    hovered_word: &str,
1582    position: Position,
1583) -> Option<Hover> {
1584    if !is_inside_interpolation_expression(text, position) {
1585        return None;
1586    }
1587
1588    let offset = position_to_offset(text, position)?;
1589    if !is_hovering_self_property(text, offset, hovered_word) {
1590        return None;
1591    }
1592
1593    let mut program = parse_with_fallback(text)?;
1594    shape_ast::transform::desugar_program(&mut program);
1595
1596    let receiver_type = receiver_type_at_offset(&program, offset)?;
1597    let field_type = extract_struct_fields(&program)
1598        .get(&receiver_type)
1599        .and_then(|fields| {
1600            fields
1601                .iter()
1602                .find(|(name, _)| name == hovered_word)
1603                .map(|(_, ty)| ty.clone())
1604        })
1605        .unwrap_or_else(|| "unknown".to_string());
1606
1607    Some(Hover {
1608        contents: HoverContents::Markup(MarkupContent {
1609            kind: MarkupKind::Markdown,
1610            value: format!(
1611                "**Property**: `{}`\n\n**Type:** `{}`\n\n**Receiver:** `{}`",
1612                hovered_word, field_type, receiver_type
1613            ),
1614        }),
1615        range: None,
1616    })
1617}
1618
1619fn is_hovering_self_property(text: &str, offset: usize, hovered_word: &str) -> bool {
1620    let bytes = text.as_bytes();
1621    if bytes.is_empty() || offset > bytes.len() {
1622        return false;
1623    }
1624
1625    let mut start = offset;
1626    while start > 0 {
1627        let ch = bytes[start - 1];
1628        if (ch as char).is_ascii_alphanumeric() || ch == b'_' {
1629            start -= 1;
1630        } else {
1631            break;
1632        }
1633    }
1634
1635    let mut end = offset;
1636    while end < bytes.len() {
1637        let ch = bytes[end];
1638        if (ch as char).is_ascii_alphanumeric() || ch == b'_' {
1639            end += 1;
1640        } else {
1641            break;
1642        }
1643    }
1644
1645    if start >= end {
1646        return false;
1647    }
1648
1649    if text.get(start..end) != Some(hovered_word) {
1650        return false;
1651    }
1652
1653    if start < 5 {
1654        return false;
1655    }
1656
1657    let self_start = start - 5;
1658    if text.get(self_start..start) != Some("self.") {
1659        return false;
1660    }
1661
1662    if self_start == 0 {
1663        return true;
1664    }
1665
1666    let prev = bytes[self_start - 1];
1667    !((prev as char).is_ascii_alphanumeric() || prev == b'_')
1668}
1669
1670fn get_impl_header_trait_hover(
1671    text: &str,
1672    word: &str,
1673    position: Position,
1674    module_cache: Option<&ModuleCache>,
1675    current_file: Option<&Path>,
1676) -> Option<Hover> {
1677    let offset = position_to_offset(text, position)?;
1678    let program = parse_with_fallback(text)?;
1679
1680    let mut selected_impl: Option<(&shape_ast::ast::ImplBlock, Span)> = None;
1681    for item in &program.items {
1682        let Item::Impl(impl_block, span) = item else {
1683            continue;
1684        };
1685        if !span_contains_offset(*span, offset) {
1686            continue;
1687        }
1688
1689        let trait_name = type_name_base_name(&impl_block.trait_name);
1690        if trait_name != word {
1691            continue;
1692        }
1693
1694        if selected_impl
1695            .map(|(_, current_span)| span.len() < current_span.len())
1696            .unwrap_or(true)
1697        {
1698            selected_impl = Some((impl_block, *span));
1699        }
1700    }
1701
1702    let (impl_block, _) = selected_impl?;
1703    let trait_name = type_name_base_name(&impl_block.trait_name);
1704    let target_type = type_name_base_name(&impl_block.target_type);
1705
1706    let resolved =
1707        resolve_trait_definition(&program, &trait_name, module_cache, current_file, None);
1708
1709    let mut content = format!(
1710        "**Trait**: `{}`\n\n**Target:** `{}`",
1711        trait_name, target_type
1712    );
1713    if let Some(resolved_trait) = resolved {
1714        if let Some(doc) = &resolved_trait.documentation {
1715            content.push_str(&format!("\n\n{}", doc));
1716        }
1717
1718        if let Some(import_path) = &resolved_trait.import_path {
1719            content.push_str(&format!("\n\n**Resolved from:** `{}`", import_path));
1720        } else {
1721            content.push_str("\n\nResolved from current file.");
1722        }
1723
1724        let signatures = trait_member_signatures(&resolved_trait.trait_def);
1725        if !signatures.is_empty() {
1726            content.push_str("\n\n**Members:**\n```shape\n");
1727            for sig in signatures {
1728                content.push_str(&sig);
1729                content.push('\n');
1730            }
1731            content.push_str("```");
1732        }
1733    } else {
1734        content.push_str("\n\nTrait definition not found in current module context.");
1735    }
1736
1737    if let Some(impl_name) = &impl_block.impl_name {
1738        content.push_str(&format!("\n\n**Implementation:** `{}`", impl_name));
1739    }
1740
1741    Some(Hover {
1742        contents: HoverContents::Markup(MarkupContent {
1743            kind: MarkupKind::Markdown,
1744            value: content,
1745        }),
1746        range: None,
1747    })
1748}
1749
1750fn trait_member_signatures(trait_def: &shape_ast::ast::TraitDef) -> Vec<String> {
1751    let mut signatures = Vec::new();
1752
1753    for member in &trait_def.members {
1754        match member {
1755            shape_ast::ast::TraitMember::Required(shape_ast::ast::InterfaceMember::Method {
1756                name,
1757                params,
1758                return_type,
1759                ..
1760            }) => {
1761                let param_names: Vec<String> = params
1762                    .iter()
1763                    .map(|p| {
1764                        let pname = p.name.clone().unwrap_or_else(|| "_".to_string());
1765                        let ptype = type_annotation_to_string(&p.type_annotation)
1766                            .unwrap_or_else(|| "unknown".to_string());
1767                        format!("{}: {}", pname, ptype)
1768                    })
1769                    .collect();
1770                let return_type_str =
1771                    type_annotation_to_string(return_type).unwrap_or_else(|| "unknown".to_string());
1772                signatures.push(format!(
1773                    "{}({}): {}",
1774                    name,
1775                    param_names.join(", "),
1776                    return_type_str
1777                ));
1778            }
1779            shape_ast::ast::TraitMember::Default(method_def) => {
1780                let param_names: Vec<String> = method_def
1781                    .params
1782                    .iter()
1783                    .map(|p| p.simple_name().unwrap_or("_").to_string())
1784                    .collect();
1785                let return_type_str = method_def
1786                    .return_type
1787                    .as_ref()
1788                    .and_then(type_annotation_to_string)
1789                    .unwrap_or_else(|| "unknown".to_string());
1790                signatures.push(format!(
1791                    "{}({}): {}",
1792                    method_def.name,
1793                    param_names.join(", "),
1794                    return_type_str
1795                ));
1796            }
1797            _ => {}
1798        }
1799    }
1800
1801    signatures
1802}
1803
1804/// Find the 0-based line number where a symbol is defined in the source text.
1805fn type_name_base_name(type_name: &TypeName) -> String {
1806    match type_name {
1807        TypeName::Simple(name) => name.to_string(),
1808        TypeName::Generic { name, .. } => name.to_string(),
1809    }
1810}
1811
1812/// Get hover for user-defined symbols
1813#[cfg(test)]
1814fn get_user_symbol_hover(text: &str, word: &str) -> Option<Hover> {
1815    let mut program = parse_with_fallback(text)?;
1816    // Desugar query syntax before analysis
1817    shape_ast::transform::desugar_program(&mut program);
1818    get_user_symbol_hover_from_program(text, &program, word, None)
1819}
1820
1821/// Get hover for user-defined symbols with scope-aware resolution at cursor position.
1822fn get_user_symbol_hover_at(text: &str, word: &str, position: Position) -> Option<Hover> {
1823    let mut program = parse_with_fallback(text)?;
1824    // Desugar query syntax before analysis
1825    shape_ast::transform::desugar_program(&mut program);
1826
1827    let offset = position_to_offset(text, position)?;
1828    if let Some(hover) = get_scoped_binding_hover(&program, text, word, offset) {
1829        return Some(hover);
1830    }
1831
1832    get_user_symbol_hover_from_program(text, &program, word, Some(offset))
1833}
1834
1835fn get_scoped_binding_hover(
1836    program: &Program,
1837    text: &str,
1838    word: &str,
1839    offset: usize,
1840) -> Option<Hover> {
1841    let scope_tree = ScopeTree::build(program, text);
1842    let binding = scope_tree.binding_at(offset)?;
1843    if binding.name != word {
1844        return None;
1845    }
1846    get_function_param_hover(program, binding.def_span, &binding.name)
1847}
1848
1849fn get_function_param_hover(
1850    program: &Program,
1851    def_span: (usize, usize),
1852    name: &str,
1853) -> Option<Hover> {
1854    let function_sigs = infer_function_signatures(program);
1855    for item in &program.items {
1856        let (params, func_name): (&[shape_ast::ast::FunctionParameter], &str) = match item {
1857            Item::Function(func, _) => (&func.params, &func.name),
1858            Item::ForeignFunction(foreign_fn, _) => (&foreign_fn.params, &foreign_fn.name),
1859            _ => continue,
1860        };
1861
1862        for param in params {
1863            let param_span = param.span();
1864            if param_span.is_dummy()
1865                || param_span.start != def_span.0
1866                || param_span.end != def_span.1
1867            {
1868                continue;
1869            }
1870
1871            let Some(param_name) = param.simple_name() else {
1872                continue;
1873            };
1874            if param_name != name {
1875                continue;
1876            }
1877
1878            let type_name = param
1879                .type_annotation
1880                .as_ref()
1881                .and_then(type_annotation_to_string)
1882                .or_else(|| {
1883                    function_sigs.get(func_name).and_then(|info| {
1884                        info.param_types
1885                            .iter()
1886                            .find(|(param, _)| param == param_name)
1887                            .map(|(_, ty)| ty.clone())
1888                    })
1889                });
1890            let ref_mode = function_sigs
1891                .get(func_name)
1892                .and_then(|info| info.param_ref_modes.get(param_name));
1893
1894            let mut content = format!("**Variable**: `{}`", param_name);
1895            if let Some(type_name) = type_name {
1896                let display_type = format_reference_aware_type(&type_name, ref_mode);
1897                content.push_str(&format!("\n\n**Type:** `{}`", display_type));
1898            }
1899
1900            return Some(Hover {
1901                contents: HoverContents::Markup(MarkupContent {
1902                    kind: MarkupKind::Markdown,
1903                    value: content,
1904                }),
1905                range: None,
1906            });
1907        }
1908    }
1909
1910    None
1911}
1912
1913fn get_user_symbol_hover_from_program(
1914    _text: &str,
1915    program: &Program,
1916    word: &str,
1917    cursor_offset: Option<usize>,
1918) -> Option<Hover> {
1919    // Parse the document to extract symbols
1920    let symbols = extract_symbols(program);
1921
1922    // Find the symbol
1923    let symbol = symbols.iter().find(|s| s.name == word)?;
1924
1925    let kind_name = match symbol.kind {
1926        SymbolKind::Variable => "Variable",
1927        SymbolKind::Constant => "Constant",
1928        SymbolKind::Function => "Function",
1929        SymbolKind::Type => "Type",
1930    };
1931
1932    let mut content = format!("**{}**: `{}`", kind_name, symbol.name);
1933
1934    // Run program-level type inference for best results
1935    let program_types = infer_program_types(program);
1936    let function_sigs = infer_function_signatures(program);
1937
1938    // Show type annotation for variables/constants
1939    // Priority: explicit annotation > engine-inferred > heuristic
1940    let type_str = if let Some(type_ann) = &symbol.type_annotation {
1941        Some(type_ann.clone())
1942    } else if matches!(symbol.kind, SymbolKind::Variable | SymbolKind::Constant) {
1943        if let Some(offset) = cursor_offset {
1944            infer_variable_type_for_display(program, word, offset).or_else(|| {
1945                choose_best_variable_type(
1946                    program_types.get(word).cloned(),
1947                    infer_variable_type(program, word),
1948                )
1949            })
1950        } else {
1951            choose_best_variable_type(
1952                program_types.get(word).cloned(),
1953                infer_variable_type(program, word),
1954            )
1955        }
1956    } else {
1957        None
1958    };
1959
1960    if let Some(type_ann) = type_str {
1961        content.push_str(&format!("\n\n**Type:** `{}`", type_ann));
1962    }
1963
1964    if symbol.kind == SymbolKind::Type {
1965        let struct_fields = extract_struct_fields(program);
1966        if let Some(fields) = struct_fields.get(word) {
1967            if !fields.is_empty() {
1968                let shape = fields
1969                    .iter()
1970                    .map(|(name, ty)| format!("{}: {}", name, ty))
1971                    .collect::<Vec<_>>()
1972                    .join(", ");
1973                content.push_str(&format!("\n\n**Shape:** `{{ {} }}`", shape));
1974            }
1975        }
1976    }
1977
1978    // Show annotations generically
1979    if !symbol.annotations.is_empty() {
1980        content.push_str("\n\n**Annotations:**\n");
1981        for ann in &symbol.annotations {
1982            content.push_str(&format!("- `@{}`\n", ann));
1983        }
1984    }
1985
1986    // Show signature for functions with inferred types
1987    if matches!(symbol.kind, SymbolKind::Function) {
1988        if let Some(sig_info) = function_sigs.get(word) {
1989            // Build an enhanced signature with inferred return type
1990            let sig = build_function_signature_from_inference(
1991                program,
1992                word,
1993                sig_info,
1994                symbol.detail.as_deref(),
1995            );
1996            if let Some(sig) = sig {
1997                content.push_str(&format!("\n\n**Signature:**\n```shape\n{}\n```", sig));
1998            }
1999        } else if let Some(detail) = &symbol.detail {
2000            content.push_str(&format!("\n\n**Signature:**\n```shape\n{}\n```", detail));
2001        }
2002    }
2003
2004    let doc = symbol.documentation.clone();
2005    if let Some(doc) = doc {
2006        content.push_str(&format!("\n\n---\n\n{}", doc));
2007    }
2008
2009    Some(Hover {
2010        contents: HoverContents::Markup(MarkupContent {
2011            kind: MarkupKind::Markdown,
2012            value: content,
2013        }),
2014        range: None,
2015    })
2016}
2017
2018fn choose_best_variable_type(primary: Option<String>, secondary: Option<String>) -> Option<String> {
2019    match (primary, secondary) {
2020        (Some(primary), Some(secondary)) => {
2021            if should_prefer_secondary_type(&primary, &secondary) {
2022                Some(secondary)
2023            } else {
2024                Some(primary)
2025            }
2026        }
2027        (Some(primary), None) => Some(primary),
2028        (None, secondary) => secondary,
2029    }
2030}
2031
2032fn should_prefer_secondary_type(primary: &str, secondary: &str) -> bool {
2033    let primary = primary.trim();
2034    let secondary = secondary.trim();
2035    if (primary.eq_ignore_ascii_case("object") && secondary.starts_with('{'))
2036        || primary == "unknown"
2037    {
2038        return true;
2039    }
2040
2041    if primary.starts_with('{') && secondary.starts_with('{') {
2042        let primary_len = parse_object_shape_fields(primary)
2043            .map(|fields| fields.len())
2044            .unwrap_or(0);
2045        let secondary_len = parse_object_shape_fields(secondary)
2046            .map(|fields| fields.len())
2047            .unwrap_or(0);
2048        return secondary_len > primary_len;
2049    }
2050
2051    false
2052}
2053
2054fn is_primitive_value_type_name(name: &str) -> bool {
2055    let normalized = name.trim().trim_end_matches('?');
2056    matches!(
2057        normalized,
2058        "int"
2059            | "integer"
2060            | "i64"
2061            | "number"
2062            | "float"
2063            | "f64"
2064            | "decimal"
2065            | "bool"
2066            | "boolean"
2067            | "()"
2068            | "void"
2069            | "unit"
2070            | "none"
2071            | "null"
2072            | "undefined"
2073            | "never"
2074    )
2075}
2076
2077fn split_top_level_union(type_str: &str) -> Vec<String> {
2078    let mut parts = Vec::new();
2079    let mut start = 0usize;
2080    let mut paren_depth = 0usize;
2081    let mut bracket_depth = 0usize;
2082    let mut brace_depth = 0usize;
2083    let mut angle_depth = 0usize;
2084
2085    for (idx, ch) in type_str.char_indices() {
2086        match ch {
2087            '(' => paren_depth += 1,
2088            ')' => paren_depth = paren_depth.saturating_sub(1),
2089            '[' => bracket_depth += 1,
2090            ']' => bracket_depth = bracket_depth.saturating_sub(1),
2091            '{' => brace_depth += 1,
2092            '}' => brace_depth = brace_depth.saturating_sub(1),
2093            '<' => angle_depth += 1,
2094            '>' => angle_depth = angle_depth.saturating_sub(1),
2095            _ => {}
2096        }
2097
2098        if ch == '|'
2099            && paren_depth == 0
2100            && bracket_depth == 0
2101            && brace_depth == 0
2102            && angle_depth == 0
2103        {
2104            parts.push(type_str[start..idx].trim().to_string());
2105            start = idx + ch.len_utf8();
2106        }
2107    }
2108
2109    parts.push(type_str[start..].trim().to_string());
2110    parts.into_iter().filter(|part| !part.is_empty()).collect()
2111}
2112
2113fn apply_ref_prefix(type_str: &str, mode: &ParamReferenceMode) -> String {
2114    let trimmed = type_str.trim();
2115    if trimmed.starts_with('&') {
2116        trimmed.to_string()
2117    } else {
2118        format!("{}{}", mode.prefix(), trimmed)
2119    }
2120}
2121
2122fn format_reference_aware_type(type_str: &str, mode: Option<&ParamReferenceMode>) -> String {
2123    let Some(mode) = mode else {
2124        return type_str.to_string();
2125    };
2126
2127    let union_parts = split_top_level_union(type_str);
2128    if union_parts.len() <= 1 {
2129        return apply_ref_prefix(type_str, mode);
2130    }
2131
2132    union_parts
2133        .into_iter()
2134        .map(|part| {
2135            if is_primitive_value_type_name(&part) {
2136                part
2137            } else {
2138                apply_ref_prefix(&part, mode)
2139            }
2140        })
2141        .collect::<Vec<_>>()
2142        .join(" | ")
2143}
2144
2145/// Build an enhanced function signature using inferred type information.
2146///
2147/// Combines the function's AST definition with engine-inferred parameter/return types.
2148fn build_function_signature_from_inference(
2149    program: &Program,
2150    func_name: &str,
2151    sig_info: &FunctionTypeInfo,
2152    _fallback_detail: Option<&str>,
2153) -> Option<String> {
2154    // Find the function definition in the AST — regular or foreign
2155    enum FuncKind<'a> {
2156        Regular(&'a shape_ast::ast::FunctionDef),
2157        Foreign(&'a shape_ast::ast::ForeignFunctionDef),
2158    }
2159
2160    let func_kind = program.items.iter().find_map(|item| match item {
2161        Item::Function(f, _) if f.name == func_name => Some(FuncKind::Regular(f)),
2162        Item::ForeignFunction(f, _) if f.name == func_name => Some(FuncKind::Foreign(f)),
2163        _ => None,
2164    })?;
2165
2166    let ast_params = match &func_kind {
2167        FuncKind::Regular(f) => f.params.as_slice(),
2168        FuncKind::Foreign(f) => f.params.as_slice(),
2169    };
2170
2171    // Build parameter list with inferred types where available
2172    let params: Vec<String> = ast_params
2173        .iter()
2174        .map(|p| {
2175            let name = p.simple_name().unwrap_or("_");
2176            let ref_mode = sig_info.param_ref_modes.get(name);
2177            if let Some(type_ann) = &p.type_annotation {
2178                let type_str =
2179                    type_annotation_to_string(type_ann).unwrap_or_else(|| "_".to_string());
2180                let display_type = format_reference_aware_type(&type_str, ref_mode);
2181                format!("{}: {}", name, display_type)
2182            } else if let Some((_, inferred)) = sig_info.param_types.iter().find(|(n, _)| n == name)
2183            {
2184                let display_type = format_reference_aware_type(inferred, ref_mode);
2185                format!("{}: {}", name, display_type)
2186            } else if let Some(ref_mode) = ref_mode {
2187                format!("{}: {}unknown", name, ref_mode.prefix())
2188            } else {
2189                name.to_string()
2190            }
2191        })
2192        .collect();
2193
2194    // Build return type string
2195    let return_str = match &func_kind {
2196        FuncKind::Foreign(f) => f.return_type.as_ref().and_then(type_annotation_to_string),
2197        FuncKind::Regular(f) => {
2198            if let Some(ref rt) = f.return_type {
2199                type_annotation_to_string(rt)
2200            } else {
2201                sig_info.return_type.clone()
2202            }
2203        }
2204    };
2205
2206    let mut sig = match &func_kind {
2207        FuncKind::Regular(_) => format!("fn {}({})", func_name, params.join(", ")),
2208        FuncKind::Foreign(f) => format!("fn {} {}({})", f.language, func_name, params.join(", ")),
2209    };
2210    if let Some(ret) = return_str {
2211        let display = crate::type_inference::simplify_result_type(&ret);
2212        sig.push_str(&format!(" -> {}", display));
2213    }
2214
2215    Some(sig)
2216}
2217
2218/// Get hover for symbols imported from other modules
2219fn get_imported_symbol_hover(
2220    text: &str,
2221    word: &str,
2222    module_cache: &ModuleCache,
2223    current_file: &Path,
2224) -> Option<Hover> {
2225    use crate::module_cache::SymbolKind as ModSymbolKind;
2226    use shape_ast::ast::{EnumMemberKind, ExportItem};
2227
2228    // Parse the current file to find import statements
2229    let program = parse_with_fallback(text)?;
2230
2231    for item in &program.items {
2232        if let Item::Import(import_stmt, _) = item {
2233            let resolved = module_cache.resolve_import(&import_stmt.from, current_file, None)?;
2234            let module_info =
2235                module_cache.load_module_with_context(&resolved, current_file, None)?;
2236
2237            // Check if the word matches any exported symbol from self module
2238            for export in &module_info.exports {
2239                if export.exported_name() != word {
2240                    continue;
2241                }
2242
2243                // Found a match — build hover content based on kind
2244                let content = match export.kind {
2245                    ModSymbolKind::Enum => {
2246                        // Find the enum definition in the module's AST for details
2247                        let mut detail = format!(
2248                            "**Enum**: `{}`\n\n*Imported from `{}`*",
2249                            word, import_stmt.from
2250                        );
2251                        for module_item in module_info.program.items.iter() {
2252                            let enum_def = match module_item {
2253                                Item::Export(e, _) => {
2254                                    if let ExportItem::Enum(ed) = &e.item {
2255                                        Some(ed)
2256                                    } else {
2257                                        None
2258                                    }
2259                                }
2260                                Item::Enum(ed, _) => Some(ed),
2261                                _ => None,
2262                            };
2263                            if let Some(ed) = enum_def {
2264                                if ed.name == word {
2265                                    detail.push_str("\n\n**Variants:**\n```shape\nenum ");
2266                                    detail.push_str(&ed.name);
2267                                    detail.push_str(" {\n");
2268                                    for m in &ed.members {
2269                                        detail.push_str("    ");
2270                                        detail.push_str(&m.name);
2271                                        match &m.kind {
2272                                            EnumMemberKind::Unit { .. } => {}
2273                                            EnumMemberKind::Tuple(types) => {
2274                                                detail.push('(');
2275                                                let type_strs: Vec<String> = types
2276                                                    .iter()
2277                                                    .map(|t| format!("{:?}", t))
2278                                                    .collect();
2279                                                detail.push_str(&type_strs.join(", "));
2280                                                detail.push(')');
2281                                            }
2282                                            EnumMemberKind::Struct(fields) => {
2283                                                detail.push_str(" { ");
2284                                                let field_strs: Vec<String> = fields
2285                                                    .iter()
2286                                                    .map(|f| {
2287                                                        format!(
2288                                                            "{}: {:?}",
2289                                                            f.name, f.type_annotation
2290                                                        )
2291                                                    })
2292                                                    .collect();
2293                                                detail.push_str(&field_strs.join(", "));
2294                                                detail.push_str(" }");
2295                                            }
2296                                        }
2297                                        detail.push_str(",\n");
2298                                    }
2299                                    detail.push_str("}\n```");
2300                                    break;
2301                                }
2302                            }
2303                        }
2304                        detail
2305                    }
2306                    ModSymbolKind::Function => {
2307                        // Find function signature
2308                        let mut detail = format!(
2309                            "**Function**: `{}`\n\n*Imported from `{}`*",
2310                            word, import_stmt.from
2311                        );
2312                        for module_item in module_info.program.items.iter() {
2313                            let func_def = match module_item {
2314                                Item::Export(e, _) => {
2315                                    if let ExportItem::Function(fd) = &e.item {
2316                                        Some(fd)
2317                                    } else {
2318                                        None
2319                                    }
2320                                }
2321                                Item::Function(fd, _) => Some(fd),
2322                                _ => None,
2323                            };
2324                            if let Some(fd) = func_def {
2325                                if fd.name == word {
2326                                    let params: Vec<String> = fd
2327                                        .params
2328                                        .iter()
2329                                        .map(|p| {
2330                                            let name = p.simple_name().unwrap_or("_");
2331                                            if let Some(ref ty) = p.type_annotation {
2332                                                format!("{}: {:?}", name, ty)
2333                                            } else {
2334                                                name.to_string()
2335                                            }
2336                                        })
2337                                        .collect();
2338                                    detail.push_str(&format!(
2339                                        "\n\n**Signature:**\n```shape\nfn {}({})",
2340                                        word,
2341                                        params.join(", ")
2342                                    ));
2343                                    if let Some(ref rt) = fd.return_type {
2344                                        detail.push_str(&format!(": {:?}", rt));
2345                                    }
2346                                    detail.push_str("\n```");
2347                                    break;
2348                                }
2349                            }
2350                        }
2351                        detail
2352                    }
2353                    _ => {
2354                        format!(
2355                            "**{}**: `{}`\n\n*Imported from `{}`*",
2356                            match export.kind {
2357                                ModSymbolKind::Variable => "Variable",
2358                                ModSymbolKind::TypeAlias => "Type",
2359                                ModSymbolKind::Interface => "Interface",
2360                                ModSymbolKind::Pattern => "Pattern",
2361                                ModSymbolKind::Annotation => "Annotation",
2362                                _ => "Symbol",
2363                            },
2364                            word,
2365                            import_stmt.from
2366                        )
2367                    }
2368                };
2369
2370                let mut full_content = content;
2371                if let Some(doc) =
2372                    module_info
2373                        .program
2374                        .docs
2375                        .comment_for_span(export.span)
2376                        .map(|comment| {
2377                            render_doc_comment(
2378                                &module_info.program,
2379                                comment,
2380                                Some(module_cache),
2381                                Some(&module_info.path),
2382                                None,
2383                            )
2384                        })
2385                {
2386                    full_content.push_str(&format!("\n\n---\n\n{}", doc));
2387                }
2388
2389                return Some(Hover {
2390                    contents: HoverContents::Markup(MarkupContent {
2391                        kind: MarkupKind::Markdown,
2392                        value: full_content,
2393                    }),
2394                    range: None,
2395                });
2396            }
2397        }
2398    }
2399
2400    None
2401}
2402
2403/// Get hover for a module name (extension module or local `mod`).
2404/// Get hover for DateTime / io / time namespaces
2405fn get_namespace_api_hover(word: &str) -> Option<Hover> {
2406    let doc = match word {
2407        "DateTime" => {
2408            "**DateTime API**\n\n\
2409             Static constructors for creating date/time values.\n\n\
2410             **Constructors:**\n\
2411             - `DateTime.now()` — Current local time\n\
2412             - `DateTime.utc()` — Current UTC time\n\
2413             - `DateTime.parse(string)` — Parse from ISO 8601, RFC 2822, or common formats\n\
2414             - `DateTime.from_epoch(ms)` — From milliseconds since Unix epoch\n\
2415             - `DateTime.from_parts(year, month, day, hour?, minute?, second?)` — Construct from components (UTC)\n\
2416             - `DateTime.from_unix_secs(secs)` — From seconds since Unix epoch\n\n\
2417             **Instance Methods:**\n\
2418             - `.year()`, `.month()`, `.day()`, `.hour()`, `.minute()`, `.second()`\n\
2419             - `.day_of_week()`, `.day_of_year()`, `.week_of_year()`\n\
2420             - `.is_weekday()`, `.is_weekend()`\n\
2421             - `.format(pattern)`, `.iso8601()`, `.rfc2822()`, `.unix_timestamp()`, `.to_unix_millis()`\n\
2422             - `.to_utc()`, `.to_timezone(tz)`, `.to_local()`, `.timezone()`, `.offset()`\n\
2423             - `.add_days(n)`, `.add_hours(n)`, `.add_minutes(n)`, `.add_seconds(n)`, `.add_months(n)`\n\
2424             - `.is_before(other)`, `.is_after(other)`, `.is_same_day(other)`\n\
2425             - `.diff(other)` — Difference as a map with days, hours, minutes, seconds, milliseconds, total_milliseconds"
2426        }
2427        "io" => {
2428            "**io Module**\n\n\
2429             File system, network, and process operations.\n\n\
2430             **File Operations:**\n\
2431             - `io.open(path, mode?)` — Open a file (`\"r\"`, `\"w\"`, `\"a\"`, `\"rw\"`)\n\
2432             - `io.read(handle, n?)`, `io.read_to_string(handle)`, `io.read_bytes(handle, n?)`\n\
2433             - `io.write(handle, data)`, `io.flush(handle)`, `io.close(handle)`\n\
2434             - `io.exists(path)`, `io.stat(path)`, `io.mkdir(path)`, `io.remove(path)`, `io.rename(from, to)`\n\
2435             - `io.read_dir(path)`\n\n\
2436             **Path Operations:**\n\
2437             - `io.join(base, path)`, `io.dirname(path)`, `io.basename(path)`\n\
2438             - `io.extension(path)`, `io.resolve(path)`\n\n\
2439             **Network:**\n\
2440             - `io.tcp_connect(addr)`, `io.tcp_listen(addr)`, `io.tcp_accept(listener)`\n\
2441             - `io.tcp_read(handle)`, `io.tcp_write(handle, data)`, `io.tcp_close(handle)`\n\
2442             - `io.udp_bind(addr)`, `io.udp_send(handle, data, addr)`, `io.udp_recv(handle)`\n\n\
2443             **Process:**\n\
2444             - `io.spawn(program, args?)`, `io.exec(program, args?)`\n\
2445             - `io.stdin()`, `io.stdout()`, `io.stderr()`, `io.read_line(handle?)`"
2446        }
2447        "time" => {
2448            "**time Module**\n\n\
2449             Precision timing utilities.\n\n\
2450             **Functions:**\n\
2451             - `time.now()` — Current monotonic instant for measuring elapsed time\n\
2452             - `time.sleep(ms)` — Sleep for ms milliseconds (async)\n\
2453             - `time.sleep_sync(ms)` — Sleep for ms milliseconds (blocking)\n\
2454             - `time.benchmark(fn, iterations?)` — Benchmark a function\n\
2455             - `time.stopwatch()` — Start a stopwatch (returns Instant)\n\
2456             - `time.millis()` — Current wall-clock time as epoch milliseconds"
2457        }
2458        _ => return None,
2459    };
2460
2461    Some(Hover {
2462        contents: HoverContents::Markup(MarkupContent {
2463            kind: MarkupKind::Markdown,
2464            value: doc.to_string(),
2465        }),
2466        range: None,
2467    })
2468}
2469
2470/// Get hover for DateTime / io / time member access (e.g., DateTime.now, io.open, time.sleep)
2471fn get_namespace_member_hover(object: &str, member: &str) -> Option<Hover> {
2472    let doc = match (object, member) {
2473        // DateTime constructors
2474        ("DateTime", "now") => {
2475            "**DateTime.now**(): DateTime\n\nReturn the current local time as a DateTime value.\n\n```shape\nlet now = DateTime.now()\nprint(now.format(\"%Y-%m-%d %H:%M\"))\n```"
2476        }
2477        ("DateTime", "utc") => {
2478            "**DateTime.utc**(): DateTime\n\nReturn the current UTC time.\n\n```shape\nlet utc = DateTime.utc()\n```"
2479        }
2480        ("DateTime", "parse") => {
2481            "**DateTime.parse**(string): DateTime\n\nParse a date/time string. Supports ISO 8601, RFC 2822, and common formats.\n\n```shape\nlet dt = DateTime.parse(\"2024-03-15T10:30:00Z\")\nlet dt2 = DateTime.parse(\"Mar 15, 2024 10:30 AM\")\n```"
2482        }
2483        ("DateTime", "from_epoch") => {
2484            "**DateTime.from_epoch**(ms: number): DateTime\n\nCreate a DateTime from milliseconds since Unix epoch.\n\n```shape\nlet dt = DateTime.from_epoch(1710500000000)\n```"
2485        }
2486        ("DateTime", "from_parts") => {
2487            "**DateTime.from_parts**(year: int, month: int, day: int, hour?: int, minute?: int, second?: int): DateTime\n\nCreate a DateTime from individual components at UTC. Hour, minute, and second default to 0.\n\n```shape\nlet dt = DateTime.from_parts(2024, 3, 15, 14, 30, 0)\nlet date = DateTime.from_parts(2024, 1, 1)\n```"
2488        }
2489        ("DateTime", "from_unix_secs") => {
2490            "**DateTime.from_unix_secs**(secs: int): DateTime\n\nCreate a DateTime from seconds since Unix epoch.\n\n```shape\nlet dt = DateTime.from_unix_secs(1705314600)\n```"
2491        }
2492
2493        // io file operations
2494        ("io", "open") => {
2495            "**io.open**(path: string, mode?: string): IoHandle\n\nOpen a file and return a handle.\n\nModes: `\"r\"` (read, default), `\"w\"` (write/create), `\"a\"` (append), `\"rw\"` (read-write).\n\n```shape\nlet f = io.open(\"data.csv\")\nlet f = io.open(\"output.txt\", \"w\")\n```"
2496        }
2497        ("io", "read") => {
2498            "**io.read**(handle: IoHandle, n?: int): string\n\nRead from a file handle. If `n` is given, read up to `n` bytes; otherwise read all."
2499        }
2500        ("io", "read_to_string") => {
2501            "**io.read_to_string**(handle: IoHandle): string\n\nRead the entire file contents as a string."
2502        }
2503        ("io", "write") => {
2504            "**io.write**(handle: IoHandle, data: string): unit\n\nWrite a string to a file handle."
2505        }
2506        ("io", "close") => {
2507            "**io.close**(handle: IoHandle): unit\n\nClose a file handle, releasing the resource."
2508        }
2509        ("io", "flush") => "**io.flush**(handle: IoHandle): unit\n\nFlush buffered writes to disk.",
2510        ("io", "exists") => {
2511            "**io.exists**(path: string): bool\n\nCheck if a file or directory exists at the given path."
2512        }
2513        ("io", "stat") => {
2514            "**io.stat**(path: string): object\n\nGet file metadata: `{ size, modified, is_dir, is_file }`."
2515        }
2516        ("io", "mkdir") => {
2517            "**io.mkdir**(path: string): unit\n\nCreate a directory (and any missing parent directories)."
2518        }
2519        ("io", "remove") => {
2520            "**io.remove**(path: string): unit\n\nRemove a file or empty directory."
2521        }
2522        ("io", "rename") => {
2523            "**io.rename**(from: string, to: string): unit\n\nRename or move a file or directory."
2524        }
2525        ("io", "read_dir") => {
2526            "**io.read_dir**(path: string): Array<object>\n\nList directory entries as objects with `name`, `path`, `is_dir`, `is_file`."
2527        }
2528        ("io", "join") => {
2529            "**io.join**(base: string, path: string): string\n\nJoin two path components."
2530        }
2531        ("io", "dirname") => {
2532            "**io.dirname**(path: string): string\n\nGet the parent directory of a path."
2533        }
2534        ("io", "basename") => {
2535            "**io.basename**(path: string): string\n\nGet the file name component of a path."
2536        }
2537        ("io", "extension") => {
2538            "**io.extension**(path: string): string\n\nGet the file extension (without the dot)."
2539        }
2540        ("io", "resolve") => {
2541            "**io.resolve**(path: string): string\n\nResolve a path to an absolute path."
2542        }
2543        ("io", "tcp_connect") => {
2544            "**io.tcp_connect**(addr: string): IoHandle\n\nConnect to a TCP server at `addr` (e.g., `\"127.0.0.1:8080\"`)."
2545        }
2546        ("io", "tcp_listen") => {
2547            "**io.tcp_listen**(addr: string): IoHandle\n\nBind a TCP listener on `addr`."
2548        }
2549        ("io", "tcp_accept") => {
2550            "**io.tcp_accept**(listener: IoHandle): IoHandle\n\nAccept a new TCP connection from a listener."
2551        }
2552        ("io", "tcp_read") => {
2553            "**io.tcp_read**(handle: IoHandle): string\n\nRead from a TCP stream."
2554        }
2555        ("io", "tcp_write") => {
2556            "**io.tcp_write**(handle: IoHandle, data: string): unit\n\nWrite to a TCP stream."
2557        }
2558        ("io", "tcp_close") => {
2559            "**io.tcp_close**(handle: IoHandle): unit\n\nClose a TCP connection."
2560        }
2561        ("io", "udp_bind") => {
2562            "**io.udp_bind**(addr: string): IoHandle\n\nBind a UDP socket on `addr`."
2563        }
2564        ("io", "udp_send") => {
2565            "**io.udp_send**(handle: IoHandle, data: string, addr: string): unit\n\nSend a UDP datagram."
2566        }
2567        ("io", "udp_recv") => {
2568            "**io.udp_recv**(handle: IoHandle): object\n\nReceive a UDP datagram, returning `{ data, addr }`."
2569        }
2570        ("io", "spawn") => {
2571            "**io.spawn**(program: string, args?: Array<string>): IoHandle\n\nSpawn a child process. Returns a handle for reading/writing to its stdin/stdout.\n\n```shape\nlet proc = io.spawn(\"ls\", [\"-la\"])\n```"
2572        }
2573        ("io", "exec") => {
2574            "**io.exec**(program: string, args?: Array<string>): object\n\nExecute a command and wait for completion. Returns `{ stdout, stderr, exit_code }`.\n\n```shape\nlet result = io.exec(\"echo\", [\"hello\"])\nprint(result.stdout)\n```"
2575        }
2576        ("io", "stdin") => "**io.stdin**(): IoHandle\n\nOpen standard input as a readable handle.",
2577        ("io", "stdout") => {
2578            "**io.stdout**(): IoHandle\n\nOpen standard output as a writable handle."
2579        }
2580        ("io", "stderr") => {
2581            "**io.stderr**(): IoHandle\n\nOpen standard error as a writable handle."
2582        }
2583        ("io", "read_line") => {
2584            "**io.read_line**(handle?: IoHandle): string\n\nRead a single line from a handle (or stdin if no handle given)."
2585        }
2586
2587        // time module
2588        ("time", "now") => {
2589            "**time.now**(): Instant\n\nReturn the current monotonic instant for measuring elapsed time.\n\n```shape\nlet start = time.now()\n// ... work ...\nprint(start.elapsed())\n```"
2590        }
2591        ("time", "sleep") => {
2592            "**time.sleep**(ms: number): unit\n\nSleep for the specified number of milliseconds. **Async** — must be awaited.\n\n```shape\nawait time.sleep(100)\n```"
2593        }
2594        ("time", "sleep_sync") => {
2595            "**time.sleep_sync**(ms: number): unit\n\nSleep for the specified number of milliseconds (blocking, for non-async contexts)."
2596        }
2597        ("time", "benchmark") => {
2598            "**time.benchmark**(fn: function, iterations?: int): object\n\nBenchmark a function over N iterations (default 1000).\n\nReturns `{ elapsed_ms, iterations, avg_ms }`."
2599        }
2600        ("time", "stopwatch") => {
2601            "**time.stopwatch**(): Instant\n\nStart a stopwatch. Call `.elapsed()` on the returned Instant to read elapsed time."
2602        }
2603        ("time", "millis") => {
2604            "**time.millis**(): number\n\nReturn current wall-clock time as milliseconds since Unix epoch."
2605        }
2606
2607        _ => return None,
2608    };
2609
2610    Some(Hover {
2611        contents: HoverContents::Markup(MarkupContent {
2612            kind: MarkupKind::Markdown,
2613            value: doc.to_string(),
2614        }),
2615        range: None,
2616    })
2617}
2618
2619fn get_module_hover(text: &str, word: &str) -> Option<Hover> {
2620    let registry = crate::completion::imports::get_registry();
2621    if let Some(module) = registry.get(word) {
2622        let mut content = format!("**Module**: `{}`\n\n{}", module.name, module.description);
2623
2624        let exports = module.export_names_public_surface(false);
2625        if !exports.is_empty() {
2626            content.push_str("\n\n**Exports:**\n");
2627            for name in &exports {
2628                if let Some(schema) = module.get_schema(&name) {
2629                    let params: Vec<String> = schema
2630                        .params
2631                        .iter()
2632                        .map(|p| format!("{}: {}", p.name, p.type_name))
2633                        .collect();
2634                    content.push_str(&format!(
2635                        "- `{}({})`{}\n",
2636                        name,
2637                        params.join(", "),
2638                        schema
2639                            .return_type
2640                            .as_ref()
2641                            .map(|r| format!(" -> {}", r))
2642                            .unwrap_or_default()
2643                    ));
2644                } else {
2645                    content.push_str(&format!("- `{}`\n", name));
2646                }
2647            }
2648        }
2649
2650        return Some(Hover {
2651            contents: HoverContents::Markup(MarkupContent {
2652                kind: MarkupKind::Markdown,
2653                value: content,
2654            }),
2655            range: None,
2656        });
2657    }
2658
2659    let local_module =
2660        crate::completion::imports::local_module_schema_from_source(word, Some(text))?;
2661    let mut content = format!(
2662        "**Module**: `{}`\n\nLocal module defined in this file.",
2663        word
2664    );
2665    if !local_module.functions.is_empty() {
2666        content.push_str("\n\n**Exports:**\n");
2667        for function in &local_module.functions {
2668            let params = function
2669                .params
2670                .iter()
2671                .map(|param| {
2672                    if param.required {
2673                        format!("{}: {}", param.name, param.type_name)
2674                    } else {
2675                        format!("{}?: {}", param.name, param.type_name)
2676                    }
2677                })
2678                .collect::<Vec<_>>();
2679            content.push_str(&format!(
2680                "- `{}({})`{}\n",
2681                function.name,
2682                params.join(", "),
2683                function
2684                    .return_type
2685                    .as_ref()
2686                    .map(|ret| format!(" -> {}", ret))
2687                    .unwrap_or_default()
2688            ));
2689        }
2690    }
2691
2692    Some(Hover {
2693        contents: HoverContents::Markup(MarkupContent {
2694            kind: MarkupKind::Markdown,
2695            value: content,
2696        }),
2697        range: None,
2698    })
2699}
2700
2701/// Get hover for a module member (e.g., "load" in "csv.load").
2702fn get_module_member_hover(text: &str, module_name: &str, member_name: &str) -> Option<Hover> {
2703    let registry = crate::completion::imports::get_registry();
2704    if let Some(module) = registry.get(module_name)
2705        && let Some(schema) = module.get_schema(member_name)
2706    {
2707        let params: Vec<String> = schema
2708            .params
2709            .iter()
2710            .map(|p| {
2711                if p.required {
2712                    format!("{}: {}", p.name, p.type_name)
2713                } else {
2714                    format!("{}?: {}", p.name, p.type_name)
2715                }
2716            })
2717            .collect();
2718
2719        let sig = format!("{}.{}({})", module_name, member_name, params.join(", "));
2720
2721        let mut content = format!("**Function**: `{}`\n\n{}", sig, schema.description);
2722
2723        if !schema.params.is_empty() {
2724            content.push_str("\n\n**Parameters:**\n");
2725            for p in &schema.params {
2726                let req = if p.required { "" } else { " (optional)" };
2727                content.push_str(&format!(
2728                    "- `{}`: `{}` — {}{}\n",
2729                    p.name, p.type_name, p.description, req
2730                ));
2731            }
2732        }
2733
2734        if let Some(ref return_type) = schema.return_type {
2735            content.push_str(&format!("\n**Returns:** `{}`", return_type));
2736        }
2737
2738        return Some(Hover {
2739            contents: HoverContents::Markup(MarkupContent {
2740                kind: MarkupKind::Markdown,
2741                value: content,
2742            }),
2743            range: None,
2744        });
2745    }
2746
2747    let local_function = crate::completion::imports::local_module_function_schema_from_source(
2748        module_name,
2749        member_name,
2750        Some(text),
2751    )?;
2752    let params = local_function
2753        .params
2754        .iter()
2755        .map(|param| {
2756            if param.required {
2757                format!("{}: {}", param.name, param.type_name)
2758            } else {
2759                format!("{}?: {}", param.name, param.type_name)
2760            }
2761        })
2762        .collect::<Vec<_>>();
2763    let sig = format!("{}.{}({})", module_name, member_name, params.join(", "));
2764
2765    let mut content = format!("**Function**: `{}`\n\nLocal module function.", sig);
2766    if let Some(return_type) = &local_function.return_type {
2767        content.push_str(&format!("\n\n**Returns:** `{}`", return_type));
2768    }
2769
2770    Some(Hover {
2771        contents: HoverContents::Markup(MarkupContent {
2772            kind: MarkupKind::Markdown,
2773            value: content,
2774        }),
2775        range: None,
2776    })
2777}
2778
2779/// Get hover for property access expressions (e.g., instr.symbol)
2780fn get_property_access_hover(text: &str, hovered_word: &str, position: Position) -> Option<Hover> {
2781    let cursor_offset = position_to_offset(text, position)?;
2782    let mut program = parse_with_fallback(text)?;
2783    // Desugar query syntax before analysis
2784    shape_ast::transform::desugar_program(&mut program);
2785
2786    struct PropertyAccessFinder<'a> {
2787        hovered_word: &'a str,
2788        offset: usize,
2789        best: Option<(usize, String, String)>, // (span_len, object_name, property)
2790    }
2791
2792    impl<'a> Visitor for PropertyAccessFinder<'a> {
2793        fn visit_expr(&mut self, expr: &Expr) -> bool {
2794            // Extract (object, property, span) from PropertyAccess or MethodCall
2795            let (object, property, span) = match expr {
2796                Expr::PropertyAccess {
2797                    object,
2798                    property,
2799                    span,
2800                    ..
2801                } => (object.as_ref(), property.as_str(), *span),
2802                Expr::MethodCall {
2803                    receiver,
2804                    method,
2805                    span,
2806                    ..
2807                } => (receiver.as_ref(), method.as_str(), *span),
2808                _ => return true,
2809            };
2810
2811            if property != self.hovered_word || !span_contains_offset(span, self.offset) {
2812                return true;
2813            }
2814
2815            let Expr::Identifier(object_name, _) = object else {
2816                return true;
2817            };
2818
2819            let len = span.len();
2820            if self
2821                .best
2822                .as_ref()
2823                .map(|(best_len, _, _)| len < *best_len)
2824                .unwrap_or(true)
2825            {
2826                self.best = Some((len, object_name.clone(), property.to_string()));
2827            }
2828            true
2829        }
2830    }
2831
2832    let mut finder = PropertyAccessFinder {
2833        hovered_word,
2834        offset: cursor_offset,
2835        best: None,
2836    };
2837    walk_program(&mut finder, &program);
2838    let (_, object_name, property) = finder.best?;
2839
2840    // Check if self is a module member access (e.g., csv.load)
2841    if let Some(hover) = get_module_member_hover(text, &object_name, &property) {
2842        return Some(hover);
2843    }
2844
2845    // Check Content API member access (e.g., Content.text, Color.red)
2846    if let Some(hover) = get_content_member_hover(&object_name, &property) {
2847        return Some(hover);
2848    }
2849
2850    // Check DateTime / io / time member access
2851    if let Some(hover) = get_namespace_member_hover(&object_name, &property) {
2852        return Some(hover);
2853    }
2854
2855    // Try engine-inferred type first, fall back to heuristic
2856    let program_types = infer_program_types(&program);
2857    let object_type = if object_name == "self" {
2858        receiver_type_at_offset(&program, cursor_offset)
2859    } else {
2860        infer_variable_visible_type_at_offset(&program, &object_name, cursor_offset).or_else(|| {
2861            choose_best_variable_type(
2862                program_types.get(&object_name).cloned(),
2863                infer_variable_type(&program, &object_name),
2864            )
2865        })
2866    }?;
2867
2868    // Try unified metadata first (Rust-defined types)
2869    if let Some(properties) = unified_metadata().get_type_properties(&object_type) {
2870        if let Some(prop_info) = properties.iter().find(|p| p.name == property) {
2871            let content = format!(
2872                "**Property**: `{}.{}`\n\n**Type:** `{}`\n\n{}",
2873                object_name, property, prop_info.property_type, prop_info.description
2874            );
2875            return Some(Hover {
2876                contents: HoverContents::Markup(MarkupContent {
2877                    kind: MarkupKind::Markdown,
2878                    value: content,
2879                }),
2880                range: None,
2881            });
2882        }
2883    }
2884
2885    // Try user-defined struct fields from AST (including generic instantiations).
2886    if let Some(field_type) = resolve_struct_field_type(&program, &object_type, &property) {
2887        let content = format!(
2888            "**Property**: `{}.{}`\n\n**Type:** `{}`\n\n**Defined on:** `{}`",
2889            object_name, property, field_type, object_type
2890        );
2891        return Some(Hover {
2892            contents: HoverContents::Markup(MarkupContent {
2893                kind: MarkupKind::Markdown,
2894                value: content,
2895            }),
2896            range: None,
2897        });
2898    }
2899
2900    // Fallback: inferred struct literal shapes when no explicit type definition exists.
2901    let struct_fields = extract_struct_fields(&program);
2902    if let Some(fields) = struct_fields.get(&object_type) {
2903        if let Some((_, field_type)) = fields.iter().find(|(name, _)| name == &property) {
2904            let content = format!(
2905                "**Property**: `{}.{}`\n\n**Type:** `{}`\n\n**Defined on:** `{}`",
2906                object_name, property, field_type, object_type
2907            );
2908            return Some(Hover {
2909                contents: HoverContents::Markup(MarkupContent {
2910                    kind: MarkupKind::Markdown,
2911                    value: content,
2912                }),
2913                range: None,
2914            });
2915        }
2916    }
2917
2918    // Inline/structural object shape (e.g., `{ x: int, y: int }`)
2919    if let Some(fields) = parse_object_shape_fields(&object_type) {
2920        if let Some((_, field_type)) = fields.iter().find(|(name, _)| name == &property) {
2921            let content = format!(
2922                "**Property**: `{}.{}`\n\n**Type:** `{}`\n\n**Defined on:** `{}`",
2923                object_name, property, field_type, object_type
2924            );
2925            return Some(Hover {
2926                contents: HoverContents::Markup(MarkupContent {
2927                    kind: MarkupKind::Markdown,
2928                    value: content,
2929                }),
2930                range: None,
2931            });
2932        }
2933    }
2934
2935    None
2936}
2937
2938/// Get hover for a join strategy keyword showing the resolved return type.
2939///
2940/// When hovering over `all`, `race`, `any`, or `settle` in an `await join` expression,
2941/// self shows the resolved return type based on the join strategy and branch count.
2942fn get_join_expression_hover(text: &str, word: &str, position: Position) -> Option<Hover> {
2943    let cursor_offset = position_to_offset(text, position)?;
2944    let program = parse_with_fallback(text)?;
2945
2946    struct JoinFinder {
2947        offset: usize,
2948        target_kind: shape_ast::ast::JoinKind,
2949        best: Option<(usize, usize)>, // (span_len, branch_count)
2950    }
2951
2952    impl shape_runtime::visitor::Visitor for JoinFinder {
2953        fn visit_expr(&mut self, expr: &Expr) -> bool {
2954            if let Expr::Join(join_expr, span) = expr {
2955                if join_expr.kind == self.target_kind && span_contains_offset(*span, self.offset) {
2956                    let len = span.len();
2957                    if self
2958                        .best
2959                        .map(|(best_len, _)| len < best_len)
2960                        .unwrap_or(true)
2961                    {
2962                        self.best = Some((len, join_expr.branches.len()));
2963                    }
2964                }
2965            }
2966            true
2967        }
2968    }
2969
2970    let target_kind = match word {
2971        "all" => JoinKind::All,
2972        "race" => JoinKind::Race,
2973        "any" => JoinKind::Any,
2974        "settle" => JoinKind::Settle,
2975        _ => return None,
2976    };
2977
2978    let mut finder = JoinFinder {
2979        offset: cursor_offset,
2980        target_kind,
2981        best: None,
2982    };
2983    shape_runtime::visitor::walk_program(&mut finder, &program);
2984
2985    let branch_count = finder.best.map(|(_, count)| count)?;
2986
2987    let (return_type, description) = match word {
2988        "all" => (
2989            format!("(T1, T2, ...T{})", branch_count),
2990            "Waits for **all** branches to complete. Returns a tuple of all results.",
2991        ),
2992        "race" => (
2993            "T".to_string(),
2994            "Returns the result of the **first** branch to complete. Cancels remaining branches.",
2995        ),
2996        "any" => (
2997            "T".to_string(),
2998            "Returns the result of the **first** branch to succeed (non-error). Cancels remaining branches.",
2999        ),
3000        "settle" => (
3001            format!("(Result<T1>, Result<T2>, ...Result<T{}>)", branch_count),
3002            "Waits for **all** branches. Returns individual Result values preserving success/error status.",
3003        ),
3004        _ => return None,
3005    };
3006
3007    let content = format!(
3008        "**Join Strategy**: `{}`\n\n{}\n\n**Branches:** {}\n**Return type:** `{}`",
3009        word, description, branch_count, return_type
3010    );
3011
3012    Some(Hover {
3013        contents: HoverContents::Markup(MarkupContent {
3014            kind: MarkupKind::Markdown,
3015            value: content,
3016        }),
3017        range: None,
3018    })
3019}
3020
3021#[cfg(test)]
3022#[path = "hover_tests.rs"]
3023mod tests;