1use tower_lsp::lsp_types::{
2 CompletionItem, CompletionItemKind, CompletionItemTag, CompletionResponse, Position,
3};
4
5use logicaffeine_compile::analysis::VarState;
6use logicaffeine_language::token::TokenType;
7
8use crate::document::DocumentState;
9use crate::index::DefinitionKind;
10
11pub fn completions(doc: &DocumentState, position: Position) -> Option<CompletionResponse> {
15 let offset = doc.line_index.offset(position);
16
17 let prev_token = doc
19 .tokens
20 .iter()
21 .rev()
22 .find(|t| t.span.end <= offset)?;
23
24 let mut items = Vec::new();
25
26 match &prev_token.kind {
27 TokenType::Period | TokenType::Indent | TokenType::Newline => {
29 add_statement_keywords(&mut items);
30 }
31
32 TokenType::Be => {
34 add_expression_completions(doc, &mut items);
35 }
36
37 TokenType::Colon => {
39 add_type_completions(doc, &mut items);
40 }
41
42 TokenType::Possessive => {
44 add_field_completions(doc, offset, &mut items);
46 }
47
48 TokenType::Inspect => {
50 add_variant_completions(doc, &mut items);
51 }
52
53 _ => {
55 add_identifier_completions(doc, &mut items);
56 }
57 }
58
59 if items.is_empty() {
60 add_identifier_completions(doc, &mut items);
62 add_statement_keywords(&mut items);
63 }
64
65 add_stdlib_completions(doc, &mut items);
68
69 Some(CompletionResponse::Array(items))
70}
71
72fn add_statement_keywords(items: &mut Vec<CompletionItem>) {
73 let keywords = [
77 (TokenType::Let, "Let ${1:name} be ${2:value}."),
78 (TokenType::Set, "Set ${1:name} to ${2:value}."),
79 (TokenType::If, "If ${1:condition}:\n ${2:body}"),
80 (TokenType::While, "While ${1:condition}:\n ${2:body}"),
81 (TokenType::Repeat, "Repeat for ${1:item} in ${2:collection}:\n ${3:body}"),
82 (TokenType::Return, "Return ${1:value}."),
83 (TokenType::Show, "Show ${1:value}."),
84 (TokenType::Give, "Give ${1:value} to ${2:target}."),
85 (TokenType::Push, "Push ${1:value} to ${2:list}."),
86 (TokenType::Call, "Call ${1:function} with ${2:args}."),
87 (TokenType::Inspect, "Inspect ${1:value}:\n ${2:pattern}:\n ${3:body}"),
88 ];
89
90 for (kind, snippet) in keywords {
91 let lesson = logicaffeine_language::teach::doc_for(&kind)
92 .expect("every statement-keyword completion is taught");
93 items.push(CompletionItem {
94 label: lesson.name.to_string(),
95 kind: Some(CompletionItemKind::KEYWORD),
96 detail: Some(lesson.what.to_string()),
97 documentation: Some(crate::teach_md::completion_docs(lesson)),
98 insert_text: Some(snippet.to_string()),
99 insert_text_format: Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
100 ..Default::default()
101 });
102 }
103}
104
105fn add_expression_completions(doc: &DocumentState, items: &mut Vec<CompletionItem>) {
106 add_identifier_completions(doc, items);
108
109 for (name, typedef) in doc.type_registry.iter_types() {
111 let type_name = doc.interner.resolve(*name);
112 if matches!(typedef, logicaffeine_language::analysis::TypeDef::Struct { .. }) {
113 items.push(CompletionItem {
114 label: format!("a new {}", type_name),
115 kind: Some(CompletionItemKind::CONSTRUCTOR),
116 detail: Some(format!("Create a new {} instance", type_name)),
117 ..Default::default()
118 });
119 }
120 }
121}
122
123fn add_type_completions(doc: &DocumentState, items: &mut Vec<CompletionItem>) {
124 let primitives = ["Int", "Nat", "Text", "Bool", "Float", "Unit", "Char", "Byte"];
126 for prim in primitives {
127 let lesson = logicaffeine_language::teach::doc_for_primitive(prim)
128 .expect("every offered primitive is taught");
129 items.push(CompletionItem {
130 label: prim.to_string(),
131 kind: Some(CompletionItemKind::TYPE_PARAMETER),
132 detail: Some(lesson.what.to_string()),
133 documentation: Some(crate::teach_md::completion_docs(lesson)),
134 ..Default::default()
135 });
136 }
137
138 let generics = ["List", "Seq", "Map", "Set", "Option", "Result"];
140 for gen in generics {
141 let lesson = logicaffeine_language::teach::doc_for_primitive(gen)
142 .expect("every offered generic is taught");
143 items.push(CompletionItem {
144 label: gen.to_string(),
145 kind: Some(CompletionItemKind::CLASS),
146 detail: Some(lesson.what.to_string()),
147 documentation: Some(crate::teach_md::completion_docs(lesson)),
148 ..Default::default()
149 });
150 }
151
152 for (name, _typedef) in doc.type_registry.iter_types() {
154 let type_name = doc.interner.resolve(*name);
155 items.push(CompletionItem {
156 label: type_name.to_string(),
157 kind: Some(CompletionItemKind::CLASS),
158 detail: Some("User-defined type".to_string()),
159 ..Default::default()
160 });
161 }
162}
163
164fn add_field_completions(doc: &DocumentState, offset: usize, items: &mut Vec<CompletionItem>) {
165 if let Some(type_name) = resolve_possessive_type(doc, offset) {
167 for (name, typedef) in doc.type_registry.iter_types() {
169 let tname = doc.interner.resolve(*name);
170 if tname != type_name {
171 continue;
172 }
173 if let logicaffeine_language::analysis::TypeDef::Struct { fields, .. } = typedef {
174 for field in fields {
175 let field_name = doc.interner.resolve(field.name);
176 items.push(CompletionItem {
177 label: field_name.to_string(),
178 kind: Some(CompletionItemKind::FIELD),
179 detail: Some(format!("{}: {}", field_name, crate::hover::format_field_type(&field.ty, &doc.interner))),
180 ..Default::default()
181 });
182 }
183 }
184 }
185 }
186
187 if items.iter().all(|i| i.kind != Some(CompletionItemKind::FIELD)) {
189 for (_name, typedef) in doc.type_registry.iter_types() {
190 if let logicaffeine_language::analysis::TypeDef::Struct { fields, .. } = typedef {
191 for field in fields {
192 let field_name = doc.interner.resolve(field.name);
193 items.push(CompletionItem {
194 label: field_name.to_string(),
195 kind: Some(CompletionItemKind::FIELD),
196 detail: Some(format!("{}", field_name)),
197 ..Default::default()
198 });
199 }
200 }
201 }
202 }
203}
204
205fn resolve_possessive_type(doc: &DocumentState, offset: usize) -> Option<String> {
207 let prev_token = doc
209 .tokens
210 .iter()
211 .rev()
212 .find(|t| t.span.end <= offset && !matches!(t.kind, TokenType::Possessive))?;
213 let var_name = doc.source.get(prev_token.span.start..prev_token.span.end)?;
214
215 let defs = doc.symbol_index.definitions_of(var_name);
217 let def = defs.first()?;
218
219 let detail = def.detail.as_ref()?;
221 let after_colon = detail.rsplit_once(": ")?;
223 let type_name = after_colon.1.trim();
224 let type_name = type_name.split(':').next().unwrap_or(type_name).trim();
226 if type_name.is_empty() || type_name == "inferred" {
227 return None;
228 }
229 Some(type_name.to_string())
230}
231
232fn add_variant_completions(doc: &DocumentState, items: &mut Vec<CompletionItem>) {
233 for (_name, typedef) in doc.type_registry.iter_types() {
236 if let logicaffeine_language::analysis::TypeDef::Enum { variants, .. } = typedef {
237 for variant in variants {
238 let variant_name = doc.interner.resolve(variant.name);
239 items.push(CompletionItem {
240 label: variant_name.to_string(),
241 kind: Some(CompletionItemKind::ENUM_MEMBER),
242 ..Default::default()
243 });
244 }
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::document::DocumentState;
253
254 fn make_doc(source: &str) -> DocumentState {
255 DocumentState::new(source.to_string(), 1)
256 }
257
258 #[test]
259 fn completion_returns_items() {
260 let doc = make_doc("## Main\n Let x be 5.\n");
261 let pos = Position { line: 1, character: 18 };
263 let result = completions(&doc, pos);
264 assert!(result.is_some(), "Expected completion response");
265 if let Some(CompletionResponse::Array(items)) = result {
266 assert!(!items.is_empty(), "Expected non-empty completions");
267 }
268 }
269
270 #[test]
271 fn completion_includes_keywords_after_period() {
272 let doc = make_doc("## Main\n Let x be 5.\n ");
275 let pos = Position { line: 2, character: 4 };
277 let result = completions(&doc, pos);
278 if let Some(CompletionResponse::Array(items)) = result {
279 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
280 let has_keywords = labels.contains(&"Let") || labels.contains(&"Show");
282 let has_identifiers = labels.contains(&"x");
283 assert!(
284 has_keywords || has_identifiers,
285 "Should include keywords or identifiers: {:?}",
286 labels
287 );
288 }
289 }
290
291 #[test]
292 fn completion_includes_variables_in_default_context() {
293 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
294 let pos = Position { line: 2, character: 9 };
297 let result = completions(&doc, pos);
298 if let Some(CompletionResponse::Array(items)) = result {
299 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
300 assert!(labels.contains(&"x"), "Should include variable 'x': {:?}", labels);
301 }
302 }
303
304 #[test]
305 fn completion_no_crash_empty_doc() {
306 let doc = make_doc("");
307 let pos = Position { line: 0, character: 0 };
308 let result = completions(&doc, pos);
309 match result {
313 None => {} Some(CompletionResponse::Array(items)) => {
315 assert!(!items.is_empty(), "If result is Some, it should have items");
317 }
318 _ => panic!("Unexpected response type"),
319 }
320 }
321
322 #[test]
323 fn completion_after_colon_type_completions() {
324 let doc = make_doc("## Main\n Let x: Int be 5.\n");
325 let pos = Position { line: 1, character: 10 };
328 let result = completions(&doc, pos);
329 if let Some(CompletionResponse::Array(items)) = result {
330 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
331 let has_types = labels.contains(&"Int") || labels.contains(&"Text") || labels.contains(&"Bool");
332 assert!(has_types, "After colon should include type completions: {:?}", labels);
333 }
334 }
335
336 #[test]
337 fn completion_items_have_correct_kind() {
338 let doc = make_doc("## Main\n Let x be 5.\n ");
339 let pos = Position { line: 2, character: 4 };
340 let result = completions(&doc, pos);
341 if let Some(CompletionResponse::Array(items)) = result {
342 let keyword_items: Vec<_> = items.iter()
343 .filter(|i| i.label == "Let" || i.label == "Show")
344 .collect();
345 for item in &keyword_items {
346 assert_eq!(item.kind, Some(CompletionItemKind::KEYWORD),
347 "'{}' should have KEYWORD kind", item.label);
348 }
349 }
350 }
351
352 #[test]
353 fn completion_snippets_have_snippet_format() {
354 let doc = make_doc("## Main\n ");
355 let pos = Position { line: 1, character: 4 };
356 let result = completions(&doc, pos);
357 if let Some(CompletionResponse::Array(items)) = result {
358 let keyword_items: Vec<_> = items.iter()
359 .filter(|i| i.label == "Let" || i.label == "If")
360 .collect();
361 for item in &keyword_items {
362 assert_eq!(item.insert_text_format,
363 Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
364 "'{}' should have SNIPPET format", item.label);
365 }
366 }
367 }
368
369 #[test]
370 fn keyword_completions_teach_with_detail_and_documentation() {
371 let doc = make_doc("## Main\n ");
372 let pos = Position { line: 1, character: 4 };
373 let Some(CompletionResponse::Array(items)) = completions(&doc, pos) else {
374 panic!("expected completions after indent");
375 };
376 for label in [
377 "Let", "Set", "If", "While", "Repeat", "Return", "Show", "Give", "Push", "Call",
378 "Inspect",
379 ] {
380 let item = items
381 .iter()
382 .find(|i| i.label == label && i.kind == Some(CompletionItemKind::KEYWORD))
383 .unwrap_or_else(|| panic!("keyword completion for {label}"));
384 let lesson = logicaffeine_language::teach::doc_for_word(label)
385 .unwrap_or_else(|| panic!("{label} must be taught"));
386 assert_eq!(
387 item.detail.as_deref(),
388 Some(lesson.what),
389 "{label}: completion detail must be the lesson's one-liner"
390 );
391 assert!(
392 item.documentation.is_some(),
393 "{label}: completion must carry teaching documentation"
394 );
395 }
396 }
397
398 #[test]
399 fn primitive_type_completions_teach_from_the_lesson_table() {
400 let doc = make_doc("## Main\n Let x: Int be 5.\n");
401 let pos = Position { line: 1, character: 10 };
403 let Some(CompletionResponse::Array(items)) = completions(&doc, pos) else {
404 panic!("expected type completions after colon");
405 };
406 let int_item = items
407 .iter()
408 .find(|i| i.label == "Int")
409 .expect("Int must be offered after a colon");
410 let lesson = logicaffeine_language::teach::doc_for_primitive("Int").unwrap();
411 assert_eq!(
412 int_item.detail.as_deref(),
413 Some(lesson.what),
414 "Int detail must come from the lesson table, not a generic placeholder"
415 );
416 assert!(
417 int_item.documentation.is_some(),
418 "Int completion must carry teaching documentation"
419 );
420 }
421
422 #[test]
423 fn documented_function_completion_carries_its_prose() {
424 let doc = make_doc(
425 "## Note\nDoubles a number.\n\n## To double (n: Int) -> Int:\n Return n * 2.\n\n## Main\nShow x.\n",
426 );
427 let pos = Position { line: 7, character: 5 };
429 let Some(CompletionResponse::Array(items)) = completions(&doc, pos) else {
430 panic!("expected completions");
431 };
432 let item = items
433 .iter()
434 .find(|i| i.label == "double")
435 .expect("double completes");
436 let Some(tower_lsp::lsp_types::Documentation::MarkupContent(content)) =
437 &item.documentation
438 else {
439 panic!("documented function must carry markdown docs");
440 };
441 assert!(content.value.contains("Doubles a number."), "{}", content.value);
442 }
443
444 #[test]
445 fn stdlib_names_complete_with_their_documentation() {
446 let doc = make_doc("## Main\n Show x.\n");
447 let pos = Position { line: 1, character: 9 };
448 let Some(CompletionResponse::Array(items)) = completions(&doc, pos) else {
449 panic!("expected completions");
450 };
451 let md5 = items.iter().find(|i| i.label == "md5").expect("md5 offered from stdlib");
452 assert_eq!(md5.kind, Some(CompletionItemKind::FUNCTION));
453 assert!(
454 md5.detail.as_deref().is_some_and(|d| d.contains("md5")),
455 "{:?}",
456 md5.detail
457 );
458 assert!(md5.documentation.is_some(), "stdlib completion teaches");
459 }
460
461 #[test]
462 fn variant_completion_no_crash_with_inspect() {
463 let doc = make_doc("## Main\n Let x be 5.\n Inspect x:\n");
465 let pos = Position { line: 2, character: 14 };
466 let result = completions(&doc, pos);
467 assert!(result.is_some(), "Should return some completions after Inspect");
469 }
470
471 #[test]
472 fn completion_moved_variable_has_deprecated_tag() {
473 use logicaffeine_compile::analysis::VarState;
474 let mut doc = make_doc("## Main\n Let x be 5.\n Let y be 0.\n Give x to y.\n Show x.\n");
475 doc.ownership_states.insert("x".to_string(), VarState::Moved);
477 let pos = Position { line: 4, character: 9 };
479 let result = completions(&doc, pos);
480 if let Some(CompletionResponse::Array(items)) = result {
481 let x_items: Vec<_> = items.iter().filter(|i| i.label == "x").collect();
482 if !x_items.is_empty() {
483 assert_eq!(
484 x_items[0].deprecated,
485 Some(true),
486 "Moved variable 'x' should be marked deprecated"
487 );
488 assert!(
489 x_items[0].tags.as_ref().map_or(false, |t| t.contains(&CompletionItemTag::DEPRECATED)),
490 "Moved variable should have DEPRECATED tag"
491 );
492 assert!(
493 x_items[0].detail.as_ref().map_or(false, |d| d.contains("(moved)")),
494 "Moved variable detail should indicate moved: {:?}",
495 x_items[0].detail
496 );
497 }
498 }
499 }
500
501 #[test]
502 fn completion_owned_variable_no_tag() {
503 let doc = make_doc("## Main\n Let x be 5.\n Show x.\n");
504 let pos = Position { line: 2, character: 9 };
505 let result = completions(&doc, pos);
506 if let Some(CompletionResponse::Array(items)) = result {
507 let x_items: Vec<_> = items.iter().filter(|i| i.label == "x").collect();
508 if !x_items.is_empty() {
509 assert!(
510 x_items[0].deprecated.is_none() || x_items[0].deprecated == Some(false),
511 "Owned variable should not be deprecated"
512 );
513 }
514 }
515 }
516}
517
518fn add_identifier_completions(doc: &DocumentState, items: &mut Vec<CompletionItem>) {
519 for def in &doc.symbol_index.definitions {
520 let kind = match def.kind {
521 DefinitionKind::Variable => CompletionItemKind::VARIABLE,
522 DefinitionKind::Function => CompletionItemKind::FUNCTION,
523 DefinitionKind::Struct => CompletionItemKind::CLASS,
524 DefinitionKind::Enum => CompletionItemKind::ENUM,
525 DefinitionKind::Field => CompletionItemKind::FIELD,
526 DefinitionKind::Parameter => CompletionItemKind::VARIABLE,
527 DefinitionKind::Block => CompletionItemKind::MODULE,
528 DefinitionKind::Variant => CompletionItemKind::ENUM_MEMBER,
529 DefinitionKind::Theorem => CompletionItemKind::CLASS,
530 };
531
532 let is_moved = doc.ownership_states.get(&def.name).map_or(false, |state| {
534 matches!(state, VarState::Moved | VarState::MaybeMoved)
535 });
536
537 let mut detail = def.detail.clone();
538 let mut tags = None;
539 let mut deprecated = None;
540 if is_moved {
541 deprecated = Some(true);
542 tags = Some(vec![CompletionItemTag::DEPRECATED]);
543 detail = Some(format!(
544 "{} (moved)",
545 detail.as_deref().unwrap_or(&def.name)
546 ));
547 }
548
549 items.push(CompletionItem {
550 label: def.name.clone(),
551 kind: Some(kind),
552 detail,
553 documentation: def.doc.clone().map(markdown_docs),
554 deprecated,
555 tags,
556 ..Default::default()
557 });
558 }
559}
560
561fn add_stdlib_completions(doc: &DocumentState, items: &mut Vec<CompletionItem>) {
565 for (name, entry) in crate::stdlib_docs::all() {
566 if doc.symbol_index.name_to_defs.contains_key(name) {
567 continue;
568 }
569 items.push(CompletionItem {
570 label: name.to_string(),
571 kind: Some(if entry.is_type {
572 CompletionItemKind::CLASS
573 } else {
574 CompletionItemKind::FUNCTION
575 }),
576 detail: Some(entry.signature.trim_start_matches("## ").to_string()),
577 documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
578 tower_lsp::lsp_types::MarkupContent {
579 kind: tower_lsp::lsp_types::MarkupKind::Markdown,
580 value: crate::stdlib_docs::hover_md(name, entry),
581 },
582 )),
583 ..Default::default()
584 });
585 }
586}
587
588fn markdown_docs(value: String) -> tower_lsp::lsp_types::Documentation {
590 tower_lsp::lsp_types::Documentation::MarkupContent(tower_lsp::lsp_types::MarkupContent {
591 kind: tower_lsp::lsp_types::MarkupKind::Markdown,
592 value,
593 })
594}