1pub mod annotations;
8pub mod docs;
9pub mod functions;
10pub mod imports;
11pub mod inference;
12pub mod methods;
13pub mod providers;
14pub mod snippets;
15pub mod stdlib_methods;
16pub mod types;
17
18pub use annotations::{annotation_completions, enum_value_completions, symbols_with_annotation};
20pub use functions::{
21 builtin_function_completions, comptime_builtin_function_completions,
22 function_argument_completions, function_completion_item, keyword_completions,
23 object_property_name_completions, object_property_value_completions,
24};
25pub use inference::{infer_param_types, infer_types, infer_types_with_context, type_to_string};
26pub use methods::{
27 extract_option_inner, extract_result_inner, method_completion_item, option_method_completions,
28 result_method_completions,
29};
30pub use providers::provider_completions;
31pub use snippets::{create_snippet, snippet_completions};
32pub use types::{
33 is_column_type, pipe_target_completions, property_completion_item, property_completions,
34 resolve_base_type, resolve_object_type, resolve_property_type, type_completions,
35};
36
37use crate::annotation_discovery::AnnotationDiscovery;
38use crate::context::{CompletionContext, analyze_context, is_inside_interpolation_expression};
39use crate::grammar_completion::get_grammar_completions;
40use crate::module_cache::ModuleCache;
41use crate::symbols::{SymbolKind, extract_symbols, symbols_to_completions};
42use crate::trait_lookup::resolve_trait_definition;
43use crate::type_inference::{
44 MethodCompletionInfo, extract_struct_fields, extract_type_methods, unified_metadata,
45};
46use crate::util::position_to_offset;
47use shape_ast::ast::{Item, MethodDef, Program, Span, Statement, TypeName};
48use shape_ast::parse_program_resilient;
49use shape_ast::parser::parse_program;
50use std::collections::{HashMap, HashSet};
51use std::path::Path;
52use tower_lsp_server::ls_types::{CompletionItem, CompletionItemKind, Position};
53
54pub fn get_completions(
57 text: &str,
58 position: Position,
59 cached_symbols: &[crate::symbols::SymbolInfo],
60 cached_types: &HashMap<String, String>,
61) -> (
62 Vec<CompletionItem>,
63 Option<Vec<crate::symbols::SymbolInfo>>,
64 Option<HashMap<String, String>>,
65) {
66 get_completions_with_context(
67 text,
68 position,
69 cached_symbols,
70 cached_types,
71 None,
72 None,
73 None,
74 )
75}
76
77pub fn get_completions_with_context(
79 text: &str,
80 position: Position,
81 cached_symbols: &[crate::symbols::SymbolInfo],
82 cached_types: &HashMap<String, String>,
83 module_cache: Option<&ModuleCache>,
84 current_file: Option<&Path>,
85 workspace_root: Option<&Path>,
86) -> (
87 Vec<CompletionItem>,
88 Option<Vec<crate::symbols::SymbolInfo>>,
89 Option<HashMap<String, String>>,
90) {
91 let mut completions = Vec::new();
92 let cursor_offset = position_to_offset(text, position);
93 let mut parsed_program = None;
94
95 let context = analyze_context(text, position);
97
98 let (
100 user_symbols,
101 updated_symbols,
102 updated_types,
103 annotation_discovery,
104 struct_fields,
105 impl_methods,
106 named_impls,
107 receiver_type_at_cursor,
108 ) = if let Ok(mut program) = parse_program(text) {
109 let analysis = analyze_parsed_program(
110 &mut program,
111 module_cache,
112 current_file,
113 workspace_root,
114 text,
115 cursor_offset,
116 );
117 parsed_program = Some(program);
118 analysis
119 } else {
120 let partial = parse_program_resilient(text);
122 if !partial.items.is_empty() {
123 let mut program = partial.into_program();
124 let analysis = analyze_parsed_program(
125 &mut program,
126 module_cache,
127 current_file,
128 workspace_root,
129 text,
130 cursor_offset,
131 );
132 parsed_program = Some(program);
133 analysis
134 } else {
135 (
137 cached_symbols.to_vec(),
138 None,
139 None,
140 AnnotationDiscovery::new(),
141 HashMap::new(),
142 HashMap::new(),
143 extract_named_impl_names_fallback(text),
144 None,
145 )
146 }
147 };
148
149 let mut type_context = updated_types
150 .clone()
151 .unwrap_or_else(|| cached_types.clone());
152 if is_inside_interpolation_expression(text, position) {
153 if let Some(receiver_type) = receiver_type_at_cursor {
154 type_context.insert("self".to_string(), receiver_type);
155 }
156 }
157
158 if is_using_impl_selector_context(text, position) {
159 completions.extend(named_impl_selector_completions(&named_impls));
160 return (completions, updated_symbols, updated_types);
161 }
162
163 match context {
164 CompletionContext::ImportModule => {
165 completions.extend(imports::import_module_completions_with_context(
166 current_file,
167 workspace_root,
168 Some(text),
169 ));
170 }
171 CompletionContext::FromModule => {
172 completions.extend(imports::from_module_completions_with_context(
173 module_cache,
174 current_file,
175 workspace_root,
176 ));
177 }
178 CompletionContext::FromModulePartial { prefix } => {
179 completions.extend(imports::hierarchical_module_completions_with_context(
180 &prefix,
181 module_cache,
182 current_file,
183 workspace_root,
184 ));
185 }
186 CompletionContext::ImportItems { module } => {
187 let module_exports = imports::module_export_completions_with_context(
188 &module,
189 current_file,
190 workspace_root,
191 Some(text),
192 );
193 if !module_exports.is_empty() {
194 completions.extend(module_exports);
195 } else {
196 completions.extend(imports::import_path_export_completions_with_context(
198 &module,
199 module_cache,
200 current_file,
201 workspace_root,
202 ));
203 }
204 }
205 CompletionContext::PropertyAccess { object } => {
206 if imports::is_module_namespace_with_context(
208 &object,
209 current_file,
210 workspace_root,
211 Some(text),
212 ) {
213 completions.extend(imports::module_member_completions_with_context(
214 &object,
215 current_file,
216 workspace_root,
217 Some(text),
218 ));
219 return (completions, updated_symbols, updated_types);
220 }
221 completions.extend(property_completions(
223 &object,
224 &type_context,
225 &struct_fields,
226 &impl_methods,
227 ));
228 }
229 CompletionContext::PatternReference => {
230 let function_symbols: Vec<_> = user_symbols
232 .iter()
233 .filter(|s| s.kind == SymbolKind::Function)
234 .cloned()
235 .collect();
236 completions.extend(symbols_to_completions(&function_symbols));
237 }
238 CompletionContext::TypeAnnotation => {
239 completions.extend(type_completions());
241 }
242 CompletionContext::FunctionCall {
243 function,
244 arg_context,
245 } => {
246 completions.extend(function_argument_completions(
248 &user_symbols,
249 &function,
250 &arg_context,
251 ));
252 }
253 CompletionContext::Annotation => {
254 completions.extend(annotation_completions(
256 &annotation_discovery,
257 parsed_program.as_ref(),
258 module_cache,
259 current_file,
260 workspace_root,
261 ));
262 }
263 CompletionContext::AnnotationArgs { annotation } => {
264 if let Some(info) = annotation_discovery.get(&annotation) {
267 for param in &info.params {
268 completions.push(CompletionItem {
269 label: param.clone(),
270 kind: Some(CompletionItemKind::VARIABLE),
271 detail: Some(format!("@{} parameter", annotation)),
272 ..Default::default()
273 });
274 }
275 }
276 completions.extend(symbols_to_completions(&user_symbols));
278 }
279 CompletionContext::ComptimeBlock => {
280 completions.extend(comptime_builtin_function_completions());
282 completions.extend(symbols_to_completions(&user_symbols));
283 completions.extend(builtin_function_completions());
284 }
285 CompletionContext::ExprAnnotation => {
286 completions.extend(annotation_completions(
288 &annotation_discovery,
289 parsed_program.as_ref(),
290 module_cache,
291 current_file,
292 workspace_root,
293 ));
294 }
295 CompletionContext::DocTag { prefix } => {
296 completions.extend(docs::doc_tag_completions(&prefix));
297 }
298 CompletionContext::DocParamName { prefix } => {
299 if let (Some(program), Some(offset)) = (parsed_program.as_ref(), cursor_offset) {
300 completions.extend(docs::doc_param_completions(program, offset, &prefix));
301 }
302 }
303 CompletionContext::DocTypeParamName { prefix } => {
304 if let (Some(program), Some(offset)) = (parsed_program.as_ref(), cursor_offset) {
305 completions.extend(docs::doc_type_param_completions(program, offset, &prefix));
306 }
307 }
308 CompletionContext::DocLinkTarget { prefix } => {
309 if let Some(program) = parsed_program.as_ref() {
310 completions.extend(docs::doc_link_completions(
311 program,
312 &prefix,
313 module_cache,
314 current_file,
315 workspace_root,
316 ));
317 }
318 }
319 CompletionContext::PipeTarget { pipe_input_type } => {
320 completions.extend(types::pipe_target_completions(
321 pipe_input_type.as_deref(),
322 &type_context,
323 &impl_methods,
324 ));
325 let func_symbols: Vec<_> = user_symbols
327 .iter()
328 .filter(|s| s.kind == crate::symbols::SymbolKind::Function)
329 .cloned()
330 .collect();
331 completions.extend(symbols_to_completions(&func_symbols));
332 completions.extend(builtin_function_completions());
334 }
335 CompletionContext::ImplBlock {
336 trait_name,
337 target_type: _,
338 existing_methods,
339 } => {
340 completions.extend(impl_block_completions(
342 text,
343 &trait_name,
344 &existing_methods,
345 module_cache,
346 current_file,
347 workspace_root,
348 ));
349 }
350 CompletionContext::TypeAliasOverride { base_type } => {
351 completions.extend(comptime_field_override_completions(
353 &struct_fields,
354 &base_type,
355 ));
356 }
357 CompletionContext::JoinStrategy => {
358 completions.extend(join_strategy_completions());
360 }
361 CompletionContext::JoinBody { strategy } => {
362 completions.extend(join_branch_completions(&strategy));
364 completions.extend(symbols_to_completions(&user_symbols));
366 completions.extend(builtin_function_completions());
367 }
368 CompletionContext::TraitBound => {
369 completions.extend(trait_bound_completions(text));
371 }
372 CompletionContext::InterpolationFormatSpec { spec_prefix } => {
373 completions.extend(interpolation_format_spec_completions(&spec_prefix));
374 }
375 _ => {
376 let byte_offset = position_to_offset(text, position);
380 if let Some(offset) = byte_offset {
381 let truncated = &text[..offset];
382 completions.extend(get_grammar_completions(truncated));
383 }
384
385 completions.extend(all_completions(&user_symbols));
387 }
388 }
389
390 dedupe_completion_items(&mut completions);
392
393 if let Some(expected) = expected_type_at_cursor(text, position, &type_context) {
395 boost_completions_by_type(&mut completions, &expected, &type_context);
396 }
397
398 (completions, updated_symbols, updated_types)
399}
400
401fn is_using_impl_selector_context(text: &str, position: Position) -> bool {
402 let Some(offset) = position_to_offset(text, position) else {
403 return false;
404 };
405 let prefix = &text[..offset];
406 let line_prefix = prefix.rsplit('\n').next().unwrap_or(prefix);
407
408 let Some(using_idx) = line_prefix.rfind("using") else {
409 return false;
410 };
411
412 let before = &line_prefix[..using_idx];
413 if before
414 .chars()
415 .last()
416 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
417 {
418 return false;
419 }
420
421 let after = &line_prefix[using_idx + "using".len()..];
422 if !after
423 .chars()
424 .all(|c| c.is_ascii_whitespace() || c.is_ascii_alphanumeric() || c == '_')
425 {
426 return false;
427 }
428
429 after.chars().any(|c| c.is_ascii_whitespace())
430}
431
432fn named_impl_selector_completions(named_impls: &[String]) -> Vec<CompletionItem> {
433 named_impls
434 .iter()
435 .map(|name| CompletionItem {
436 label: name.clone(),
437 kind: Some(CompletionItemKind::REFERENCE),
438 detail: Some("Named trait implementation".to_string()),
439 documentation: Some(tower_lsp_server::ls_types::Documentation::String(
440 "Select self named implementation with `expr using ImplName`.".to_string(),
441 )),
442 ..Default::default()
443 })
444 .collect()
445}
446
447#[allow(clippy::type_complexity)]
449fn analyze_parsed_program(
450 program: &mut Program,
451 module_cache: Option<&ModuleCache>,
452 current_file: Option<&Path>,
453 workspace_root: Option<&Path>,
454 text: &str,
455 cursor_offset: Option<usize>,
456) -> (
457 Vec<crate::symbols::SymbolInfo>,
458 Option<Vec<crate::symbols::SymbolInfo>>,
459 Option<HashMap<String, String>>,
460 AnnotationDiscovery,
461 HashMap<String, Vec<(String, String)>>,
462 HashMap<String, Vec<MethodCompletionInfo>>,
463 Vec<String>,
464 Option<String>,
465) {
466 shape_ast::transform::desugar_program(program);
468 let symbols = extract_symbols(program);
469 let mut inferred_types =
470 infer_types_with_context(program, current_file, workspace_root, Some(text));
471 if let Some(types) = inferred_types.as_mut() {
472 let param_types = infer_param_types(program, types);
473 types.extend(param_types);
474 }
475
476 let mut ann_discovery = AnnotationDiscovery::new();
478 ann_discovery.discover_from_program(program);
479 if let (Some(cache), Some(file_path)) = (module_cache, current_file) {
480 ann_discovery.discover_from_imports_with_cache(program, file_path, cache, workspace_root);
481 } else {
482 ann_discovery.discover_from_imports(program);
483 }
484
485 let fields = extract_struct_fields(program);
487
488 let mut impl_meths = extract_type_methods(program);
490 let mut named_impl_names: Vec<String> = program
491 .items
492 .iter()
493 .filter_map(|item| match item {
494 Item::Impl(impl_block, _) => impl_block.impl_name.clone(),
495 _ => None,
496 })
497 .collect();
498 named_impl_names.sort();
499 named_impl_names.dedup();
500
501 for (type_name, ext_methods) in imports::extension_type_methods() {
503 let entry = impl_meths.entry(type_name).or_default();
504 for m in ext_methods {
505 if !entry.iter().any(|existing| existing.name == m.name) {
506 entry.push(m);
507 }
508 }
509 }
510
511 for (type_name, std_methods) in stdlib_methods::stdlib_type_methods() {
520 let entry = impl_meths.entry(type_name.clone()).or_default();
521 for m in std_methods {
522 if !entry.iter().any(|existing| existing.name == m.name) {
523 entry.push(m.clone());
524 }
525 }
526 }
527
528 (
529 symbols.clone(),
530 Some(symbols),
531 inferred_types,
532 ann_discovery,
533 fields,
534 impl_meths,
535 named_impl_names,
536 cursor_offset.and_then(|offset| receiver_type_for_offset(program, offset)),
537 )
538}
539
540fn extract_named_impl_names_fallback(text: &str) -> Vec<String> {
541 let tokens: Vec<&str> = text
542 .split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
543 .filter(|s| !s.is_empty())
544 .collect();
545 let mut out = Vec::new();
546
547 for i in 0..tokens.len() {
548 if tokens[i] != "impl" {
549 continue;
550 }
551 let upper = (i + 16).min(tokens.len());
553 for j in (i + 1)..upper {
554 if tokens[j] == "as" && j + 1 < tokens.len() {
555 out.push(tokens[j + 1].to_string());
556 break;
557 }
558 }
559 }
560
561 out.sort();
562 out.dedup();
563 out
564}
565
566fn expected_type_at_cursor(
568 text: &str,
569 position: Position,
570 type_context: &HashMap<String, String>,
571) -> Option<String> {
572 let lines: Vec<&str> = text.lines().collect();
573 let line_idx = position.line as usize;
574 if line_idx >= lines.len() {
575 return None;
576 }
577 let line = lines[line_idx];
578 let char_pos = (position.character as usize).min(line.len());
579 let before = &line[..char_pos];
580 let trimmed = before.trim();
581
582 if let Some(eq_pos) = trimmed.rfind('=') {
585 let is_comparison = eq_pos > 0
587 && matches!(
588 trimmed.as_bytes().get(eq_pos.wrapping_sub(1)),
589 Some(b'=' | b'!' | b'<' | b'>')
590 )
591 || matches!(trimmed.as_bytes().get(eq_pos + 1), Some(b'='));
592 if !is_comparison {
593 let before_eq = trimmed[..eq_pos].trim();
594 if let Some(colon_pos) = before_eq.rfind(':') {
595 let type_str = before_eq[colon_pos + 1..].trim();
596 if !type_str.is_empty()
597 && type_str.chars().next().map_or(false, |c| c.is_alphabetic())
598 {
599 return Some(normalize_type(type_str));
600 }
601 }
602 }
603 }
604
605 if trimmed.starts_with("return") {
607 for i in (0..line_idx).rev() {
609 let prev_line = lines[i].trim();
610 if prev_line.starts_with("fn ") || prev_line.starts_with("function ") {
612 if let Some(arrow_pos) = prev_line.rfind("->") {
613 let ret_type = prev_line[arrow_pos + 2..]
614 .trim()
615 .trim_end_matches('{')
616 .trim();
617 if !ret_type.is_empty() {
618 return Some(normalize_type(ret_type));
619 }
620 }
621 if let Some(colon_pos) = prev_line.rfind(')') {
623 let after_paren = prev_line[colon_pos + 1..].trim();
624 if let Some(rest) = after_paren.strip_prefix(':') {
625 let ret_type = rest.trim().trim_end_matches('{').trim();
626 if !ret_type.is_empty() {
627 return Some(normalize_type(ret_type));
628 }
629 }
630 }
631 break;
632 }
633 }
634 }
635
636 if let Some(op_pos) = trimmed.rfind(|c: char| matches!(c, '+' | '-' | '*' | '/')) {
638 let before_op = trimmed[..op_pos].trim();
639 let ident = before_op.split_whitespace().last().unwrap_or("");
641 if let Some(t) = type_context.get(ident) {
642 return Some(normalize_type(t));
643 }
644 }
645
646 if let Some(paren_pos) = find_unclosed_paren(trimmed) {
649 let before_paren = trimmed[..paren_pos].trim();
650 let func_name = before_paren
651 .split(|c: char| !c.is_alphanumeric() && c != '_')
652 .last()
653 .unwrap_or("");
654 if !func_name.is_empty() {
655 let args_text = &trimmed[paren_pos + 1..];
657 let arg_idx = args_text.chars().filter(|&c| c == ',').count();
658 let metadata = unified_metadata();
660 if let Some(func) = metadata.get_function(func_name) {
661 if arg_idx < func.parameters.len() {
662 let param = &func.parameters[arg_idx];
663 if !param.param_type.is_empty() {
664 return Some(normalize_type(¶m.param_type));
665 }
666 }
667 }
668 }
669 }
670
671 None
672}
673
674fn find_unclosed_paren(text: &str) -> Option<usize> {
676 let mut depth = 0i32;
677 let mut last_open = None;
678 for (i, c) in text.char_indices() {
679 match c {
680 '(' => {
681 depth += 1;
682 last_open = Some(i);
683 }
684 ')' => {
685 depth -= 1;
686 if depth < 0 {
687 depth = 0;
688 }
689 }
690 _ => {}
691 }
692 }
693 if depth > 0 { last_open } else { None }
694}
695
696fn normalize_type(t: &str) -> String {
698 t.trim().to_lowercase()
699}
700
701fn types_compatible(actual: &str, expected: &str) -> TypeMatch {
703 let a = actual.to_lowercase();
704 let e = expected.to_lowercase();
705
706 if a == e {
707 return TypeMatch::Exact;
708 }
709
710 let numeric = ["number", "int", "decimal", "float", "f64", "i64"];
712 if numeric.contains(&a.as_str()) && numeric.contains(&e.as_str()) {
713 return TypeMatch::Compatible;
714 }
715
716 if a == "_" || e == "_" || a == "unknown" || e == "unknown" {
718 return TypeMatch::Compatible;
719 }
720
721 TypeMatch::Incompatible
722}
723
724#[derive(Debug, PartialEq)]
725enum TypeMatch {
726 Exact,
727 Compatible,
728 Incompatible,
729}
730
731fn boost_completions_by_type(
733 completions: &mut [CompletionItem],
734 expected: &str,
735 type_context: &HashMap<String, String>,
736) {
737 for item in completions.iter_mut() {
738 let result_type = infer_completion_result_type(item, type_context);
740 let priority = match result_type {
741 Some(ref t) => match types_compatible(t, expected) {
742 TypeMatch::Exact => "0", TypeMatch::Compatible => "1",
744 TypeMatch::Incompatible => "2",
745 },
746 None => "2", };
748
749 let base = item.sort_text.as_deref().unwrap_or(&item.label);
751 item.sort_text = Some(format!("{}_{}", priority, base));
752 }
753}
754
755fn infer_completion_result_type(
757 item: &CompletionItem,
758 type_context: &HashMap<String, String>,
759) -> Option<String> {
760 if let Some(detail) = &item.detail {
762 if let Some(arrow_pos) = detail.rfind("->") {
764 let ret = detail[arrow_pos + 2..].trim();
765 if !ret.is_empty() {
766 return Some(normalize_type(ret));
767 }
768 }
769 if let Some(colon_pos) = detail.rfind(':') {
771 let t = detail[colon_pos + 1..].trim();
772 if !t.is_empty() && !t.contains(' ') {
773 return Some(normalize_type(t));
774 }
775 }
776 }
777
778 if item.kind == Some(CompletionItemKind::VARIABLE) {
780 return type_context.get(&item.label).map(|t| normalize_type(t));
781 }
782
783 if item.kind == Some(CompletionItemKind::FUNCTION) {
785 let metadata = unified_metadata();
786 if let Some(func) = metadata.get_function(&item.label) {
787 if !func.return_type.is_empty() {
788 return Some(normalize_type(&func.return_type));
789 }
790 }
791 }
792
793 None
794}
795
796fn impl_block_completions(
798 text: &str,
799 trait_name: &str,
800 existing_methods: &[String],
801 module_cache: Option<&ModuleCache>,
802 current_file: Option<&Path>,
803 workspace_root: Option<&Path>,
804) -> Vec<CompletionItem> {
805 use tower_lsp_server::ls_types::{Documentation, InsertTextFormat, MarkupContent, MarkupKind};
806
807 let program = match parse_program(text) {
808 Ok(p) => p,
809 Err(_) => return Vec::new(),
810 };
811
812 let Some(resolved_trait) = resolve_trait_definition(
813 &program,
814 trait_name,
815 module_cache,
816 current_file,
817 workspace_root,
818 ) else {
819 if trait_name == "Content" && !existing_methods.contains(&"render".to_string()) {
821 return vec![CompletionItem {
822 label: "render".to_string(),
823 kind: Some(CompletionItemKind::METHOD),
824 detail: Some("trait Content method".to_string()),
825 documentation: Some(tower_lsp_server::ls_types::Documentation::MarkupContent(
826 tower_lsp_server::ls_types::MarkupContent {
827 kind: tower_lsp_server::ls_types::MarkupKind::Markdown,
828 value: "Implement `render` from trait `Content`\n\n```\nmethod render() -> ContentNode;\n```\n\nReturn a `ContentNode` representing this value's rich content.".to_string(),
829 },
830 )),
831 insert_text: Some("method render() -> ContentNode {\n Content.text(f\"${1:self}\")\n}".to_string()),
832 insert_text_format: Some(InsertTextFormat::SNIPPET),
833 ..CompletionItem::default()
834 }];
835 }
836 return Vec::new();
837 };
838
839 let mut completions = Vec::new();
841 for member in &resolved_trait.trait_def.members {
842 match member {
843 shape_ast::ast::TraitMember::Required(shape_ast::ast::TraitMemberSignature::Method {
844 name,
845 params,
846 return_type,
847 ..
848 }) => {
849 if existing_methods.iter().any(|m| m == name) {
851 continue;
852 }
853
854 let param_names: Vec<String> = params
856 .iter()
857 .map(|p| p.name.clone().unwrap_or_else(|| "_".to_string()))
858 .collect();
859
860 let return_type_str = crate::type_inference::type_annotation_to_string(return_type)
861 .unwrap_or_else(|| "_".to_string());
862
863 let snippet_params: Vec<String> = param_names
865 .iter()
866 .enumerate()
867 .map(|(i, n)| format!("${{{}:{}}}", i + 1, n))
868 .collect();
869
870 let snippet = format!(
871 "method {}({}) {{\n $0\n}}",
872 name,
873 snippet_params.join(", ")
874 );
875
876 let signature =
877 format!("{}({}): {}", name, param_names.join(", "), return_type_str);
878
879 completions.push(CompletionItem {
880 label: name.clone(),
881 kind: Some(CompletionItemKind::METHOD),
882 detail: Some(format!("trait {} method", trait_name)),
883 documentation: Some(Documentation::MarkupContent(MarkupContent {
884 kind: MarkupKind::Markdown,
885 value: format!(
886 "Implement `{}` from trait `{}`\n\n```\n{}\n```",
887 name, trait_name, signature
888 ),
889 })),
890 insert_text: Some(snippet),
891 insert_text_format: Some(InsertTextFormat::SNIPPET),
892 ..CompletionItem::default()
893 });
894 }
895 shape_ast::ast::TraitMember::Default(method_def) => {
896 if existing_methods.iter().any(|m| m == &method_def.name) {
898 continue;
899 }
900
901 let param_names: Vec<String> = method_def
902 .params
903 .iter()
904 .map(|p| p.simple_name().unwrap_or("_").to_string())
905 .collect();
906
907 let return_type_str = method_def
908 .return_type
909 .as_ref()
910 .and_then(|rt| crate::type_inference::type_annotation_to_string(rt))
911 .unwrap_or_else(|| "_".to_string());
912
913 let snippet_params: Vec<String> = param_names
914 .iter()
915 .enumerate()
916 .map(|(i, n)| format!("${{{}:{}}}", i + 1, n))
917 .collect();
918
919 let snippet = format!(
920 "method {}({}) {{\n $0\n}}",
921 method_def.name,
922 snippet_params.join(", ")
923 );
924
925 let signature = format!(
926 "{}({}): {}",
927 method_def.name,
928 param_names.join(", "),
929 return_type_str
930 );
931
932 completions.push(CompletionItem {
933 label: method_def.name.clone(),
934 kind: Some(CompletionItemKind::METHOD),
935 detail: Some(format!("trait {} method (default)", trait_name)),
936 documentation: Some(Documentation::MarkupContent(MarkupContent {
937 kind: MarkupKind::Markdown,
938 value: format!(
939 "Override default method `{}` from trait `{}`\n\n```\n{}\n```\n\nThis method has a default implementation.",
940 method_def.name, trait_name, signature
941 ),
942 })),
943 insert_text: Some(snippet),
944 insert_text_format: Some(InsertTextFormat::SNIPPET),
945 ..CompletionItem::default()
946 });
947 }
948 _ => {}
949 }
950 }
951
952 completions
953}
954
955fn comptime_field_override_completions(
957 struct_fields: &HashMap<String, Vec<(String, String)>>,
958 base_type: &str,
959) -> Vec<CompletionItem> {
960 use tower_lsp_server::ls_types::{Documentation, InsertTextFormat};
961
962 let mut completions = Vec::new();
963
964 if let Some(fields) = struct_fields.get(base_type) {
965 for (name, type_str) in fields {
966 if !type_str.starts_with("comptime ") {
968 continue;
969 }
970 let snippet = format!("{}: ${{1}}", name);
971 completions.push(CompletionItem {
972 label: name.clone(),
973 kind: Some(CompletionItemKind::FIELD),
974 detail: Some(type_str.clone()),
975 documentation: Some(Documentation::String(format!(
976 "Override comptime field `{}` of type `{}`",
977 name, base_type
978 ))),
979 insert_text: Some(snippet),
980 insert_text_format: Some(InsertTextFormat::SNIPPET),
981 ..CompletionItem::default()
982 });
983 }
984 }
985
986 completions
987}
988
989fn trait_bound_completions(text: &str) -> Vec<CompletionItem> {
991 use tower_lsp_server::ls_types::Documentation;
992
993 let mut completions = Vec::new();
994
995 if let Ok(program) = parse_program(text) {
999 for item in &program.items {
1000 if let shape_ast::ast::Item::Trait(trait_def, _) = item {
1001 completions.push(CompletionItem {
1002 label: trait_def.name.clone(),
1003 kind: Some(CompletionItemKind::INTERFACE),
1004 detail: Some("trait".to_string()),
1005 documentation: Some(Documentation::String(format!(
1006 "Trait `{}`",
1007 trait_def.name
1008 ))),
1009 ..CompletionItem::default()
1010 });
1011 }
1012 }
1013 }
1014
1015 if completions.is_empty() {
1017 for line in text.lines() {
1018 let trimmed = line.trim();
1019 if let Some(rest) = trimmed.strip_prefix("trait ") {
1020 if let Some(name) = rest.split_whitespace().next() {
1021 let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
1023 if !name.is_empty() {
1024 completions.push(CompletionItem {
1025 label: name.to_string(),
1026 kind: Some(CompletionItemKind::INTERFACE),
1027 detail: Some("trait".to_string()),
1028 documentation: Some(Documentation::String(format!("Trait `{}`", name))),
1029 ..CompletionItem::default()
1030 });
1031 }
1032 }
1033 }
1034 }
1035 }
1036
1037 completions
1038}
1039
1040fn all_completions(user_symbols: &[crate::symbols::SymbolInfo]) -> Vec<CompletionItem> {
1042 let mut completions = Vec::new();
1043
1044 completions.extend(symbols_to_completions(user_symbols));
1046
1047 completions.extend(keyword_completions());
1049
1050 completions.extend(builtin_function_completions());
1052
1053 completions
1054}
1055
1056fn dedupe_completion_items(items: &mut Vec<CompletionItem>) {
1057 let mut seen = HashSet::new();
1058 items.retain(|item| seen.insert(item.label.clone()));
1059}
1060
1061fn join_strategy_completions() -> Vec<CompletionItem> {
1065 vec![
1066 CompletionItem {
1067 label: "all".to_string(),
1068 kind: Some(CompletionItemKind::KEYWORD),
1069 detail: Some("Join strategy".to_string()),
1070 documentation: Some(tower_lsp_server::ls_types::Documentation::String(
1071 "Wait for all branches to complete. Returns a tuple of all results.".to_string(),
1072 )),
1073 ..CompletionItem::default()
1074 },
1075 CompletionItem {
1076 label: "race".to_string(),
1077 kind: Some(CompletionItemKind::KEYWORD),
1078 detail: Some("Join strategy".to_string()),
1079 documentation: Some(tower_lsp_server::ls_types::Documentation::String(
1080 "Return the first branch to complete, cancel the rest.".to_string(),
1081 )),
1082 ..CompletionItem::default()
1083 },
1084 CompletionItem {
1085 label: "any".to_string(),
1086 kind: Some(CompletionItemKind::KEYWORD),
1087 detail: Some("Join strategy".to_string()),
1088 documentation: Some(tower_lsp_server::ls_types::Documentation::String(
1089 "Return the first branch to succeed (non-error), cancel the rest.".to_string(),
1090 )),
1091 ..CompletionItem::default()
1092 },
1093 CompletionItem {
1094 label: "settle".to_string(),
1095 kind: Some(CompletionItemKind::KEYWORD),
1096 detail: Some("Join strategy".to_string()),
1097 documentation: Some(tower_lsp_server::ls_types::Documentation::String(
1098 "Wait for all branches, preserving individual success/error results.".to_string(),
1099 )),
1100 ..CompletionItem::default()
1101 },
1102 ]
1103}
1104
1105fn join_branch_completions(strategy: &str) -> Vec<CompletionItem> {
1107 use tower_lsp_server::ls_types::{Documentation, InsertTextFormat};
1108
1109 let strategy_hint = match strategy {
1110 "all" => "all branches must complete",
1111 "race" => "first to complete wins",
1112 "any" => "first to succeed wins",
1113 "settle" => "all branches settle (success or error)",
1114 _ => "concurrent branch",
1115 };
1116
1117 vec![
1118 CompletionItem {
1119 label: "label: expr".to_string(),
1120 kind: Some(CompletionItemKind::SNIPPET),
1121 detail: Some(format!("Named branch ({})", strategy_hint)),
1122 documentation: Some(Documentation::String(
1123 "Add a labeled branch to the join expression.\nLabels enable named access to results.".to_string(),
1124 )),
1125 insert_text: Some("${1:name}: ${2:expr}".to_string()),
1126 insert_text_format: Some(InsertTextFormat::SNIPPET),
1127 ..CompletionItem::default()
1128 },
1129 CompletionItem {
1130 label: "@annotation branch".to_string(),
1131 kind: Some(CompletionItemKind::SNIPPET),
1132 detail: Some("Annotated branch".to_string()),
1133 documentation: Some(Documentation::String(
1134 "Add an annotated branch with per-branch configuration.\nExample: @timeout(5s) fetch_data()".to_string(),
1135 )),
1136 insert_text: Some("@${1:annotation} ${2:expr}".to_string()),
1137 insert_text_format: Some(InsertTextFormat::SNIPPET),
1138 ..CompletionItem::default()
1139 },
1140 ]
1141}
1142
1143fn interpolation_format_spec_completions(spec_prefix: &str) -> Vec<CompletionItem> {
1144 use tower_lsp_server::ls_types::{Documentation, InsertTextFormat};
1145
1146 let mut items = vec![
1147 CompletionItem {
1148 label: "fixed(2)".to_string(),
1149 kind: Some(CompletionItemKind::FUNCTION),
1150 detail: Some("Numeric fixed precision format".to_string()),
1151 documentation: Some(Documentation::String(
1152 "Format numeric values with fixed precision.\nExample: `f\"{price:fixed(2)}\"`"
1153 .to_string(),
1154 )),
1155 insert_text: Some("fixed(${1:2})".to_string()),
1156 insert_text_format: Some(InsertTextFormat::SNIPPET),
1157 ..CompletionItem::default()
1158 },
1159 CompletionItem {
1160 label: "table(...)".to_string(),
1161 kind: Some(CompletionItemKind::FUNCTION),
1162 detail: Some("Typed table formatting".to_string()),
1163 documentation: Some(Documentation::String(
1164 "Render table values with typed options (no stringly keys).\nExample: `table(max_rows=20, align=right, precision=2, border=on)`".to_string(),
1165 )),
1166 insert_text: Some(
1167 "table(max_rows=${1:20}, align=${2:right}, precision=${3:2}, border=${4:on})"
1168 .to_string(),
1169 ),
1170 insert_text_format: Some(InsertTextFormat::SNIPPET),
1171 ..CompletionItem::default()
1172 },
1173 ];
1174
1175 let trimmed = spec_prefix.trim_start();
1176 if let Some(table_inner) = trimmed.strip_prefix("table(") {
1177 items.extend(table_format_argument_completions(table_inner));
1178 }
1179
1180 items
1181}
1182
1183fn table_format_argument_completions(table_inner: &str) -> Vec<CompletionItem> {
1184 use tower_lsp_server::ls_types::Documentation;
1185
1186 let mut items = Vec::new();
1187 let trailing = table_inner.rsplit(',').next().unwrap_or("").trim_start();
1188
1189 let push_value = |label: &str, detail: &str, doc: &str| CompletionItem {
1190 label: label.to_string(),
1191 kind: Some(CompletionItemKind::ENUM_MEMBER),
1192 detail: Some(detail.to_string()),
1193 documentation: Some(Documentation::String(doc.to_string())),
1194 ..CompletionItem::default()
1195 };
1196
1197 if let Some((key, value_prefix)) = trailing.split_once('=') {
1198 let key = key.trim();
1199 let value_prefix = value_prefix.trim();
1200
1201 let mut push_if_matches = |candidate: CompletionItem| {
1202 if value_prefix.is_empty() || candidate.label.starts_with(value_prefix) {
1203 items.push(candidate);
1204 }
1205 };
1206
1207 match key {
1208 "align" => {
1209 push_if_matches(push_value("left", "Alignment", "Left-aligned cells."));
1210 push_if_matches(push_value("center", "Alignment", "Center-aligned cells."));
1211 push_if_matches(push_value("right", "Alignment", "Right-aligned cells."));
1212 }
1213 "color" => {
1214 for color in [
1215 "default", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
1216 ] {
1217 push_if_matches(push_value(color, "Color", "Table color hint."));
1218 }
1219 }
1220 "border" => {
1221 push_if_matches(push_value("on", "Border", "Render table borders."));
1222 push_if_matches(push_value("off", "Border", "Render borderless table."));
1223 }
1224 _ => {}
1225 }
1226
1227 return items;
1228 }
1229
1230 let key_prefix = trailing.trim();
1232 let key_item = |label: &str, detail: &str| CompletionItem {
1233 label: label.to_string(),
1234 kind: Some(CompletionItemKind::PROPERTY),
1235 detail: Some(detail.to_string()),
1236 ..CompletionItem::default()
1237 };
1238
1239 for (key, detail) in [
1240 ("max_rows=", "Maximum number of rendered rows"),
1241 ("align=", "Global cell alignment (left|center|right)"),
1242 ("precision=", "Numeric precision for float columns"),
1243 ("color=", "Optional color hint"),
1244 ("border=", "Border mode (on|off)"),
1245 ] {
1246 if key_prefix.is_empty() || key.starts_with(key_prefix) {
1247 items.push(key_item(key, detail));
1248 }
1249 }
1250
1251 items
1252}
1253
1254fn receiver_type_for_offset(program: &Program, offset: usize) -> Option<String> {
1255 for item in &program.items {
1256 match item {
1257 Item::Impl(impl_block, span) => {
1258 if !span_contains_offset(*span, offset) {
1259 continue;
1260 }
1261 if impl_block
1262 .methods
1263 .iter()
1264 .any(|m| method_body_contains_offset(m, offset))
1265 {
1266 return Some(type_name_base_name(&impl_block.target_type));
1267 }
1268 }
1269 Item::Extend(extend_stmt, span) => {
1270 if !span_contains_offset(*span, offset) {
1271 continue;
1272 }
1273 if extend_stmt
1274 .methods
1275 .iter()
1276 .any(|m| method_body_contains_offset(m, offset))
1277 {
1278 return Some(type_name_base_name(&extend_stmt.type_name));
1279 }
1280 }
1281 _ => {}
1282 }
1283 }
1284 None
1285}
1286
1287fn method_body_contains_offset(method: &MethodDef, offset: usize) -> bool {
1288 method.body.iter().any(|stmt| {
1289 let span = match stmt {
1290 Statement::Return(_, span)
1291 | Statement::Break(span)
1292 | Statement::Continue(span)
1293 | Statement::VariableDecl(_, span)
1294 | Statement::Assignment(_, span)
1295 | Statement::Expression(_, span)
1296 | Statement::For(_, span)
1297 | Statement::While(_, span)
1298 | Statement::If(_, span)
1299 | Statement::Extend(_, span)
1300 | Statement::RemoveTarget(span)
1301 | Statement::SetParamType { span, .. }
1302 | Statement::SetParamValue { span, .. }
1303 | Statement::SetReturnType { span, .. }
1304 | Statement::SetReturnExpr { span, .. }
1305 | Statement::ReplaceBodyExpr { span, .. }
1306 | Statement::ReplaceBody { span, .. }
1307 | Statement::ReplaceModuleExpr { span, .. } => *span,
1308 };
1309 span_contains_offset(span, offset)
1310 })
1311}
1312
1313fn type_name_base_name(type_name: &TypeName) -> String {
1314 match type_name {
1315 TypeName::Simple(name) => name.to_string(),
1316 TypeName::Generic { name, .. } => name.to_string(),
1317 }
1318}
1319
1320fn span_contains_offset(span: Span, offset: usize) -> bool {
1321 span.start <= offset && offset <= span.end
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326 use super::*;
1327 use std::collections::HashMap;
1328
1329 fn completions_for(code: &str, position: Position) -> Vec<CompletionItem> {
1330 let (completions, _, _) = get_completions(code, position, &[], &HashMap::new());
1331 completions
1332 }
1333
1334 #[test]
1335 fn test_keyword_completions() {
1336 let keywords = keyword_completions();
1337 assert!(!keywords.is_empty());
1338
1339 let labels: Vec<_> = keywords.iter().map(|k| k.label.as_str()).collect();
1341 assert!(labels.contains(&"let"));
1342 assert!(labels.contains(&"const"));
1343 assert!(labels.contains(&"fn"));
1344 assert!(
1345 !labels.contains(&"function"),
1346 "Legacy alias should not be globally suggested"
1347 );
1348 }
1351
1352 #[test]
1353 fn test_builtin_functions() {
1354 let functions = builtin_function_completions();
1355 assert!(!functions.is_empty());
1356
1357 let labels: Vec<_> = functions.iter().map(|f| f.label.as_str()).collect();
1359 assert!(
1362 labels.contains(&"abs") || labels.contains(&"sqrt") || labels.contains(&"print"),
1363 "Should include core builtin functions"
1364 );
1365 }
1366
1367 #[test]
1368 fn test_get_completions() {
1369 let completions = completions_for(
1370 "",
1371 Position {
1372 line: 0,
1373 character: 0,
1374 },
1375 );
1376
1377 assert!(completions.len() > 50);
1379 }
1380
1381 #[test]
1382 fn test_dynamic_variable_completion() {
1383 let code = r#"let myVar = 5;
1384const MY_CONST = 10;
1385
1386"#;
1387 let position = Position {
1388 line: 3,
1389 character: 0,
1390 };
1391 let completions = completions_for(code, position);
1392
1393 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1394 assert!(
1395 labels.contains(&"myVar"),
1396 "Should include user-defined variable"
1397 );
1398 assert!(
1399 labels.contains(&"MY_CONST"),
1400 "Should include user-defined constant"
1401 );
1402 }
1403
1404 #[test]
1405 fn test_dynamic_function_completion() {
1406 let code = r#"function myFunction(x, y) {
1407 return x + y;
1408}
1409
1410"#;
1411 let position = Position {
1412 line: 4,
1413 character: 0,
1414 };
1415 let completions = completions_for(code, position);
1416
1417 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1418 assert!(
1419 labels.contains(&"myFunction"),
1420 "Should include user-defined function"
1421 );
1422 }
1423
1424 #[test]
1425 fn test_doc_tag_completion() {
1426 let code = "/// @pa\nfn add(x: number) -> number { x }\n";
1427 let position = Position {
1428 line: 0,
1429 character: 6,
1430 };
1431 let completions = completions_for(code, position);
1432 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1433 assert!(labels.contains(&"param"));
1434 }
1435
1436 #[test]
1437 fn test_doc_param_completion_uses_attached_function_params() {
1438 let code = "/// Summary.\n/// @param va\nfn add(value: number, scale: number) -> number { value * scale }\n";
1439 let position = Position {
1440 line: 1,
1441 character: 13,
1442 };
1443 let completions = completions_for(code, position);
1444 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1445 assert!(labels.contains(&"value"));
1446 assert!(!labels.contains(&"scale"));
1447 }
1448
1449 #[test]
1450 fn test_interpolation_format_spec_completions_include_fixed_and_table() {
1451 let code = r#"let s = f"value: {price:f}""#;
1452 let pos = Position {
1453 line: 0,
1454 character: code.find("{price:f").unwrap() as u32 + 8,
1455 };
1456 let items = completions_for(code, pos);
1457 let labels: Vec<_> = items.iter().map(|c| c.label.as_str()).collect();
1458 assert!(
1459 labels.contains(&"fixed(2)"),
1460 "expected fixed completion, got {:?}",
1461 labels
1462 );
1463 assert!(
1464 labels.contains(&"table(...)"),
1465 "expected table completion, got {:?}",
1466 labels
1467 );
1468 }
1469
1470 #[test]
1471 fn test_interpolation_table_align_value_completions() {
1472 let code = r#"let s = f"{rows:table(align=)}""#;
1473 let pos = Position {
1474 line: 0,
1475 character: code.find("align=").unwrap() as u32 + 6,
1476 };
1477 let items = completions_for(code, pos);
1478 let labels: Vec<_> = items.iter().map(|c| c.label.as_str()).collect();
1479 assert!(
1480 labels.contains(&"left") && labels.contains(&"right"),
1481 "expected alignment value completions, got {:?}",
1482 labels
1483 );
1484 }
1485
1486 #[test]
1487 fn test_dynamic_pattern_completion() {
1488 let code = r#"function myPattern(candle) {
1489 return candle.close > candle.open;
1490}
1491
1492let x = 1
1493"#;
1494 let position = Position {
1495 line: 5,
1496 character: 0,
1497 };
1498 let completions = completions_for(code, position);
1499
1500 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1501 assert!(
1502 labels.contains(&"myPattern"),
1503 "Should include user-defined pattern. Got: {:?}",
1504 labels
1505 );
1506 }
1507
1508 #[test]
1509 fn test_property_completion_with_typed_variable() {
1510 let code = "type Point { x: number, y: number }\nlet p: Point\np.x\n";
1512 let position = Position {
1513 line: 2,
1514 character: 2,
1515 };
1516 let completions = completions_for(code, position);
1517
1518 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1519 assert!(
1520 labels.contains(&"x"),
1521 "Should include Point field 'x'. Got: {:?}",
1522 labels
1523 );
1524 assert!(
1525 labels.contains(&"y"),
1526 "Should include Point field 'y'. Got: {:?}",
1527 labels
1528 );
1529 }
1530
1531 #[test]
1532 fn test_property_completion_for_self_inside_dollar_interpolation() {
1533 let code = "type User { name: String }\nimpl Display for User as JsonDisplay {\n method display() { f$\"\"\"{ \"name\": \"${self.na}\" }\"\"\" }\n}\n";
1534 let cursor_offset = code
1535 .find("self.na")
1536 .expect("expected self property access in interpolation")
1537 + "self.".len();
1538 let position = crate::util::offset_to_position(code, cursor_offset);
1539 let completions = completions_for(code, position);
1540 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1541 assert!(
1542 labels.contains(&"name"),
1543 "Expected `name` completion for self receiver in interpolation, got: {:?}",
1544 labels
1545 );
1546 }
1547
1548 #[test]
1549 fn test_struct_chained_property_completion() {
1550 let code = "type Summary { total: number, ratio: number }\ntype Result { summary: Summary }\nlet bt: Result\nbt.summary.x\n";
1552 let position = Position {
1553 line: 3,
1554 character: 11,
1555 };
1556 let completions = completions_for(code, position);
1557 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1558 assert!(
1559 labels.contains(&"total"),
1560 "Should include Summary field 'total'. Got: {:?}",
1561 labels
1562 );
1563 assert!(
1564 labels.contains(&"ratio"),
1565 "Should include Summary field 'ratio'. Got: {:?}",
1566 labels
1567 );
1568 }
1569
1570 #[test]
1571 fn test_user_defined_type_property_completion() {
1572 let code =
1574 "type Item { name: string, price: number, quantity: number }\nlet item: Item\nitem.x\n";
1575 let position = Position {
1576 line: 2,
1577 character: 5,
1578 };
1579 let completions = completions_for(code, position);
1580 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1581 assert!(
1582 labels.contains(&"name"),
1583 "Should include Item field 'name'. Got: {:?}",
1584 labels
1585 );
1586 assert!(
1587 labels.contains(&"price"),
1588 "Should include Item field 'price'. Got: {:?}",
1589 labels
1590 );
1591 assert!(
1592 labels.contains(&"quantity"),
1593 "Should include Item field 'quantity'. Got: {:?}",
1594 labels
1595 );
1596 }
1597
1598 #[test]
1599 fn test_pattern_reference_completion() {
1600 let code = r#"function hammer(candle) {
1601 return candle.close > candle.open;
1602}
1603
1604let x = 1
1605"#;
1606 let position = Position {
1608 line: 5,
1609 character: 0,
1610 };
1611 let completions = completions_for(code, position);
1612
1613 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1614 assert!(
1616 labels.contains(&"hammer"),
1617 "Should include user-defined pattern 'hammer'. Got: {:?}",
1618 labels
1619 );
1620 }
1621
1622 #[test]
1623 fn test_metadata_keywords() {
1624 let keywords = shape_runtime::metadata::LanguageMetadata::keywords();
1626 assert!(!keywords.is_empty());
1627 assert!(keywords.iter().any(|k| k.keyword == "let"));
1628 assert!(keywords.iter().any(|k| k.keyword == "function"));
1629 }
1630
1631 #[test]
1632 fn test_metadata_functions() {
1633 let metadata = unified_metadata();
1635 let functions = metadata.all_functions();
1636 assert!(!functions.is_empty(), "Should have some functions loaded");
1637 let has_builtins = functions.iter().any(|f| {
1640 f.name == "abs"
1641 || f.name == "sqrt"
1642 || f.name == "print"
1643 || f.name == "sma"
1644 || f.name == "rsi"
1645 });
1646 assert!(
1647 has_builtins,
1648 "Should include either core builtins or stdlib functions"
1649 );
1650 }
1651
1652 #[test]
1653 fn test_metadata_types() {
1654 let types = shape_runtime::metadata::LanguageMetadata::builtin_types();
1656 assert!(!types.is_empty());
1657 assert!(types.iter().any(|t| t.name == "Number"));
1658 assert!(types.iter().any(|t| t.name == "Table"));
1659 }
1660
1661 #[test]
1662 fn test_metadata_row_properties() {
1663 let meta = unified_metadata();
1666 let props = meta
1667 .get_type_properties("Row")
1668 .expect("Row type should exist");
1669 assert_eq!(
1670 props.len(),
1671 0,
1672 "Row should have no hardcoded properties - fields are dynamic"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_metadata_column_methods() {
1678 let methods = shape_runtime::metadata::LanguageMetadata::column_methods();
1688 assert!(methods.is_empty());
1689 }
1690
1691 #[test]
1692 fn test_result_type_completions() {
1693 let code = "instr.";
1695 let position = Position {
1696 line: 0,
1697 character: 6,
1698 };
1699 let mut type_context = HashMap::new();
1701 type_context.insert("instr".to_string(), "Result<Instrument>".to_string());
1702 let (completions, _, _) = get_completions(code, position, &[], &type_context);
1703 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1704
1705 assert!(
1707 labels.contains(&"unwrap"),
1708 "Should include Result method 'unwrap'. Got: {:?}",
1709 labels
1710 );
1711 assert!(
1712 labels.contains(&"unwrap_or"),
1713 "Should include Result method 'unwrap_or'. Got: {:?}",
1714 labels
1715 );
1716 assert!(
1717 labels.contains(&"is_ok"),
1718 "Should include Result method 'is_ok'. Got: {:?}",
1719 labels
1720 );
1721 assert!(
1722 labels.contains(&"is_err"),
1723 "Should include Result method 'is_err'. Got: {:?}",
1724 labels
1725 );
1726
1727 assert!(
1729 !labels.contains(&"symbol"),
1730 "Should NOT include Instrument property 'symbol' on Result type. Got: {:?}",
1731 labels
1732 );
1733 }
1734
1735 #[test]
1736 fn test_option_type_completions() {
1737 let code = "opt.";
1739 let position = Position {
1740 line: 0,
1741 character: 4,
1742 };
1743 let mut type_context = HashMap::new();
1745 type_context.insert("opt".to_string(), "Option<Number>".to_string());
1746 let (completions, _, _) = get_completions(code, position, &[], &type_context);
1747 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1748
1749 assert!(
1751 labels.contains(&"unwrap"),
1752 "Should include Option method 'unwrap'. Got: {:?}",
1753 labels
1754 );
1755 assert!(
1756 labels.contains(&"is_some"),
1757 "Should include Option method 'is_some'. Got: {:?}",
1758 labels
1759 );
1760 assert!(
1761 labels.contains(&"is_none"),
1762 "Should include Option method 'is_none'. Got: {:?}",
1763 labels
1764 );
1765 }
1766
1767 #[test]
1768 fn test_new_keywords_in_completions() {
1769 let keywords = keyword_completions();
1770 let labels: Vec<_> = keywords.iter().map(|k| k.label.as_str()).collect();
1771 assert!(labels.contains(&"match"), "Should include 'match' keyword");
1773 assert!(labels.contains(&"try"), "Should include 'try' keyword");
1774 assert!(
1775 !labels.contains(&"stream"),
1776 "Deprecated/placeholder keyword should not be globally suggested"
1777 );
1778 }
1779
1780 #[test]
1781 fn test_using_impl_selector_context_detection() {
1782 let code = "let x = value using ";
1783 let position = Position {
1784 line: 0,
1785 character: 20,
1786 };
1787 assert!(is_using_impl_selector_context(code, position));
1788 }
1789
1790 #[test]
1791 fn test_using_impl_selector_completions() {
1792 let code = "trait Display { method display() -> string }\n\
1793 type User { name: string }\n\
1794 impl Display for User as JsonDisplay {\n\
1795 method display() { \"json\" }\n\
1796 }\n\
1797 let u = User { name: \"a\" }\n\
1798 print(u using )\n";
1799 let position = Position {
1800 line: 6,
1801 character: 14,
1802 };
1803 let (completions, _, _) = get_completions(code, position, &[], &HashMap::new());
1804 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1805 assert!(
1806 labels.contains(&"JsonDisplay"),
1807 "Expected named impl selector completion. Got: {:?}",
1808 labels
1809 );
1810 }
1811
1812 #[test]
1813 fn test_keyword_descriptions_have_examples() {
1814 let keywords = shape_runtime::metadata::LanguageMetadata::keywords();
1815 let type_kw = keywords
1817 .iter()
1818 .find(|k| k.keyword == "type")
1819 .expect("type keyword");
1820 assert!(
1821 type_kw.description.contains("type Point"),
1822 "'type' should have struct example"
1823 );
1824
1825 let comptime_kw = keywords
1826 .iter()
1827 .find(|k| k.keyword == "comptime")
1828 .expect("comptime keyword");
1829 assert!(
1830 comptime_kw.description.contains("comptime symbol"),
1831 "'comptime' should have example"
1832 );
1833
1834 let match_kw = keywords
1835 .iter()
1836 .find(|k| k.keyword == "match")
1837 .expect("match keyword");
1838 assert!(
1839 match_kw.description.contains("match color"),
1840 "'match' should have example"
1841 );
1842
1843 let for_kw = keywords
1844 .iter()
1845 .find(|k| k.keyword == "for")
1846 .expect("for keyword");
1847 assert!(
1848 for_kw.description.contains("for x in"),
1849 "'for' should have example"
1850 );
1851 }
1852
1853 #[test]
1854 fn test_builtin_function_metadata_has_signatures() {
1855 let metadata = unified_metadata();
1856 let abs_fn = metadata.get_function("abs").expect("abs should exist");
1857 assert!(
1858 abs_fn.signature.contains("abs"),
1859 "abs signature should contain function name"
1860 );
1861 assert!(
1862 abs_fn.signature.contains("number"),
1863 "abs signature should mention number type"
1864 );
1865 assert!(
1866 !abs_fn.description.is_empty(),
1867 "abs should have a description"
1868 );
1869 assert_eq!(abs_fn.parameters.len(), 1, "abs should have 1 parameter");
1870
1871 let print_fn = metadata.get_function("print").expect("print should exist");
1872 assert!(
1873 print_fn.signature.contains("print"),
1874 "print signature should contain name"
1875 );
1876
1877 let max_fn = metadata.get_function("max").expect("max should exist");
1878 assert_eq!(max_fn.parameters.len(), 2, "max should have 2 parameters");
1879 }
1880
1881 #[test]
1882 fn test_snippets_have_correct_format() {
1883 use tower_lsp_server::ls_types::InsertTextFormat;
1884 let snippets = snippet_completions();
1885 for snippet in &snippets {
1886 assert_eq!(
1887 snippet.insert_text_format,
1888 Some(InsertTextFormat::SNIPPET),
1889 "Snippet '{}' should have SNIPPET insert format",
1890 snippet.label
1891 );
1892 assert!(
1893 snippet.insert_text.is_some(),
1894 "Snippet '{}' should have insert text",
1895 snippet.label
1896 );
1897 }
1898 }
1899
1900 #[test]
1901 fn test_user_defined_struct_field_completions() {
1902 let code = "type MyType { i: int, name: string }\nlet b = MyType { i: 10, name: \"hello\" }\nb.i\n";
1906 let position = Position {
1907 line: 2,
1908 character: 2, };
1910 let completions = completions_for(code, position);
1911 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1912
1913 assert!(
1914 labels.contains(&"i"),
1915 "Should include struct field 'i'. Got: {:?}",
1916 labels
1917 );
1918 assert!(
1919 labels.contains(&"name"),
1920 "Should include struct field 'name'. Got: {:?}",
1921 labels
1922 );
1923 assert!(
1924 !labels.contains(&"length"),
1925 "Should NOT show generic 'length' fallback for typed struct. Got: {:?}",
1926 labels
1927 );
1928 }
1929
1930 #[test]
1931 fn test_unwrapped_type_has_inner_properties() {
1932 let code = "type Device { name: string, status: string }\nlet dev: Device\ndev.x\n";
1934 let position = Position {
1935 line: 2,
1936 character: 4,
1937 };
1938 let completions = completions_for(code, position);
1939 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1940
1941 assert!(
1943 labels.contains(&"name"),
1944 "Device should include 'name'. Got: {:?}",
1945 labels
1946 );
1947 assert!(
1948 !labels.contains(&"is_ok"),
1949 "Device should NOT include Result method 'is_ok'. Got: {:?}",
1950 labels
1951 );
1952 }
1953
1954 #[test]
1955 fn test_impl_method_completions() {
1956 let code = "trait Q {\n method filter(self, p) -> any;\n method select(self, c) -> any;\n}\nimpl Q for T {\n method filter(p) { self }\n}\nlet t: T\nt.x\n";
1961 let position = Position {
1962 line: 8,
1963 character: 2, };
1965 let mut type_context = HashMap::new();
1966 type_context.insert("t".to_string(), "T".to_string());
1967 let (completions, _, _) = get_completions(code, position, &[], &type_context);
1968 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1969 assert!(
1970 labels.contains(&"filter"),
1971 "Should include 'filter' from impl. Got: {:?}",
1972 labels
1973 );
1974 assert!(
1975 labels.contains(&"select"),
1976 "Should include 'select' from trait (via impl). Got: {:?}",
1977 labels
1978 );
1979 }
1980
1981 #[test]
1982 fn test_extend_method_completions() {
1983 let code =
1985 "extend Array {\n method double() {\n self\n }\n}\nlet a: Array\na.x\n";
1986 let position = Position {
1987 line: 6,
1988 character: 2, };
1990 let mut type_context = HashMap::new();
1991 type_context.insert("a".to_string(), "Array".to_string());
1992 let (completions, _, _) = get_completions(code, position, &[], &type_context);
1993 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
1994 assert!(
1995 labels.contains(&"double"),
1996 "Should include 'double' from extend block. Got: {:?}",
1997 labels
1998 );
1999 }
2000
2001 #[test]
2002 fn test_property_completions_chained_struct() {
2003 let code = "type Outer { inner: Inner }\ntype Inner { val: number }\nlet o = Outer { inner: Inner { val: 1 } }\no.inner.x\n";
2005 let position = Position {
2006 line: 3,
2007 character: 8, };
2009 let completions = completions_for(code, position);
2010 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2011 assert!(
2012 labels.contains(&"val"),
2013 "Should include 'val' from Inner struct. Got: {:?}",
2014 labels
2015 );
2016 }
2017
2018 #[test]
2019 fn test_string_method_completions() {
2020 let code = "let s = \"hi\"\ns.x\n";
2025 let position = Position {
2026 line: 1,
2027 character: 2, };
2029 let completions = completions_for(code, position);
2030 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2031 assert!(
2033 labels.contains(&"toString"),
2034 "Should include universal method 'toString'. Got: {:?}",
2035 labels
2036 );
2037 assert!(
2038 labels.contains(&"type"),
2039 "Should include universal method 'type'. Got: {:?}",
2040 labels
2041 );
2042 }
2043
2044 #[test]
2045 fn test_number_method_completions() {
2046 let code = "let n = 42\nn.x\n";
2049 let position = Position {
2050 line: 1,
2051 character: 2,
2052 };
2053 let completions = completions_for(code, position);
2054 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2055 assert!(
2056 labels.contains(&"toString"),
2057 "Should include universal method 'toString'. Got: {:?}",
2058 labels
2059 );
2060 }
2061
2062 #[test]
2063 fn test_array_method_completions() {
2064 let code = "let a = [1, 2]\na.x\n";
2072 let position = Position {
2073 line: 1,
2074 character: 2,
2075 };
2076 let completions = completions_for(code, position);
2077 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2078 assert!(
2079 labels.contains(&"toString"),
2080 "Should include universal method 'toString'. Got: {:?}",
2081 labels
2082 );
2083 assert!(
2084 labels.contains(&"type"),
2085 "Should include universal method 'type'. Got: {:?}",
2086 labels
2087 );
2088 assert!(
2089 labels.contains(&"map"),
2090 "Should include stdlib Vec.map. Got: {:?}",
2091 labels
2092 );
2093 assert!(
2094 labels.contains(&"filter"),
2095 "Should include stdlib Vec.filter. Got: {:?}",
2096 labels
2097 );
2098 }
2099
2100 #[test]
2101 fn test_closure_param_completions_with_struct() {
2102 let code = "type Candle { open: number, close: number }\nlet t: Table<Candle>\nt.filter(|c| c.x)\n";
2104 let position = Position {
2105 line: 2,
2106 character: 16, };
2108 let mut type_context = HashMap::new();
2109 type_context.insert("t".to_string(), "Table<Candle>".to_string());
2110 let (completions, _, _) = get_completions(code, position, &[], &type_context);
2111 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2112 assert!(
2113 labels.contains(&"open"),
2114 "Should include 'open' from Candle struct. Got: {:?}",
2115 labels
2116 );
2117 assert!(
2118 labels.contains(&"close"),
2119 "Should include 'close' from Candle struct. Got: {:?}",
2120 labels
2121 );
2122 }
2123
2124 #[test]
2125 fn test_pipe_target_completions() {
2126 let code = "let a = [1, 2]\na |> ";
2128 let position = Position {
2129 line: 1,
2130 character: 5,
2131 };
2132 let completions = completions_for(code, position);
2133 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2134 assert!(
2136 labels.contains(&"map"),
2137 "Pipe target should include 'map'. Got: {:?}",
2138 labels
2139 );
2140 assert!(
2141 labels.contains(&"filter"),
2142 "Pipe target should include 'filter'. Got: {:?}",
2143 labels
2144 );
2145 }
2146
2147 #[test]
2148 fn test_pipe_chain_type_tracking() {
2149 let code = "let a = [1]\nlet b = a\nb.x\n";
2152 let position = Position {
2153 line: 2,
2154 character: 2,
2155 };
2156 let completions = completions_for(code, position);
2157 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2158 assert!(
2159 !labels.is_empty(),
2160 "Should have some completions for b. Got: {:?}",
2161 labels
2162 );
2163 }
2164
2165 #[test]
2166 fn test_impl_block_completions_suggests_unimplemented() {
2167 let code = "trait Queryable {\n method filter(self, pred) -> any;\n method select(self, cols) -> any;\n method orderBy(self, col) -> any;\n}\nimpl Queryable for MyTable {\n method filter(pred) { self }\n \n}\n";
2169 let completions =
2170 impl_block_completions(code, "Queryable", &["filter".to_string()], None, None, None);
2171 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2172 assert!(
2173 labels.contains(&"select"),
2174 "Should suggest unimplemented 'select'. Got: {:?}",
2175 labels
2176 );
2177 assert!(
2178 labels.contains(&"orderBy"),
2179 "Should suggest unimplemented 'orderBy'. Got: {:?}",
2180 labels
2181 );
2182 assert!(
2183 !labels.contains(&"filter"),
2184 "Should NOT suggest already-implemented 'filter'. Got: {:?}",
2185 labels
2186 );
2187 }
2188
2189 #[test]
2190 fn test_impl_block_completions_empty_when_all_implemented() {
2191 let code = "trait Simple {\n method foo() -> any\n}\nimpl Simple for Bar {\n method foo() { self }\n}\n";
2192 let completions =
2193 impl_block_completions(code, "Simple", &["foo".to_string()], None, None, None);
2194 assert!(
2195 completions.is_empty(),
2196 "Should have no completions when all methods implemented"
2197 );
2198 }
2199
2200 #[test]
2201 fn test_impl_block_completions_unknown_trait_returns_empty() {
2202 let code = "let x = 42\n";
2203 let completions = impl_block_completions(code, "NonExistent", &[], None, None, None);
2204 assert!(
2205 completions.is_empty(),
2206 "Should return empty for unknown trait"
2207 );
2208 }
2209
2210 #[test]
2211 fn test_impl_block_completions_snippet_format() {
2212 let code = "trait Filt {\n method filter(self, pred) -> any;\n}\n";
2214 let completions = impl_block_completions(code, "Filt", &[], None, None, None);
2215 assert_eq!(completions.len(), 1);
2216 let item = &completions[0];
2217 assert_eq!(item.label, "filter");
2218 assert_eq!(
2220 item.insert_text_format,
2221 Some(tower_lsp_server::ls_types::InsertTextFormat::SNIPPET)
2222 );
2223 let snippet = item.insert_text.as_ref().unwrap();
2225 assert!(
2226 snippet.starts_with("method filter("),
2227 "Snippet should start with 'method filter('. Got: {}",
2228 snippet
2229 );
2230 }
2231
2232 #[test]
2233 fn test_impl_block_completions_resolve_trait_from_modules() {
2234 let code = "type User { name: String }\nimpl Display for User {\n \n}\n";
2235 let position = Position {
2236 line: 2,
2237 character: 4,
2238 };
2239 let cache = ModuleCache::new();
2240 let current_file = std::env::current_dir()
2241 .unwrap_or_else(|_| std::path::PathBuf::from("."))
2242 .join("__shape_lsp_impl_completion_test__.shape");
2243
2244 let (completions, _, _) = get_completions_with_context(
2245 code,
2246 position,
2247 &[],
2248 &HashMap::new(),
2249 Some(&cache),
2250 Some(current_file.as_path()),
2251 None,
2252 );
2253 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2254 assert!(
2255 labels.contains(&"display"),
2256 "Expected Display trait method completion from module resolution, got: {:?}",
2257 labels
2258 );
2259 }
2260
2261 #[test]
2262 fn test_comptime_field_override_completions() {
2263 let mut struct_fields = HashMap::new();
2264 struct_fields.insert(
2265 "Currency".to_string(),
2266 vec![
2267 ("symbol".to_string(), "comptime string = \"$\"".to_string()),
2268 ("decimals".to_string(), "comptime number = 2".to_string()),
2269 ("amount".to_string(), "number".to_string()),
2270 ],
2271 );
2272 let completions = comptime_field_override_completions(&struct_fields, "Currency");
2273 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2274 assert_eq!(labels.len(), 2, "Should only show comptime fields");
2275 assert!(
2276 labels.contains(&"symbol"),
2277 "Should include comptime field 'symbol'. Got: {:?}",
2278 labels
2279 );
2280 assert!(
2281 labels.contains(&"decimals"),
2282 "Should include comptime field 'decimals'. Got: {:?}",
2283 labels
2284 );
2285 assert!(
2286 !labels.contains(&"amount"),
2287 "Should NOT include runtime field 'amount'. Got: {:?}",
2288 labels
2289 );
2290 }
2291
2292 #[test]
2293 fn test_comptime_field_override_completions_integration() {
2294 let code = "type Currency { comptime symbol: string = \"$\", comptime decimals: number = 2, amount: number }\ntype EUR = Currency { symbol: \"E\" }";
2297 let position = Position {
2299 line: 1,
2300 character: 22,
2301 };
2302 let completions = completions_for(code, position);
2303 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2304 assert!(
2305 labels.contains(&"symbol"),
2306 "Should suggest comptime field 'symbol'. Got: {:?}",
2307 labels
2308 );
2309 assert!(
2310 labels.contains(&"decimals"),
2311 "Should suggest comptime field 'decimals'. Got: {:?}",
2312 labels
2313 );
2314 assert!(
2315 !labels.contains(&"amount"),
2316 "Should NOT suggest runtime field 'amount'. Got: {:?}",
2317 labels
2318 );
2319 }
2320
2321 #[test]
2322 fn test_join_strategy_completions() {
2323 let completions = join_strategy_completions();
2324 assert_eq!(
2325 completions.len(),
2326 4,
2327 "Should have 4 join strategy completions"
2328 );
2329
2330 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2331 assert!(labels.contains(&"all"));
2332 assert!(labels.contains(&"race"));
2333 assert!(labels.contains(&"any"));
2334 assert!(labels.contains(&"settle"));
2335
2336 for item in &completions {
2338 assert_eq!(item.kind, Some(CompletionItemKind::KEYWORD));
2339 }
2340 }
2341
2342 #[test]
2343 fn test_join_strategy_completions_integration() {
2344 let code = "async fn foo() {\n await join ";
2346 let position = Position {
2347 line: 1,
2348 character: 13,
2349 };
2350 let context = crate::context::analyze_context(code, position);
2351 assert_eq!(
2352 context,
2353 crate::context::CompletionContext::JoinStrategy,
2354 "Should detect JoinStrategy context"
2355 );
2356 }
2357
2358 #[test]
2359 fn test_async_keywords_in_completions() {
2360 let keywords = keyword_completions();
2361 let labels: Vec<_> = keywords.iter().map(|k| k.label.as_str()).collect();
2362 assert!(labels.contains(&"await"), "Should include 'await' keyword");
2363 assert!(labels.contains(&"async"), "Should include 'async' keyword");
2364 assert!(
2365 !labels.contains(&"join"),
2366 "join should only appear in `await join` context completions"
2367 );
2368 }
2369
2370 #[test]
2371 fn test_join_branch_completions() {
2372 let completions = join_branch_completions("all");
2373 assert_eq!(completions.len(), 2);
2374 assert!(
2375 completions.iter().any(|c| c.label.contains("label")),
2376 "Should include labeled branch snippet"
2377 );
2378 assert!(
2379 completions.iter().any(|c| c.label.contains("annotation")),
2380 "Should include annotated branch snippet"
2381 );
2382 }
2383
2384 #[test]
2385 fn test_join_body_completions_integration() {
2386 let code = "async fn foo() {\n await join all {\n ";
2388 let position = Position {
2389 line: 2,
2390 character: 4,
2391 };
2392 let completions = completions_for(code, position);
2393 let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
2394 assert!(
2395 labels.iter().any(|l| l.contains("label")),
2396 "Should include labeled branch snippet in join body, got: {:?}",
2397 labels
2398 );
2399 }
2400
2401 #[test]
2402 fn test_type_aware_completion_typed_assignment() {
2403 let code = "let n = 42\nlet s = \"hello\"\nlet x: number = ";
2405 let position = Position {
2406 line: 2,
2407 character: 16,
2408 };
2409 let mut type_context = HashMap::new();
2410 type_context.insert("n".to_string(), "number".to_string());
2411 type_context.insert("s".to_string(), "string".to_string());
2412 let (completions, _, _) = get_completions(code, position, &[], &type_context);
2413
2414 let with_sort: Vec<_> = completions
2416 .iter()
2417 .filter(|c| c.sort_text.is_some())
2418 .collect();
2419 assert!(!with_sort.is_empty(), "expected sort_text on completions");
2420
2421 let n_sort = completions
2423 .iter()
2424 .find(|c| c.label == "n")
2425 .and_then(|c| c.sort_text.as_deref());
2426 let s_sort = completions
2427 .iter()
2428 .find(|c| c.label == "s")
2429 .and_then(|c| c.sort_text.as_deref());
2430 if let (Some(n_s), Some(s_s)) = (n_sort, s_sort) {
2431 assert!(
2432 n_s < s_s,
2433 "number var 'n' should sort before string var 's'. n={}, s={}",
2434 n_s,
2435 s_s
2436 );
2437 }
2438 }
2439
2440 #[test]
2441 fn test_expected_type_from_typed_assignment() {
2442 let code = "let x: number = ";
2443 let position = Position {
2444 line: 0,
2445 character: 16,
2446 };
2447 let type_context = HashMap::new();
2448 let expected = expected_type_at_cursor(code, position, &type_context);
2449 assert_eq!(expected, Some("number".to_string()));
2450 }
2451
2452 #[test]
2453 fn test_expected_type_from_return() {
2454 let code = "fn foo() -> number {\n return ";
2455 let position = Position {
2456 line: 1,
2457 character: 9,
2458 };
2459 let type_context = HashMap::new();
2460 let expected = expected_type_at_cursor(code, position, &type_context);
2461 assert_eq!(expected, Some("number".to_string()));
2462 }
2463
2464 #[test]
2465 fn test_expected_type_from_binary_op() {
2466 let code = "let x = n + ";
2467 let position = Position {
2468 line: 0,
2469 character: 12,
2470 };
2471 let mut type_context = HashMap::new();
2472 type_context.insert("n".to_string(), "number".to_string());
2473 let expected = expected_type_at_cursor(code, position, &type_context);
2474 assert_eq!(expected, Some("number".to_string()));
2475 }
2476
2477 #[test]
2478 fn test_types_compatible_exact() {
2479 assert_eq!(types_compatible("number", "number"), TypeMatch::Exact);
2480 assert_eq!(types_compatible("string", "string"), TypeMatch::Exact);
2481 }
2482
2483 #[test]
2484 fn test_types_compatible_numeric() {
2485 assert_eq!(types_compatible("int", "number"), TypeMatch::Compatible);
2486 assert_eq!(types_compatible("decimal", "number"), TypeMatch::Compatible);
2487 }
2488
2489 #[test]
2490 fn test_types_compatible_incompatible() {
2491 assert_eq!(
2492 types_compatible("string", "number"),
2493 TypeMatch::Incompatible
2494 );
2495 assert_eq!(types_compatible("bool", "number"), TypeMatch::Incompatible);
2496 }
2497}