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