1use tower_lsp::lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, InlayHintTooltip, MarkupContent, MarkupKind, Range};
2
3use logicaffeine_compile::analysis::VarState;
4use logicaffeine_language::token::TokenType;
5
6use crate::document::DocumentState;
7use crate::index::{DefinitionKind, resolve_token_name};
8
9pub fn inlay_hints(doc: &DocumentState, range: Range) -> Vec<InlayHint> {
13 let mut hints = Vec::new();
14 let is_default_range = range == Range::default();
15
16 for def in &doc.symbol_index.definitions {
17 if def.kind != DefinitionKind::Variable {
18 continue;
19 }
20 if def.span == logicaffeine_language::token::Span::default() {
21 continue;
22 }
23
24 if let Some(detail) = &def.detail {
25 if detail.contains("(inferred)") {
26 let pos = doc.line_index.position(def.span.end);
27 if !is_default_range && (pos.line < range.start.line || pos.line > range.end.line) {
28 continue;
29 }
30 let type_label = extract_inferred_type(detail);
31 hints.push(InlayHint {
32 position: pos,
33 label: InlayHintLabel::String(format!(": {}", type_label)),
34 kind: Some(InlayHintKind::TYPE),
35 text_edits: None,
36 tooltip: None,
37 padding_left: Some(false),
38 padding_right: Some(true),
39 data: None,
40 });
41 }
42 }
43 }
44
45 for token in &doc.tokens {
47 if !matches!(
48 &token.kind,
49 TokenType::Identifier | TokenType::ProperName(_)
50 | TokenType::Adjective(_) | TokenType::Noun(_)
51 ) {
52 continue;
53 }
54 if token.span == logicaffeine_language::token::Span::default() {
55 continue;
56 }
57
58 let pos = doc.line_index.position(token.span.end);
59 if !is_default_range && (pos.line < range.start.line || pos.line > range.end.line) {
60 continue;
61 }
62
63 if let Some(name) = resolve_token_name(token, &doc.interner) {
64 if let Some(state) = doc.ownership_states.get(name) {
65 let (label, tooltip) = match state {
66 VarState::Moved => ("moved", "This variable has been given away and can no longer be used."),
67 VarState::MaybeMoved => ("maybe moved", "This variable might have been given away in a conditional branch."),
68 VarState::Borrowed => ("borrowed", "This variable is currently borrowed (lent via Show)."),
69 VarState::Owned => continue, };
71 hints.push(InlayHint {
72 position: pos,
73 label: InlayHintLabel::String(format!(" {}", label)),
74 kind: Some(InlayHintKind::PARAMETER),
75 text_edits: None,
76 tooltip: Some(InlayHintTooltip::MarkupContent(MarkupContent {
77 kind: MarkupKind::Markdown,
78 value: tooltip.to_string(),
79 })),
80 padding_left: Some(true),
81 padding_right: Some(false),
82 data: None,
83 });
84 }
85 }
86 }
87
88 hints
89}
90
91fn extract_inferred_type(detail: &str) -> &str {
94 if let Some(colon_pos) = detail.rfind(": ") {
96 let after_colon = &detail[colon_pos + 2..];
97 if let Some(paren_pos) = after_colon.find(" (inferred)") {
98 let ty = after_colon[..paren_pos].trim();
99 if !ty.is_empty() {
100 return ty;
101 }
102 }
103 }
104 "auto"
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::document::DocumentState;
111
112 fn make_doc(source: &str) -> DocumentState {
113 DocumentState::new(source.to_string(), 1)
114 }
115
116 #[test]
117 fn inlay_hints_for_inferred_integer() {
118 let doc = make_doc("## Main\n Let x be 5.\n");
119 let range = Range::default();
120 let hints = inlay_hints(&doc, range);
121 assert!(!hints.is_empty(), "Expected inlay hint for inferred variable");
122 match &hints[0].label {
123 InlayHintLabel::String(s) => {
124 assert_eq!(s, ": Int", "Integer literal should infer Int, got '{}'", s);
125 }
126 _ => panic!("Expected string label"),
127 }
128 }
129
130 #[test]
131 fn inlay_hints_for_inferred_text() {
132 let doc = make_doc("## Main\n Let msg be \"hello\".\n");
133 let range = Range::default();
134 let hints = inlay_hints(&doc, range);
135 assert!(!hints.is_empty(), "Expected inlay hint for text variable");
136 match &hints[0].label {
137 InlayHintLabel::String(s) => {
138 assert_eq!(s, ": Text", "Text literal should infer Text, got '{}'", s);
139 }
140 _ => panic!("Expected string label"),
141 }
142 }
143
144 #[test]
145 fn inlay_hints_for_inferred_bool() {
146 let doc = make_doc("## Main\n Let flag be true.\n");
147 let range = Range::default();
148 let hints = inlay_hints(&doc, range);
149 assert!(!hints.is_empty(), "Expected inlay hint for bool variable");
150 match &hints[0].label {
151 InlayHintLabel::String(s) => {
152 assert_eq!(s, ": Bool", "Bool literal should infer Bool, got '{}'", s);
153 }
154 _ => panic!("Expected string label"),
155 }
156 }
157
158 #[test]
159 fn inlay_hints_empty_for_empty_doc() {
160 let doc = make_doc("");
161 let range = Range::default();
162 let hints = inlay_hints(&doc, range);
163 assert!(hints.is_empty());
164 }
165
166 #[test]
167 fn inlay_hints_skip_default_spans() {
168 let doc = make_doc("## Main\n Let x be 5.\n");
169 let range = Range::default();
170 let hints = inlay_hints(&doc, range);
171 for hint in &hints {
172 assert_eq!(hint.kind, Some(InlayHintKind::TYPE), "Hints should be TYPE kind");
173 }
174 }
175
176 #[test]
177 fn inlay_hints_hint_position_correct() {
178 let doc = make_doc("## Main\n Let x be 5.\n");
179 let range = Range::default();
180 let hints = inlay_hints(&doc, range);
181 assert!(!hints.is_empty(), "Should have at least one inlay hint for inferred variable");
182 let hint = &hints[0];
183 assert_eq!(hint.position.line, 1, "Hint should be on line 1 (the Let statement)");
184 }
185
186 #[test]
187 fn extract_inferred_type_from_detail() {
188 assert_eq!(extract_inferred_type("Let x: Int (inferred)"), "Int");
189 assert_eq!(extract_inferred_type("Let x: Text (inferred)"), "Text");
190 assert_eq!(extract_inferred_type("Let x: auto (inferred)"), "auto");
191 assert_eq!(extract_inferred_type("Let x: Int"), "auto"); }
193
194 #[test]
195 fn inlay_hints_respects_range_parameter() {
196 let doc = make_doc("## Main\n Let x be 5.\n Let y be 10.\n");
197 let range = tower_lsp::lsp_types::Range {
198 start: tower_lsp::lsp_types::Position { line: 1, character: 0 },
199 end: tower_lsp::lsp_types::Position { line: 1, character: 99 },
200 };
201 let restricted_hints = inlay_hints(&doc, range);
202 let all_hints = inlay_hints(&doc, Range::default());
203 assert!(restricted_hints.len() <= all_hints.len(),
204 "Restricted range should have <= hints: {} vs {}", restricted_hints.len(), all_hints.len());
205 }
206
207 #[test]
208 fn inlay_hint_borrowed_after_show() {
209 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
210 let hints = inlay_hints(&doc, Range::default());
211 let borrowed_hints: Vec<_> = hints.iter()
212 .filter(|h| matches!(&h.label, InlayHintLabel::String(s) if s.contains("borrowed")))
213 .collect();
214 if doc.ownership_states.get("x").map_or(false, |s| matches!(s, VarState::Borrowed)) {
216 assert!(
217 !borrowed_hints.is_empty(),
218 "Should have 'borrowed' inlay hint for x after Show"
219 );
220 }
221 }
222
223 #[test]
224 fn inlay_hint_no_marker_for_owned() {
225 let doc = make_doc("## Main\n Let x be 5.\n");
226 let hints = inlay_hints(&doc, Range::default());
227 let ownership_hints: Vec<_> = hints.iter()
228 .filter(|h| matches!(&h.label, InlayHintLabel::String(s) if s.contains("moved") || s.contains("borrowed")))
229 .collect();
230 assert!(
232 ownership_hints.is_empty(),
233 "Owned variables should not have ownership inlay hints. Got: {:?}",
234 ownership_hints.iter().map(|h| &h.label).collect::<Vec<_>>()
235 );
236 }
237}