1use std::collections::HashMap;
7
8use crate::type_inference::simplify_result_type;
9use crate::util::offset_to_position;
10use shape_ast::ast::expr_helpers::ComptimeForExpr;
11use shape_ast::ast::{
12 Expr, FunctionDef, Item, Program, Span, Spanned, Statement, TypeAnnotation, VariableDecl,
13};
14use shape_ast::parser::parse_program;
15use shape_runtime::visitor::{Visitor, walk_program};
16use tower_lsp_server::ls_types::{InlayHint, InlayHintKind, InlayHintLabel, Position, Range};
17
18use crate::type_inference::{
19 FunctionTypeInfo, ParamReferenceMode, infer_expr_type, infer_expr_type_via_engine,
20 infer_function_signatures, infer_program_types, infer_program_types_with_context,
21 infer_variable_type_for_display, unified_metadata,
22};
23
24pub struct InlayHintConfig {
26 pub show_type_hints: bool,
27 pub show_parameter_hints: bool,
28 pub show_variable_type_hints: bool,
30 pub show_return_type_hints: bool,
32}
33
34impl Default for InlayHintConfig {
35 fn default() -> Self {
36 Self {
37 show_type_hints: true,
38 show_parameter_hints: true,
39 show_variable_type_hints: true,
40 show_return_type_hints: true,
41 }
42 }
43}
44
45struct HintContext<'a> {
48 text: &'a str,
49 program: &'a Program,
50 range: Range,
51 config: &'a InlayHintConfig,
52 hints: Vec<InlayHint>,
53 type_map: HashMap<String, String>,
55 function_types: HashMap<String, FunctionTypeInfo>,
57}
58
59impl<'a> HintContext<'a> {
60 fn is_primitive_value_type_name(name: &str) -> bool {
61 let normalized = name.trim().trim_end_matches('?');
62 matches!(
63 normalized,
64 "int"
65 | "integer"
66 | "i64"
67 | "number"
68 | "float"
69 | "f64"
70 | "decimal"
71 | "bool"
72 | "boolean"
73 | "()"
74 | "void"
75 | "unit"
76 | "none"
77 | "null"
78 | "undefined"
79 | "never"
80 )
81 }
82
83 fn split_top_level_union(type_str: &str) -> Vec<String> {
84 let mut parts = Vec::new();
85 let mut start = 0usize;
86 let mut paren_depth = 0usize;
87 let mut bracket_depth = 0usize;
88 let mut brace_depth = 0usize;
89 let mut angle_depth = 0usize;
90
91 for (idx, ch) in type_str.char_indices() {
92 match ch {
93 '(' => paren_depth += 1,
94 ')' => paren_depth = paren_depth.saturating_sub(1),
95 '[' => bracket_depth += 1,
96 ']' => bracket_depth = bracket_depth.saturating_sub(1),
97 '{' => brace_depth += 1,
98 '}' => brace_depth = brace_depth.saturating_sub(1),
99 '<' => angle_depth += 1,
100 '>' => angle_depth = angle_depth.saturating_sub(1),
101 _ => {}
102 }
103 if ch == '|'
104 && paren_depth == 0
105 && bracket_depth == 0
106 && brace_depth == 0
107 && angle_depth == 0
108 {
109 parts.push(type_str[start..idx].trim().to_string());
110 start = idx + ch.len_utf8();
111 }
112 }
113
114 parts.push(type_str[start..].trim().to_string());
115 parts.into_iter().filter(|part| !part.is_empty()).collect()
116 }
117
118 fn apply_ref_prefix(type_str: &str, mode: &ParamReferenceMode) -> String {
119 let trimmed = type_str.trim();
120 if trimmed.starts_with('&') {
121 trimmed.to_string()
122 } else {
123 format!("{}{}", mode.prefix(), trimmed)
124 }
125 }
126
127 fn format_reference_aware_type(type_str: &str, mode: Option<&ParamReferenceMode>) -> String {
128 let Some(mode) = mode else {
129 return type_str.to_string();
130 };
131
132 let union_parts = Self::split_top_level_union(type_str);
133 if union_parts.len() <= 1 {
134 return Self::apply_ref_prefix(type_str, mode);
135 }
136
137 union_parts
138 .into_iter()
139 .map(|part| {
140 if Self::is_primitive_value_type_name(&part) {
141 part
142 } else {
143 Self::apply_ref_prefix(&part, mode)
144 }
145 })
146 .collect::<Vec<_>>()
147 .join(" | ")
148 }
149
150 fn new(
151 text: &'a str,
152 program: &'a Program,
153 range: Range,
154 config: &'a InlayHintConfig,
155 type_map: HashMap<String, String>,
156 function_types: HashMap<String, FunctionTypeInfo>,
157 ) -> Self {
158 Self {
159 text,
160 program,
161 range,
162 config,
163 hints: Vec::new(),
164 type_map,
165 function_types,
166 }
167 }
168
169 fn collect_variable_type_hint(&mut self, decl: &VariableDecl) {
173 if !self.config.show_type_hints
174 || !self.config.show_variable_type_hints
175 || decl.type_annotation.is_some()
176 {
177 return;
178 }
179
180 let var_name = decl.pattern.as_identifier();
182 let inferred_type = var_name
183 .and_then(|name| {
184 decl.pattern
185 .as_identifier_span()
186 .and_then(|span| {
187 if span.is_dummy() {
188 None
189 } else {
190 infer_variable_type_for_display(self.program, name, span.end)
191 }
192 })
193 .or_else(|| self.type_map.get(name).cloned())
194 })
195 .or_else(|| {
196 decl.value
197 .as_ref()
198 .and_then(infer_expr_type_via_engine)
199 .or_else(|| decl.value.as_ref().and_then(infer_expr_type))
200 });
201
202 if let Some(inferred_type) = inferred_type {
203 if let Some(span) = decl.pattern.as_identifier_span() {
204 if !span.is_dummy() {
205 let position = offset_to_position(self.text, span.end);
206 if is_in_range(position, self.range) {
207 self.hints.push(InlayHint {
208 position,
209 label: InlayHintLabel::String(format!(": {}", inferred_type)),
210 kind: Some(InlayHintKind::TYPE),
211 text_edits: None,
212 tooltip: None,
213 padding_left: Some(false),
214 padding_right: Some(true),
215 data: None,
216 });
217 }
218 }
219 }
220 }
221 }
222
223 fn collect_function_type_hints(&mut self, func_def: &FunctionDef) {
226 if !self.config.show_type_hints {
227 return;
228 }
229
230 let info = match self.function_types.get(&func_def.name) {
231 Some(info) => info.clone(),
232 None => return,
233 };
234
235 for (param_name, type_str) in &info.param_types {
237 if let Some(ast_param) = func_def
239 .params
240 .iter()
241 .find(|p| p.simple_name() == Some(param_name.as_str()))
242 {
243 let span = ast_param.span();
244 if !span.is_dummy() {
245 let display_type = Self::format_reference_aware_type(
246 type_str,
247 info.param_ref_modes.get(param_name),
248 );
249 let position = offset_to_position(self.text, span.end);
250 if is_in_range(position, self.range) {
251 self.hints.push(InlayHint {
252 position,
253 label: InlayHintLabel::String(format!(": {}", display_type)),
254 kind: Some(InlayHintKind::TYPE),
255 text_edits: None,
256 tooltip: None,
257 padding_left: Some(false),
258 padding_right: Some(true),
259 data: None,
260 });
261 }
262 }
263 }
264 }
265
266 if let Some(return_type) = &info.return_type {
268 if !self.config.show_return_type_hints {
269 return;
270 }
271 if let Some(hint_offset) = self.return_hint_offset(func_def) {
272 let position = offset_to_position(self.text, hint_offset);
273 if is_in_range(position, self.range) {
274 let display_type = simplify_result_type(return_type);
275 self.hints.push(InlayHint {
276 position,
277 label: InlayHintLabel::String(format!("-> {}", display_type)),
278 kind: Some(InlayHintKind::TYPE),
279 text_edits: None,
280 tooltip: None,
281 padding_left: Some(true),
282 padding_right: Some(true),
283 data: None,
284 });
285 }
286 }
287 }
288 }
289
290 fn return_hint_offset(&self, func_def: &FunctionDef) -> Option<usize> {
296 let text_len = self.text.len();
297 let header_start = func_def.name_span.end.min(text_len);
298 let header_tail = &self.text[header_start..];
299 if let Some(open_brace_rel) = header_tail.find('{') {
300 let header = &header_tail[..open_brace_rel];
301 if let Some(close_paren_rel) = header.rfind(')') {
302 return Some(header_start + close_paren_rel + 1);
303 }
304 }
305
306 let last_param_end = func_def
307 .params
308 .iter()
309 .filter_map(|param| {
310 let span = param.span();
311 if span.is_dummy() {
312 None
313 } else {
314 Some(span.end)
315 }
316 })
317 .max();
318
319 if let Some(end) = last_param_end {
320 return Some(end);
321 }
322
323 if !func_def.name_span.is_dummy() {
324 return Some(func_def.name_span.end);
325 }
326
327 None
328 }
329
330 fn collect_comptime_for_hint(&mut self, comptime_for: &ComptimeForExpr, span: &Span) {
337 if span.is_dummy() {
338 return;
339 }
340
341 let hint_label =
343 if let Expr::PropertyAccess { property, .. } = comptime_for.iterable.as_ref() {
344 if property == "fields" {
345 "comptime unrolled".to_string()
349 } else {
350 "comptime unrolled".to_string()
351 }
352 } else {
353 "comptime unrolled".to_string()
354 };
355
356 let position = offset_to_position(self.text, span.end);
357 if is_in_range(position, self.range) {
358 self.hints.push(InlayHint {
359 position,
360 label: InlayHintLabel::String(hint_label),
361 kind: Some(InlayHintKind::TYPE),
362 text_edits: None,
363 tooltip: Some(tower_lsp_server::ls_types::InlayHintTooltip::String(
364 "This loop is unrolled at compile time by the comptime system.".to_string(),
365 )),
366 padding_left: Some(true),
367 padding_right: Some(false),
368 data: None,
369 });
370 }
371 }
372
373 fn collect_table_row_hints(&mut self, decl: &VariableDecl) {
377 if !self.config.show_parameter_hints {
378 return;
379 }
380
381 let rows = match &decl.value {
383 Some(Expr::TableRows(rows, _)) => rows,
384 _ => return,
385 };
386
387 let inner_type = match &decl.type_annotation {
389 Some(TypeAnnotation::Generic { name, args }) if name == "Table" => args
390 .first()
391 .and_then(|a| a.as_simple_name())
392 .map(String::from),
393 _ => None,
394 };
395 let inner_type = match inner_type {
396 Some(t) => t,
397 None => return,
398 };
399
400 let field_names: Vec<String> = self
402 .program
403 .items
404 .iter()
405 .find_map(|item| {
406 if let Item::StructType(struct_def, _) = item {
407 if struct_def.name == inner_type {
408 Some(
409 struct_def
410 .fields
411 .iter()
412 .filter(|f| !f.is_comptime)
413 .map(|f| f.name.clone())
414 .collect(),
415 )
416 } else {
417 None
418 }
419 } else {
420 None
421 }
422 })
423 .unwrap_or_default();
424
425 if field_names.is_empty() {
426 return;
427 }
428
429 for row in rows {
431 for (i, elem) in row.iter().enumerate() {
432 if let Some(field_name) = field_names.get(i) {
433 let elem_span = elem.span();
434 if !elem_span.is_dummy() {
435 let position = offset_to_position(self.text, elem_span.start);
436 if is_in_range(position, self.range) {
437 self.hints.push(InlayHint {
438 position,
439 label: InlayHintLabel::String(format!("{}:", field_name)),
440 kind: Some(InlayHintKind::PARAMETER),
441 text_edits: None,
442 tooltip: None,
443 padding_left: Some(false),
444 padding_right: Some(true),
445 data: None,
446 });
447 }
448 }
449 }
450 }
451 }
452 }
453
454 fn collect_parameter_hints(&mut self, args: &[Expr], func_name: &str) {
455 if func_name == "print" {
456 return;
457 }
458
459 let func_info = unified_metadata().get_function(func_name);
460
461 if let Some(func) = func_info {
462 for (i, arg) in args.iter().enumerate() {
463 if let Some(param) = func.parameters.get(i) {
464 if param.name.len() > 1 {
466 let arg_span = arg.span();
468 if !arg_span.is_dummy() {
469 let position = offset_to_position(self.text, arg_span.start);
470 if is_in_range(position, self.range) {
471 self.hints.push(InlayHint {
472 position,
473 label: InlayHintLabel::String(format!("{}:", param.name)),
474 kind: Some(InlayHintKind::PARAMETER),
475 text_edits: None,
476 tooltip: Some(
477 tower_lsp_server::ls_types::InlayHintTooltip::String(
478 param.description.clone(),
479 ),
480 ),
481 padding_left: Some(false),
482 padding_right: Some(true),
483 data: None,
484 });
485 }
486 }
487 }
488 }
489 }
490 }
491 }
492}
493
494impl<'a> Visitor for HintContext<'a> {
495 fn visit_item(&mut self, item: &Item) -> bool {
496 match item {
497 Item::VariableDecl(decl, _) => {
498 self.collect_variable_type_hint(decl);
499 self.collect_table_row_hints(decl);
500 }
501 Item::Function(func_def, _) => self.collect_function_type_hints(func_def),
502 _ => {}
503 }
504 true }
506
507 fn visit_stmt(&mut self, stmt: &Statement) -> bool {
508 if let Statement::VariableDecl(decl, _) = stmt {
510 self.collect_variable_type_hint(decl);
511 self.collect_table_row_hints(decl);
512 }
513 true }
515
516 fn visit_expr(&mut self, expr: &Expr) -> bool {
517 if let Expr::FunctionCall { name, args, .. } = expr {
519 if self.config.show_parameter_hints {
520 self.collect_parameter_hints(args, name);
521 }
522 }
523
524 if let Expr::ComptimeFor(comptime_for, span) = expr {
526 if self.config.show_type_hints {
527 self.collect_comptime_for_hint(comptime_for, span);
528 }
529 }
530
531 true }
533}
534
535pub fn get_inlay_hints(
541 text: &str,
542 range: Range,
543 config: &InlayHintConfig,
544 _cached_program: Option<&Program>,
545) -> Vec<InlayHint> {
546 get_inlay_hints_with_context(text, range, config, _cached_program, None, None)
547}
548
549pub fn get_inlay_hints_with_context(
551 text: &str,
552 range: Range,
553 config: &InlayHintConfig,
554 _cached_program: Option<&Program>,
555 current_file: Option<&std::path::Path>,
556 workspace_root: Option<&std::path::Path>,
557) -> Vec<InlayHint> {
558 let program = match parse_program(text) {
560 Ok(p) => p,
561 Err(_) => {
562 let partial = shape_ast::parse_program_resilient(text);
563 if partial.items.is_empty() {
564 return Vec::new();
565 }
566 partial.into_program()
567 }
568 };
569
570 let type_map = if current_file.is_none() && workspace_root.is_none() {
572 infer_program_types(&program)
573 } else {
574 infer_program_types_with_context(&program, current_file, workspace_root, Some(text))
575 };
576 let function_types = infer_function_signatures(&program);
577
578 let mut ctx = HintContext::new(text, &program, range, config, type_map, function_types);
579
580 walk_program(&mut ctx, &program);
582
583 if config.show_type_hints {
585 collect_comptime_alias_hints(text, &program, range, &mut ctx.hints);
586 }
587
588 ctx.hints
589}
590
591fn collect_comptime_alias_hints(
596 text: &str,
597 program: &shape_ast::ast::Program,
598 range: Range,
599 hints: &mut Vec<InlayHint>,
600) {
601 use std::collections::HashMap;
602
603 let mut struct_comptime: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
605 for item in &program.items {
606 if let Item::StructType(struct_def, _) = item {
607 let comptime_fields: Vec<(String, Option<String>)> = struct_def
608 .fields
609 .iter()
610 .filter(|f| f.is_comptime)
611 .map(|f| {
612 let default = f.default_value.as_ref().map(format_comptime_value);
613 (f.name.clone(), default)
614 })
615 .collect();
616 if !comptime_fields.is_empty() {
617 struct_comptime.insert(struct_def.name.clone(), comptime_fields);
618 }
619 }
620 }
621
622 for item in &program.items {
624 if let Item::TypeAlias(alias_def, span) = item {
625 let base_type = match &alias_def.type_annotation {
626 shape_ast::ast::TypeAnnotation::Basic(name) => name.clone(),
627 _ => continue,
628 };
629
630 let comptime_fields = match struct_comptime.get(&base_type) {
631 Some(fields) => fields,
632 None => continue,
633 };
634
635 let resolved: Vec<String> = comptime_fields
637 .iter()
638 .filter_map(|(name, default)| {
639 let value = alias_def
640 .meta_param_overrides
641 .as_ref()
642 .and_then(|o| o.get(name))
643 .map(format_comptime_value)
644 .or_else(|| default.clone());
645 value.map(|v| format!("{} = {}", name, v))
646 })
647 .collect();
648
649 if resolved.is_empty() {
650 continue;
651 }
652
653 let hint_offset = span.end;
654 let position = offset_to_position(text, hint_offset);
655 if is_in_range(position, range) {
656 hints.push(InlayHint {
657 position,
658 label: InlayHintLabel::String(format!(" [{}]", resolved.join(", "))),
659 kind: Some(InlayHintKind::TYPE),
660 text_edits: None,
661 tooltip: Some(tower_lsp_server::ls_types::InlayHintTooltip::String(
662 format!("Resolved comptime values from {}", base_type),
663 )),
664 padding_left: Some(false),
665 padding_right: Some(true),
666 data: None,
667 });
668 }
669 }
670 }
671}
672
673fn format_comptime_value(expr: &Expr) -> String {
675 match expr {
676 Expr::Literal(lit, _) => match lit {
677 shape_ast::ast::Literal::String(s) => format!("\"{}\"", s),
678 shape_ast::ast::Literal::Number(n) => format!("{}", n),
679 shape_ast::ast::Literal::Int(n) => format!("{}", n),
680 shape_ast::ast::Literal::Decimal(d) => format!("{}D", d),
681 shape_ast::ast::Literal::Bool(b) => format!("{}", b),
682 shape_ast::ast::Literal::None => "None".to_string(),
683 _ => "...".to_string(),
684 },
685 _ => "...".to_string(),
686 }
687}
688
689fn is_in_range(pos: Position, range: Range) -> bool {
691 if pos.line < range.start.line || pos.line > range.end.line {
692 return false;
693 }
694 if pos.line == range.start.line && pos.character < range.start.character {
695 return false;
696 }
697 if pos.line == range.end.line && pos.character > range.end.character {
698 return false;
699 }
700 true
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::type_inference::infer_literal_type;
707 use shape_ast::ast::Literal;
708
709 #[test]
710 fn test_infer_literal_type() {
711 assert_eq!(infer_literal_type(&Literal::Number(42.0)), "number");
712 assert_eq!(
713 infer_literal_type(&Literal::String("hello".to_string())),
714 "string"
715 );
716 assert_eq!(infer_literal_type(&Literal::Bool(true)), "bool");
717 assert_eq!(infer_literal_type(&Literal::None), "Option");
718 }
719
720 #[test]
721 fn test_infer_literal_type_int() {
722 assert_eq!(infer_literal_type(&Literal::Int(42)), "int");
723 }
724
725 #[test]
726 fn test_infer_literal_type_decimal() {
727 use rust_decimal::Decimal;
728 assert_eq!(
729 infer_literal_type(&Literal::Decimal(Decimal::new(1050, 2))),
730 "decimal"
731 );
732 }
733
734 #[test]
735 fn test_numeric_type_hints_int() {
736 let config = InlayHintConfig::default();
737 let range = Range {
738 start: Position {
739 line: 0,
740 character: 0,
741 },
742 end: Position {
743 line: 10,
744 character: 100,
745 },
746 };
747
748 let hints = get_inlay_hints("let i = 10", range, &config, None);
749 assert!(!hints.is_empty(), "Expected at least one hint for integer");
750 let label = match &hints[0].label {
751 InlayHintLabel::String(s) => s.clone(),
752 _ => panic!("Expected string label"),
753 };
754 assert!(
755 label.contains("int"),
756 "Expected 'int' in hint, got: {}",
757 label
758 );
759 }
760
761 #[test]
762 fn test_numeric_type_hints_decimal() {
763 let config = InlayHintConfig::default();
764 let range = Range {
765 start: Position {
766 line: 0,
767 character: 0,
768 },
769 end: Position {
770 line: 10,
771 character: 100,
772 },
773 };
774
775 let hints = get_inlay_hints("let d = 10D", range, &config, None);
776 assert!(!hints.is_empty(), "Expected at least one hint for decimal");
777 let label = match &hints[0].label {
778 InlayHintLabel::String(s) => s.clone(),
779 _ => panic!("Expected string label"),
780 };
781 assert!(
782 label.contains("decimal"),
783 "Expected 'decimal' in hint, got: {}",
784 label
785 );
786 }
787
788 #[test]
789 fn test_numeric_type_hints_number() {
790 let config = InlayHintConfig::default();
791 let range = Range {
792 start: Position {
793 line: 0,
794 character: 0,
795 },
796 end: Position {
797 line: 10,
798 character: 100,
799 },
800 };
801
802 let hints = get_inlay_hints("let f = 10.0", range, &config, None);
803 assert!(!hints.is_empty(), "Expected at least one hint for float");
804 let label = match &hints[0].label {
805 InlayHintLabel::String(s) => s.clone(),
806 _ => panic!("Expected string label"),
807 };
808 assert!(
809 label.contains("number"),
810 "Expected 'number' in hint, got: {}",
811 label
812 );
813 }
814
815 #[test]
816 fn test_offset_to_position() {
817 let text = "let x = 42;\nlet y = 10;";
818 let pos = offset_to_position(text, 0);
819 assert_eq!(pos.line, 0);
820 assert_eq!(pos.character, 0);
821
822 let pos = offset_to_position(text, 12);
823 assert_eq!(pos.line, 1);
824 assert_eq!(pos.character, 0);
825 }
826
827 #[test]
828 fn test_match_expression_type_hint() {
829 let config = InlayHintConfig::default();
830 let range = Range {
831 start: Position {
832 line: 0,
833 character: 0,
834 },
835 end: Position {
836 line: 10,
837 character: 100,
838 },
839 };
840
841 let code = "let test = match 2 {\n 0 => true,\n _ => false,\n}";
842 let hints = get_inlay_hints(code, range, &config, None);
843 eprintln!(
844 "Hints for match: {:?}",
845 hints
846 .iter()
847 .map(|h| match &h.label {
848 InlayHintLabel::String(s) => s.clone(),
849 _ => "non-string".to_string(),
850 })
851 .collect::<Vec<_>>()
852 );
853 let type_hints: Vec<_> = hints
854 .iter()
855 .filter(|h| h.kind == Some(InlayHintKind::TYPE))
856 .collect();
857 assert!(
858 !type_hints.is_empty(),
859 "Expected a type hint for 'let test = match ...'"
860 );
861 let label = match &type_hints[0].label {
862 InlayHintLabel::String(s) => s.clone(),
863 _ => panic!("Expected string label"),
864 };
865 assert!(
866 label.contains("bool"),
867 "Expected 'bool' in hint, got: {}",
868 label
869 );
870 }
871
872 #[test]
873 fn test_infer_try_operator_type() {
874 use shape_ast::ast::Span;
875
876 let expr = Expr::TryOperator(
877 Box::new(Expr::FunctionCall {
878 name: "some_func".to_string(),
879 args: vec![],
880 named_args: vec![],
881 span: Span::DUMMY,
882 }),
883 Span::DUMMY,
884 );
885 let _ = infer_expr_type(&expr);
886 }
887
888 fn full_range() -> Range {
889 Range {
890 start: Position {
891 line: 0,
892 character: 0,
893 },
894 end: Position {
895 line: 100,
896 character: 100,
897 },
898 }
899 }
900
901 fn type_hint_labels(hints: &[InlayHint]) -> Vec<String> {
902 hints
903 .iter()
904 .filter(|h| h.kind == Some(InlayHintKind::TYPE))
905 .map(|h| match &h.label {
906 InlayHintLabel::String(s) => s.clone(),
907 _ => "non-string".to_string(),
908 })
909 .collect()
910 }
911
912 #[test]
913 fn test_function_return_type_hint() {
914 let code = "fn add(a: int, b: int) {\n return a + b\n}";
915 let config = InlayHintConfig::default();
916 let hints = get_inlay_hints(code, full_range(), &config, None);
917 let labels = type_hint_labels(&hints);
918 eprintln!("Return type hints: {:?}", labels);
919 let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
921 assert!(
922 has_return_hint,
923 "Expected a return type hint for fn without return annotation, got: {:?}",
924 labels
925 );
926 }
927
928 #[test]
929 fn test_function_return_hint_for_empty_params_anchors_after_close_paren() {
930 let code = "fn test() {\n}\n";
931 let config = InlayHintConfig::default();
932 let hints = get_inlay_hints(code, full_range(), &config, None);
933
934 let return_hint = hints
935 .iter()
936 .find(|hint| match &hint.label {
937 InlayHintLabel::String(label) => label.starts_with("->"),
938 _ => false,
939 })
940 .expect("expected return type hint");
941
942 let expected_col = code
943 .lines()
944 .next()
945 .and_then(|line| line.find(')'))
946 .map(|idx| idx as u32 + 1)
947 .expect("header should contain ')'");
948
949 assert_eq!(
950 return_hint.position,
951 Position {
952 line: 0,
953 character: expected_col
954 }
955 );
956 match &return_hint.label {
957 InlayHintLabel::String(label) => assert_eq!(label, "-> ()"),
958 _ => panic!("expected string inlay label"),
959 }
960 }
961
962 #[test]
963 fn test_print_parameter_hints_are_suppressed() {
964 let code = "print(\"hello\")\n";
965 let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
966 let has_parameter_hints = hints
967 .iter()
968 .any(|h| h.kind == Some(InlayHintKind::PARAMETER));
969 assert!(
970 !has_parameter_hints,
971 "print() should not emit parameter hints, got: {:?}",
972 hints
973 );
974 }
975
976 #[test]
977 fn test_function_param_type_hint_not_shown_when_annotated() {
978 let code = "fn greet(name: string) {\n return name\n}";
979 let config = InlayHintConfig::default();
980 let hints = get_inlay_hints(code, full_range(), &config, None);
981 let labels = type_hint_labels(&hints);
982 let has_param_hint = labels
984 .iter()
985 .any(|l| l.contains("string") && l.starts_with(":"));
986 assert!(
987 !has_param_hint,
988 "Should not show param type hint when annotation exists, got: {:?}",
989 labels
990 );
991 }
992
993 #[test]
994 fn test_function_no_hint_when_return_annotated() {
995 let code = "fn double(x: int) -> int {\n return x * 2\n}";
996 let config = InlayHintConfig::default();
997 let hints = get_inlay_hints(code, full_range(), &config, None);
998 let labels = type_hint_labels(&hints);
999 let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
1001 assert!(
1002 !has_return_hint,
1003 "Should not show return type hint when annotation exists, got: {:?}",
1004 labels
1005 );
1006 }
1007
1008 #[test]
1009 fn test_function_param_hint_shows_inferred_shared_reference() {
1010 let code = r#"
1011fn read_only(a) {
1012 return a.len()
1013}
1014let s = "abc"
1015read_only(s)
1016"#;
1017 let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1018 let labels = type_hint_labels(&hints);
1019 assert!(
1020 labels.iter().any(|l| l == ": &string"),
1021 "Expected inferred shared reference hint ': &string', got: {:?}",
1022 labels
1023 );
1024 }
1025
1026 #[test]
1027 fn test_function_param_hint_shows_inferred_exclusive_reference() {
1028 let code = r#"
1029fn write_ref(a) {
1030 a = a + "!"
1031 return a
1032}
1033let s = "abc"
1034write_ref(s)
1035"#;
1036 let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1037 let labels = type_hint_labels(&hints);
1038 assert!(
1039 labels.iter().any(|l| l == ": &mut string"),
1040 "Expected inferred exclusive reference hint ': &mut string', got: {:?}",
1041 labels
1042 );
1043 }
1044
1045 #[test]
1046 fn test_function_param_hint_union_is_memberwise_reference_aware() {
1047 let code = r#"
1048fn foo(a) { return a }
1049let i = foo(1)
1050let s = foo("hi")
1051"#;
1052 let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1053 let labels = type_hint_labels(&hints);
1054 let union_hint = labels
1055 .iter()
1056 .find(|l| l.starts_with(":") && l.contains("int") && l.contains("string"))
1057 .cloned()
1058 .unwrap_or_default();
1059 assert!(
1060 union_hint.contains("&string"),
1061 "Expected union hint to show reference-aware heap member, got: {:?}",
1062 labels
1063 );
1064 assert!(
1065 union_hint.contains("int"),
1066 "Expected union hint to keep primitive member by value, got: {:?}",
1067 labels
1068 );
1069 }
1070
1071 #[test]
1072 fn test_variable_type_hint_disabled() {
1073 let config = InlayHintConfig {
1074 show_type_hints: true,
1075 show_parameter_hints: true,
1076 show_variable_type_hints: false,
1077 show_return_type_hints: true,
1078 };
1079 let hints = get_inlay_hints("let x = 42", full_range(), &config, None);
1080 let type_labels = type_hint_labels(&hints);
1081 assert!(
1082 type_labels.is_empty(),
1083 "Should not show variable type hints when disabled, got: {:?}",
1084 type_labels
1085 );
1086 }
1087
1088 #[test]
1089 fn test_return_type_hint_disabled() {
1090 let config = InlayHintConfig {
1091 show_type_hints: true,
1092 show_parameter_hints: true,
1093 show_variable_type_hints: true,
1094 show_return_type_hints: false,
1095 };
1096 let code = "fn add(a: int, b: int) {\n return a + b\n}";
1097 let hints = get_inlay_hints(code, full_range(), &config, None);
1098 let labels = type_hint_labels(&hints);
1099 let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
1100 assert!(
1101 !has_return_hint,
1102 "Should not show return type hints when disabled, got: {:?}",
1103 labels
1104 );
1105 }
1106
1107 #[test]
1108 fn test_variable_inside_function_gets_hint() {
1109 let config = InlayHintConfig::default();
1110 let code = "fn foo() {\n let x = 42\n return x\n}";
1111 let hints = get_inlay_hints(code, full_range(), &config, None);
1112 let type_labels = type_hint_labels(&hints);
1113 let has_int_hint = type_labels.iter().any(|l| l.contains("int"));
1114 assert!(
1115 has_int_hint,
1116 "Should show type hint for variable inside function body, got: {:?}",
1117 type_labels
1118 );
1119 }
1120
1121 #[test]
1122 fn test_string_variable_hint() {
1123 let config = InlayHintConfig::default();
1124 let hints = get_inlay_hints("let name = \"hello\"", full_range(), &config, None);
1125 let labels = type_hint_labels(&hints);
1126 let has_string = labels.iter().any(|l| l.contains("string"));
1127 assert!(
1128 has_string,
1129 "Should show 'string' hint for string literal, got: {:?}",
1130 labels
1131 );
1132 }
1133
1134 #[test]
1135 fn test_bool_variable_hint() {
1136 let config = InlayHintConfig::default();
1137 let hints = get_inlay_hints("let flag = true", full_range(), &config, None);
1138 let labels = type_hint_labels(&hints);
1139 let has_bool = labels.iter().any(|l| l.contains("bool"));
1140 assert!(
1141 has_bool,
1142 "Should show 'bool' hint for bool literal, got: {:?}",
1143 labels
1144 );
1145 }
1146
1147 #[test]
1148 fn test_no_hint_when_type_annotated() {
1149 let config = InlayHintConfig::default();
1150 let hints = get_inlay_hints("let x: int = 42", full_range(), &config, None);
1151 let type_labels = type_hint_labels(&hints);
1152 let has_int = type_labels.iter().any(|l| l.contains("int"));
1154 assert!(
1155 !has_int,
1156 "Should not show type hint when annotation exists, got: {:?}",
1157 type_labels
1158 );
1159 }
1160
1161 #[test]
1162 fn test_table_row_literal_field_hints() {
1163 let code = r#"type FinRecord {
1164 month: int,
1165 revenue: number,
1166 profit: number,
1167 note: string
1168}
1169let t: Table<FinRecord> = [1, 100.0, 60.0, "jan"], [2, 120.0, 70.0, "feb"]
1170"#;
1171 let config = InlayHintConfig::default();
1172 let hints = get_inlay_hints(code, full_range(), &config, None);
1173 let param_hints: Vec<String> = hints
1174 .iter()
1175 .filter(|h| h.kind == Some(InlayHintKind::PARAMETER))
1176 .map(|h| match &h.label {
1177 InlayHintLabel::String(s) => s.clone(),
1178 _ => "non-string".to_string(),
1179 })
1180 .collect();
1181 assert_eq!(
1183 param_hints.len(),
1184 8,
1185 "Expected 8 parameter hints for 2 rows x 4 fields, got: {:?}",
1186 param_hints
1187 );
1188 assert_eq!(param_hints[0], "month:");
1189 assert_eq!(param_hints[1], "revenue:");
1190 assert_eq!(param_hints[2], "profit:");
1191 assert_eq!(param_hints[3], "note:");
1192 assert_eq!(param_hints[4], "month:");
1194 assert_eq!(param_hints[7], "note:");
1195 }
1196
1197 #[test]
1198 fn test_parse_error_with_no_recoverable_items_emits_no_hints() {
1199 let code = r#"
1200from std.core.snapshot import { Snapshot }
1201
1202let x = {x: 1}
1203x.y = 1
1204let i = 10D
1205"#;
1206 let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1207
1208 assert!(
1209 hints.is_empty(),
1210 "invalid parse with no recoverable AST should not emit hints"
1211 );
1212 }
1213}