1use tower_lsp::lsp_types::{
2 ParameterInformation, ParameterLabel, Position, SignatureHelp, SignatureInformation,
3};
4
5use logicaffeine_language::token::TokenType;
6
7use crate::document::DocumentState;
8use crate::index::DefinitionKind;
9
10pub fn signature_help(doc: &DocumentState, position: Position) -> Option<SignatureHelp> {
15 let offset = doc.line_index.offset(position);
16
17 let call_token = doc.tokens.iter().rev().find(|t| {
19 t.span.end <= offset && matches!(t.kind, TokenType::Call)
20 })?;
21
22 let call_idx = doc.tokens.iter().position(|t| {
24 t.span == call_token.span && t.kind == call_token.kind
25 })?;
26
27 let func_name_token = doc.tokens.get(call_idx + 1)?;
29 let func_name = doc.source.get(func_name_token.span.start..func_name_token.span.end)?;
30
31 let defs = doc.symbol_index.definitions_of(func_name);
34 let func_def = defs.iter().find(|d| d.kind == DefinitionKind::Function);
35 let (detail, documentation) = match func_def {
36 Some(def) => (def.detail.clone()?, def.doc.clone()),
37 None => {
38 let entry = crate::stdlib_docs::stdlib_doc(func_name).filter(|e| !e.is_type)?;
39 (entry.signature.trim_start_matches("## ").to_string(), entry.doc.clone())
40 }
41 };
42 let detail = &detail;
43
44 let active_param = doc.tokens[call_idx..]
47 .iter()
48 .take_while(|t| t.span.start < offset)
49 .filter(|t| {
50 matches!(t.kind, TokenType::Comma)
51 || doc
52 .source
53 .get(t.span.start..t.span.end)
54 .map(|s| s == "and")
55 .unwrap_or(false)
56 })
57 .count();
58
59 let params: Vec<ParameterInformation> = extract_params_from_signature(detail)
61 .into_iter()
62 .map(|(name, ty)| ParameterInformation {
63 label: ParameterLabel::Simple(name.clone()),
64 documentation: Some(tower_lsp::lsp_types::Documentation::String(
65 format!("{}: {}", name, ty),
66 )),
67 })
68 .collect();
69
70 Some(SignatureHelp {
71 signatures: vec![SignatureInformation {
72 label: detail.clone(),
73 documentation: documentation.map(|d| {
74 tower_lsp::lsp_types::Documentation::MarkupContent(
75 tower_lsp::lsp_types::MarkupContent {
76 kind: tower_lsp::lsp_types::MarkupKind::Markdown,
77 value: d,
78 },
79 )
80 }),
81 parameters: if params.is_empty() {
82 None
83 } else {
84 Some(params)
85 },
86 active_parameter: Some(active_param as u32),
87 }],
88 active_signature: Some(0),
89 active_parameter: Some(active_param as u32),
90 })
91}
92
93fn extract_params_from_signature(detail: &str) -> Vec<(String, String)> {
99 let head = detail.split("->").next().unwrap_or(detail);
100 let mut params = Vec::new();
101 let mut rest = head;
102 while let Some(open) = rest.find('(') {
103 let Some(close) = rest[open + 1..].find(')') else { break };
104 let group = &rest[open + 1..open + 1 + close];
105 for part in group.split(',') {
106 let part = part.trim();
107 if part.is_empty() {
108 continue;
109 }
110 let mut split = part.splitn(2, ':');
111 let Some(name) = split.next().map(|s| s.trim().to_string()) else { continue };
112 let ty = split
113 .next()
114 .map(|s| s.trim().to_string())
115 .unwrap_or_else(|| "auto".to_string());
116 params.push((name, ty));
117 }
118 rest = &rest[open + 1 + close + 1..];
119 }
120 params
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::document::DocumentState;
127
128 fn make_doc(source: &str) -> DocumentState {
129 DocumentState::new(source.to_string(), 1)
130 }
131
132 #[test]
133 fn signature_help_returns_none_without_call() {
134 let doc = make_doc("## Main\n Let x be 5.\n");
135 let pos = Position { line: 1, character: 10 };
136 let result = signature_help(&doc, pos);
137 assert!(result.is_none(), "Should return None when not in a Call expression");
138 }
139
140 #[test]
141 fn signature_help_no_crash_empty_doc() {
142 let doc = make_doc("");
143 let pos = Position { line: 0, character: 0 };
144 let result = signature_help(&doc, pos);
145 assert!(result.is_none());
146 }
147
148 #[test]
149 fn signature_help_no_crash_on_out_of_bounds() {
150 let doc = make_doc("## Main\n Let x be 5.\n");
151 let pos = Position { line: 5, character: 0 };
152 let result = signature_help(&doc, pos);
153 assert!(result.is_none(), "OOB should return None");
154 }
155
156 #[test]
157 fn extract_params_basic() {
158 let params = extract_params_from_signature("To add(a: Int, b: Int) -> Int");
159 assert_eq!(params.len(), 2);
160 assert_eq!(params[0], ("a".to_string(), "Int".to_string()));
161 assert_eq!(params[1], ("b".to_string(), "Int".to_string()));
162 }
163
164 #[test]
165 fn extract_params_multiple() {
166 let params = extract_params_from_signature("To greet(name: Text, age: Int, loud: Bool) -> Text");
167 assert_eq!(params.len(), 3);
168 assert_eq!(params[0].0, "name");
169 assert_eq!(params[1].0, "age");
170 assert_eq!(params[2].0, "loud");
171 }
172
173 #[test]
174 fn extract_params_empty() {
175 let params = extract_params_from_signature("To noop() -> Unit");
176 assert!(params.is_empty());
177 }
178
179 #[test]
180 fn extract_params_no_parens() {
181 let params = extract_params_from_signature("something without parens");
182 assert!(params.is_empty());
183 }
184
185 #[test]
186 fn signature_help_returns_signature_for_defined_function() {
187 let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
188 let doc = make_doc(source);
189 let pos = Position { line: 4, character: 30 };
190 let result = signature_help(&doc, pos);
191 if let Some(help) = &result {
192 assert!(!help.signatures.is_empty(), "Should have a signature");
193 let sig = &help.signatures[0];
194 if let Some(params) = &sig.parameters {
196 let names: Vec<&str> = params
197 .iter()
198 .map(|p| match &p.label {
199 ParameterLabel::Simple(s) => s.as_str(),
200 _ => "",
201 })
202 .collect();
203 assert!(names.contains(&"a"), "Should include param 'a': {:?}", names);
204 assert!(names.contains(&"b"), "Should include param 'b': {:?}", names);
205 }
206 }
207 }
208
209 #[test]
210 fn active_parameter_tracking() {
211 let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
212 let doc = make_doc(source);
213 let pos = Position { line: 4, character: 35 };
215 if let Some(help) = signature_help(&doc, pos) {
216 let active = help.active_parameter.unwrap_or(0);
217 assert!(active >= 1, "After 'and' separator, active_parameter should be >= 1, got {}", active);
218 }
219 }
220
221 #[test]
222 fn call_not_found_returns_none() {
223 let doc = make_doc("## Main\n Let x be 5.\n");
224 let pos = Position { line: 1, character: 4 };
226 let result = signature_help(&doc, pos);
227 assert!(result.is_none(), "Should return None when no Call precedes position");
228 }
229
230 #[test]
231 fn with_not_counted_as_separator() {
232 let source = "## To add with a: Int and b: Int\n Show a.\n\n## Main\n Let r be Call add with 1 and 2.\n";
236 let doc = make_doc(source);
237 let pos = Position { line: 4, character: 27 };
242 if let Some(help) = signature_help(&doc, pos) {
243 let active = help.active_parameter.unwrap_or(99);
244 assert_eq!(active, 0, "Before 'and', active_parameter should be 0 (with not counted), got {}", active);
245 }
246 }
247
248 #[test]
249 fn signature_help_documents_from_the_note_prose() {
250 let source = "## Note\nAdds two integers.\n\n## To compute (a: Int, b: Int) -> Int:\n Return a + b.\n\n## Main\n Let r be Call compute with 1 and 2.\n";
251 let doc = make_doc(source);
252 let pos = Position { line: 7, character: 34 };
253 let help = signature_help(&doc, pos).expect("documented function signature");
254 let sig = &help.signatures[0];
255 let Some(tower_lsp::lsp_types::Documentation::MarkupContent(content)) =
256 &sig.documentation
257 else {
258 panic!("the ## Note prose must document the signature");
259 };
260 assert!(content.value.contains("Adds two integers."), "{}", content.value);
261 }
262
263 #[test]
264 fn signature_help_falls_back_to_the_stdlib() {
265 let source = "## Main\n Let r be Call randomInt with 1 and 6.\n";
266 let doc = make_doc(source);
267 let pos = Position { line: 1, character: 32 };
268 let help = signature_help(&doc, pos).expect("stdlib names answer signature help");
269 let sig = &help.signatures[0];
270 assert!(sig.label.contains("randomInt"), "{}", sig.label);
271 let params = sig.parameters.as_ref().expect("min and max parameters");
272 assert_eq!(params.len(), 2, "prepositional groups both count: {params:?}");
273 assert!(sig.documentation.is_some(), "the literate Note teaches");
274 }
275
276 #[test]
277 fn multi_group_signatures_yield_every_parameter() {
278 let params = extract_params_from_signature(
279 "To native write (path: Text) and (content: Text) -> Result of Unit and Text",
280 );
281 assert_eq!(params.len(), 2);
282 assert_eq!(params[0].0, "path");
283 assert_eq!(params[1].0, "content");
284 }
285
286 #[test]
287 fn signature_help_finds_function_via_span_not_pointer() {
288 let source = "## To add(a: Int, b: Int) -> Int\n Return a + b.\n\n## Main\n Let r be Call add with 1 and 2.\n";
291 let doc = make_doc(source);
292 let pos = Position { line: 4, character: 30 };
294 let result = signature_help(&doc, pos);
295 if let Some(help) = &result {
298 assert!(!help.signatures.is_empty(), "Should have at least one signature");
299 }
300 }
301}