1use 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
27std::thread_local! {
30 static CACHED_PROGRAM: std::cell::RefCell<Option<Program>> = const { std::cell::RefCell::new(None) };
31}
32
33fn 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 let cached = CACHED_PROGRAM.with(|c| c.borrow().clone());
43 if cached.is_some() {
44 return cached;
45 }
46 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
57pub 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 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 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 let word = get_word_at_position(text, position)?;
91
92 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 if let Some(hover) = get_hover_for_word(text, &word, position, module_cache, current_file) {
102 return Some(hover);
103 }
104
105 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
115fn 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 if let Some(hover) = get_interpolation_format_spec_hover(text, word, position) {
125 return Some(hover);
126 }
127
128 if let Some(hover) = get_annotation_hover(text, word, position, module_cache, current_file) {
130 return Some(hover);
131 }
132
133 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 if word == "async" {
142 if let Some(hover) = get_async_structured_hover(text, position) {
143 return Some(hover);
144 }
145 }
146
147 if word == "scope" {
149 if let Some(hover) = get_async_scope_keyword_hover(text, position) {
150 return Some(hover);
151 }
152 }
153
154 if word == "comptime" {
156 if let Some(hover) = get_comptime_block_hover(text, position) {
157 return Some(hover);
158 }
159 }
160
161 if let Some(hover) = get_comptime_builtin_hover(word) {
163 return Some(hover);
164 }
165
166 if let Some(hover) = get_self_receiver_hover(text, word, position) {
168 return Some(hover);
169 }
170
171 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 if let Some(hover) = get_content_api_hover(word) {
181 return Some(hover);
182 }
183
184 if let Some(hover) = get_namespace_api_hover(word) {
186 return Some(hover);
187 }
188
189 if let Some(hover) = get_keyword_hover(word) {
191 return Some(hover);
192 }
193
194 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 if let Some(hover) = get_builtin_function_hover(word) {
206 return Some(hover);
207 }
208
209 if let Some(hover) = get_module_hover(text, word) {
211 return Some(hover);
212 }
213
214 if let Some(hover) = get_comptime_field_hover(text, word, position) {
216 return Some(hover);
217 }
218
219 if let Some(hover) = get_type_param_hover(text, word, position) {
221 return Some(hover);
222 }
223
224 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 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 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
415fn 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 builtin_annotation_hover(word)
480}
481
482fn 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
566fn 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
632fn get_content_member_hover(object: &str, member: &str) -> Option<Hover> {
634 let doc = match (object, member) {
635 ("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", "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", "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", "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", "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
723fn 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
742fn 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
776fn 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
824fn 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
903fn 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
947fn 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
1031fn get_comptime_builtin_hover(word: &str) -> Option<Hover> {
1033 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
1074fn 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 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 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
1160fn 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
1176fn 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 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
1234fn 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 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
1851fn 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 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
1950fn 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
1974fn 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#[cfg(test)]
1984fn get_user_symbol_hover(text: &str, word: &str) -> Option<Hover> {
1985 let mut program = parse_with_fallback(text)?;
1986 shape_ast::transform::desugar_program(&mut program);
1988 get_user_symbol_hover_from_program(text, &program, word, None, None, None)
1989}
1990
1991fn 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 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 let symbols = extract_symbols(program);
2106
2107 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 let program_types = infer_program_types(program);
2121 let function_sigs = infer_function_signatures(program);
2122
2123 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(§ion);
2166 }
2167 }
2168
2169 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 if matches!(symbol.kind, SymbolKind::Function) {
2179 if let Some(sig_info) = function_sigs.get(word) {
2180 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
2336fn 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 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 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 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
2409fn 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 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 for export in &module_info.exports {
2430 if export.exported_name() != word {
2431 continue;
2432 }
2433
2434 let content = match export.kind {
2436 ModSymbolKind::Enum => {
2437 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 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
2594fn 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
2661fn get_namespace_member_hover(object: &str, member: &str) -> Option<Hover> {
2663 let doc = match (object, member) {
2664 ("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", "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", "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
2892fn 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
2970fn 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 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)>, }
2982
2983 impl<'a> Visitor for PropertyAccessFinder<'a> {
2984 fn visit_expr(&mut self, expr: &Expr) -> bool {
2985 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 if let Some(hover) = get_module_member_hover(text, &object_name, &property) {
3033 return Some(hover);
3034 }
3035
3036 if let Some(hover) = get_content_member_hover(&object_name, &property) {
3038 return Some(hover);
3039 }
3040
3041 if let Some(hover) = get_namespace_member_hover(&object_name, &property) {
3043 return Some(hover);
3044 }
3045
3046 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 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 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 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 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
3129fn 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)>, }
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;