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