1use crate::completion::methods::{
4 extract_option_inner, extract_result_inner, method_completion_item,
5};
6use crate::type_inference::MethodCompletionInfo;
7use crate::type_inference::{parse_object_shape_fields, unified_metadata};
8use shape_runtime::metadata::{LanguageMetadata, PropertyInfo};
9use shape_runtime::type_system::checking::method_table::MethodTable;
10use std::collections::HashMap;
11use std::sync::OnceLock;
12use tower_lsp_server::ls_types::{
13 CompletionItem, CompletionItemKind, Documentation, InsertTextFormat,
14};
15
16static METHOD_TABLE: OnceLock<MethodTable> = OnceLock::new();
18
19fn method_table() -> &'static MethodTable {
20 METHOD_TABLE.get_or_init(MethodTable::new)
21}
22
23use super::methods::{option_method_completions, result_method_completions};
24
25pub fn property_completion_item(prop: &PropertyInfo) -> CompletionItem {
27 CompletionItem {
28 label: prop.name.clone(),
29 kind: Some(CompletionItemKind::PROPERTY),
30 detail: Some(prop.property_type.clone()),
31 documentation: Some(Documentation::String(prop.description.clone())),
32 ..CompletionItem::default()
33 }
34}
35
36pub fn property_completions(
38 object: &str,
39 type_context: &HashMap<String, String>,
40 struct_fields: &HashMap<String, Vec<(String, String)>>,
41 impl_methods: &HashMap<String, Vec<MethodCompletionInfo>>,
42) -> Vec<CompletionItem> {
43 if let Some(content_completions) = content_api_completions(object) {
45 return content_completions;
46 }
47
48 if let Some(ns_completions) = namespace_api_completions(object) {
50 return ns_completions;
51 }
52
53 let mut completions = Vec::new();
54 let meta = unified_metadata();
55
56 if let Some(resolved_type) = resolve_object_type(object, type_context, struct_fields) {
57 if extract_result_inner(&resolved_type).is_some() {
59 return result_method_completions();
60 }
61
62 if extract_option_inner(&resolved_type).is_some() {
64 return option_method_completions();
65 }
66
67 if let Some(props) = meta.get_type_properties(&resolved_type) {
69 for prop in props {
70 completions.push(property_completion_item(prop));
71 }
72 }
73
74 if completions.is_empty() {
76 if let Some(fields) = struct_fields.get(&resolved_type) {
77 for (name, field_type) in fields {
78 completions.push(CompletionItem {
79 label: name.clone(),
80 kind: Some(CompletionItemKind::FIELD),
81 detail: Some(field_type.clone()),
82 documentation: None,
83 ..CompletionItem::default()
84 });
85 }
86 }
87 }
88
89 if completions.is_empty() {
91 if let Some(fields) = parse_object_shape_fields(&resolved_type) {
92 for (name, field_type) in fields {
93 completions.push(CompletionItem {
94 label: name.clone(),
95 kind: Some(CompletionItemKind::FIELD),
96 detail: Some(field_type.clone()),
97 documentation: None,
98 ..CompletionItem::default()
99 });
100 }
101 }
102 }
103
104 let normalized = normalize_type_for_methods(&resolved_type);
109 let mut seen_method_names: std::collections::HashSet<String> =
110 std::collections::HashSet::new();
111 let lookup_keys: Vec<String> = if normalized == resolved_type {
112 vec![resolved_type.clone()]
113 } else {
114 vec![resolved_type.clone(), normalized.clone()]
115 };
116 for key in &lookup_keys {
117 let Some(methods) = impl_methods.get(key) else {
118 continue;
119 };
120 for method in methods {
121 if !seen_method_names.insert(method.name.clone()) {
122 continue;
123 }
124 let detail = method
125 .signature
126 .clone()
127 .unwrap_or_else(|| method.name.clone());
128 completions.push(CompletionItem {
129 label: method.name.clone(),
130 kind: Some(CompletionItemKind::METHOD),
131 detail: Some(detail),
132 documentation: method
133 .documentation
134 .as_ref()
135 .map(|doc| Documentation::String(doc.clone())),
136 insert_text: Some(format!("{}(${{1}})", method.name)),
137 insert_text_format: Some(InsertTextFormat::SNIPPET),
138 ..CompletionItem::default()
139 });
140 }
141 }
142
143 let table = method_table();
146 let method_type = normalized;
147 let builtin_methods = table.methods_for_type(&method_type);
148 for sig in &builtin_methods {
149 if !completions.iter().any(|c| c.label == sig.name) {
151 let has_params = !sig.param_types.is_empty();
152 let insert = if has_params {
153 format!("{}(${{1}})", sig.name)
154 } else {
155 format!("{}()", sig.name)
156 };
157 completions.push(CompletionItem {
158 label: sig.name.clone(),
159 kind: Some(CompletionItemKind::METHOD),
160 detail: Some(format!("{} method", resolved_type)),
161 documentation: None,
162 insert_text: Some(insert),
163 insert_text_format: Some(InsertTextFormat::SNIPPET),
164 ..CompletionItem::default()
165 });
166 }
167 }
168
169 if resolved_type.eq_ignore_ascii_case("backtestresult") {
171 return completions;
174 }
175
176 if is_column_type(&resolved_type) {
177 for method in LanguageMetadata::column_methods() {
178 completions.push(method_completion_item(&method));
179 }
180 return completions;
181 }
182
183 if resolved_type.eq_ignore_ascii_case("strategycontext") {
185 return completions;
187 }
188
189 if !completions.is_empty() {
190 return completions;
191 }
192 }
193
194 if object.contains("row") || object.contains("data") {
196 if let Some(props) = meta.get_type_properties("Row") {
197 for prop in props {
198 completions.push(property_completion_item(prop));
199 }
200 }
201 } else if object == "ctx" || object.ends_with(".ctx") {
202 } else if object.contains("series") || object.contains("column") || object.contains("data") {
204 for method in LanguageMetadata::column_methods() {
205 completions.push(method_completion_item(&method));
206 }
207 }
208 completions
211}
212
213pub fn type_completions() -> Vec<CompletionItem> {
215 LanguageMetadata::builtin_types()
216 .into_iter()
217 .map(|type_info| CompletionItem {
218 label: type_info.name,
219 kind: Some(CompletionItemKind::STRUCT),
220 detail: Some(type_info.description.clone()),
221 documentation: Some(Documentation::String(type_info.description)),
222 ..CompletionItem::default()
223 })
224 .collect()
225}
226
227pub fn resolve_object_type(
228 object: &str,
229 type_context: &HashMap<String, String>,
230 struct_fields: &HashMap<String, Vec<(String, String)>>,
231) -> Option<String> {
232 let parts: Vec<&str> = object.split('.').collect();
233 if parts.is_empty() {
234 return None;
235 }
236
237 let mut current = resolve_base_type(parts[0], type_context)?;
238 for part in parts.iter().skip(1) {
239 current = resolve_property_type(¤t, part, struct_fields)?;
240 }
241
242 Some(current)
243}
244
245pub fn resolve_base_type(segment: &str, type_context: &HashMap<String, String>) -> Option<String> {
246 let ident = segment.trim().split(['[', '(']).next().unwrap_or("").trim();
247 if ident.is_empty() {
248 return None;
249 }
250
251 type_context.get(ident).cloned()
252}
253
254pub fn resolve_property_type(
255 base_type: &str,
256 property: &str,
257 struct_fields: &HashMap<String, Vec<(String, String)>>,
258) -> Option<String> {
259 let property = property
260 .trim()
261 .split(['[', '('])
262 .next()
263 .unwrap_or("")
264 .trim();
265
266 if let Some(fields) = struct_fields.get(base_type) {
268 if let Some((_, field_type)) = fields.iter().find(|(name, _)| name == property) {
269 return Some(field_type.clone());
270 }
271 }
272
273 if let Some(fields) = parse_object_shape_fields(base_type) {
275 if let Some((_, field_type)) = fields.into_iter().find(|(name, _)| name == property) {
276 return Some(field_type);
277 }
278 }
279
280 let meta = unified_metadata();
282 if let Some(props) = meta.get_type_properties(base_type) {
283 if let Some(prop) = props.iter().find(|p| p.name == property) {
284 return Some(prop.property_type.clone());
285 }
286 }
287
288 if base_type.eq_ignore_ascii_case("backtestresult") {
290 match property {
291 "trades" => Some("Table<Trade>".to_string()),
292 "equity" | "returns" | "drawdown" | "positions" | "metrics" | "exposure" => {
293 Some("Table<Row>".to_string())
294 }
295 "summary" => Some("BacktestSummary".to_string()),
296 "config" => Some("Object".to_string()),
297 _ => None,
298 }
299 } else {
300 None
301 }
302}
303
304pub fn pipe_target_completions(
307 pipe_input_type: Option<&str>,
308 _type_context: &HashMap<String, String>,
309 impl_methods: &HashMap<String, Vec<MethodCompletionInfo>>,
310) -> Vec<CompletionItem> {
311 let mut completions = Vec::new();
312
313 if let Some(input_type) = pipe_input_type {
314 if let Some(methods) = impl_methods.get(input_type) {
316 for method in methods {
317 let detail = method
318 .signature
319 .clone()
320 .unwrap_or_else(|| method.name.clone());
321 completions.push(CompletionItem {
322 label: method.name.clone(),
323 kind: Some(CompletionItemKind::METHOD),
324 detail: Some(detail),
325 documentation: method
326 .documentation
327 .as_ref()
328 .map(|doc| Documentation::String(doc.clone())),
329 insert_text: Some(format!("{}(${{1}})", method.name)),
330 insert_text_format: Some(InsertTextFormat::SNIPPET),
331 ..CompletionItem::default()
332 });
333 }
334 }
335
336 let table = method_table();
338 let method_type = normalize_type_for_methods(input_type);
339 for sig in table.methods_for_type(&method_type) {
340 if !completions.iter().any(|c| c.label == sig.name) {
341 let has_params = !sig.param_types.is_empty();
342 let insert = if has_params {
343 format!("{}(${{1}})", sig.name)
344 } else {
345 format!("{}()", sig.name)
346 };
347 completions.push(CompletionItem {
348 label: sig.name.clone(),
349 kind: Some(CompletionItemKind::METHOD),
350 detail: Some(format!("{} method", input_type)),
351 documentation: None,
352 insert_text: Some(insert),
353 insert_text_format: Some(InsertTextFormat::SNIPPET),
354 ..CompletionItem::default()
355 });
356 }
357 }
358 }
359
360 let common_pipe_fns = ["map", "filter", "reduce", "forEach", "find", "sort"];
362 for name in &common_pipe_fns {
363 if !completions.iter().any(|c| c.label == *name) {
364 completions.push(CompletionItem {
365 label: name.to_string(),
366 kind: Some(CompletionItemKind::FUNCTION),
367 detail: Some("Common pipe function".to_string()),
368 insert_text: Some(format!("{}(${{1}})", name)),
369 insert_text_format: Some(InsertTextFormat::SNIPPET),
370 ..CompletionItem::default()
371 });
372 }
373 }
374
375 completions
376}
377
378fn normalize_type_for_methods(type_name: &str) -> String {
381 if let Some(generic_start) = type_name.find('<') {
382 let base = type_name[..generic_start].trim();
383 if !base.is_empty() {
384 return normalize_type_for_methods(base);
385 }
386 }
387 if type_name.ends_with("[]") {
388 return "Vec".to_string();
389 }
390 match type_name {
391 "int" | "decimal" | "float" => "number".to_string(),
392 "Array" => "Vec".to_string(),
393 _ => type_name.to_string(),
394 }
395}
396
397pub fn is_column_type(type_name: &str) -> bool {
398 let lower = type_name.to_lowercase();
399 lower == "series"
400 || lower.starts_with("series<")
401 || lower == "column"
402 || lower.starts_with("column<")
403}
404
405fn content_api_completions(object: &str) -> Option<Vec<CompletionItem>> {
407 let items: Vec<(&str, CompletionItemKind, &str, &str)> = match object {
408 "Content" => vec![
409 (
410 "text",
411 CompletionItemKind::METHOD,
412 "text(${1:string})",
413 "Create a plain text content node",
414 ),
415 (
416 "table",
417 CompletionItemKind::METHOD,
418 "table(${1:data})",
419 "Create a table from a collection",
420 ),
421 (
422 "chart",
423 CompletionItemKind::METHOD,
424 "chart(${1:type}, ${2:data})",
425 "Create a chart visualization",
426 ),
427 (
428 "fragment",
429 CompletionItemKind::METHOD,
430 "fragment(${1:parts})",
431 "Compose multiple content nodes",
432 ),
433 (
434 "code",
435 CompletionItemKind::METHOD,
436 "code(${1:language}, ${2:source})",
437 "Create a code block",
438 ),
439 (
440 "kv",
441 CompletionItemKind::METHOD,
442 "kv(${1:pairs})",
443 "Create key-value content",
444 ),
445 ],
446 "Color" => vec![
447 (
448 "red",
449 CompletionItemKind::ENUM_MEMBER,
450 "red",
451 "Red terminal color",
452 ),
453 (
454 "green",
455 CompletionItemKind::ENUM_MEMBER,
456 "green",
457 "Green terminal color",
458 ),
459 (
460 "blue",
461 CompletionItemKind::ENUM_MEMBER,
462 "blue",
463 "Blue terminal color",
464 ),
465 (
466 "yellow",
467 CompletionItemKind::ENUM_MEMBER,
468 "yellow",
469 "Yellow terminal color",
470 ),
471 (
472 "magenta",
473 CompletionItemKind::ENUM_MEMBER,
474 "magenta",
475 "Magenta terminal color",
476 ),
477 (
478 "cyan",
479 CompletionItemKind::ENUM_MEMBER,
480 "cyan",
481 "Cyan terminal color",
482 ),
483 (
484 "white",
485 CompletionItemKind::ENUM_MEMBER,
486 "white",
487 "White terminal color",
488 ),
489 (
490 "default",
491 CompletionItemKind::ENUM_MEMBER,
492 "default",
493 "Default terminal color",
494 ),
495 (
496 "rgb",
497 CompletionItemKind::METHOD,
498 "rgb(${1:r}, ${2:g}, ${3:b})",
499 "Custom RGB color (0-255 per channel)",
500 ),
501 ],
502 "Border" => vec![
503 (
504 "rounded",
505 CompletionItemKind::ENUM_MEMBER,
506 "rounded",
507 "Rounded corners (default)",
508 ),
509 (
510 "sharp",
511 CompletionItemKind::ENUM_MEMBER,
512 "sharp",
513 "Sharp 90-degree corners",
514 ),
515 (
516 "heavy",
517 CompletionItemKind::ENUM_MEMBER,
518 "heavy",
519 "Thick border lines",
520 ),
521 (
522 "double",
523 CompletionItemKind::ENUM_MEMBER,
524 "double",
525 "Double-line border",
526 ),
527 (
528 "minimal",
529 CompletionItemKind::ENUM_MEMBER,
530 "minimal",
531 "Minimal separator lines",
532 ),
533 ("none", CompletionItemKind::ENUM_MEMBER, "none", "No border"),
534 ],
535 "ChartType" => vec![
536 (
537 "line",
538 CompletionItemKind::ENUM_MEMBER,
539 "line",
540 "Line chart",
541 ),
542 ("bar", CompletionItemKind::ENUM_MEMBER, "bar", "Bar chart"),
543 (
544 "scatter",
545 CompletionItemKind::ENUM_MEMBER,
546 "scatter",
547 "Scatter plot",
548 ),
549 (
550 "area",
551 CompletionItemKind::ENUM_MEMBER,
552 "area",
553 "Area chart",
554 ),
555 (
556 "candlestick",
557 CompletionItemKind::ENUM_MEMBER,
558 "candlestick",
559 "Candlestick chart",
560 ),
561 (
562 "histogram",
563 CompletionItemKind::ENUM_MEMBER,
564 "histogram",
565 "Histogram",
566 ),
567 ],
568 "Align" => vec![
569 (
570 "left",
571 CompletionItemKind::ENUM_MEMBER,
572 "left",
573 "Left-aligned (default)",
574 ),
575 (
576 "center",
577 CompletionItemKind::ENUM_MEMBER,
578 "center",
579 "Center-aligned",
580 ),
581 (
582 "right",
583 CompletionItemKind::ENUM_MEMBER,
584 "right",
585 "Right-aligned",
586 ),
587 ],
588 _ => return None,
589 };
590
591 let completions = items
592 .into_iter()
593 .map(|(label, kind, insert, doc)| {
594 let is_snippet = insert.contains("${");
595 CompletionItem {
596 label: label.to_string(),
597 kind: Some(kind),
598 detail: Some(format!("{} member", object)),
599 documentation: Some(Documentation::String(doc.to_string())),
600 insert_text: Some(insert.to_string()),
601 insert_text_format: if is_snippet {
602 Some(InsertTextFormat::SNIPPET)
603 } else {
604 None
605 },
606 ..CompletionItem::default()
607 }
608 })
609 .collect();
610
611 Some(completions)
612}
613
614fn namespace_api_completions(object: &str) -> Option<Vec<CompletionItem>> {
616 let items: Vec<(&str, CompletionItemKind, &str, &str)> = match object {
617 "DateTime" => vec![
618 (
619 "now",
620 CompletionItemKind::METHOD,
621 "now()",
622 "Current local time as DateTime",
623 ),
624 (
625 "utc",
626 CompletionItemKind::METHOD,
627 "utc()",
628 "Current UTC time as DateTime",
629 ),
630 (
631 "parse",
632 CompletionItemKind::METHOD,
633 "parse(${1:string})",
634 "Parse a date/time string (ISO 8601, RFC 2822, common formats)",
635 ),
636 (
637 "from_epoch",
638 CompletionItemKind::METHOD,
639 "from_epoch(${1:ms})",
640 "Create DateTime from milliseconds since Unix epoch",
641 ),
642 (
643 "from_parts",
644 CompletionItemKind::METHOD,
645 "from_parts(${1:year}, ${2:month}, ${3:day})",
646 "Create DateTime from components (year, month, day, hour?, minute?, second?) at UTC",
647 ),
648 (
649 "from_unix_secs",
650 CompletionItemKind::METHOD,
651 "from_unix_secs(${1:secs})",
652 "Create DateTime from seconds since Unix epoch",
653 ),
654 ],
655 "io" => vec![
656 (
657 "open",
658 CompletionItemKind::FUNCTION,
659 "open(${1:path}, ${2:mode})",
660 "Open a file and return a handle",
661 ),
662 (
663 "read",
664 CompletionItemKind::FUNCTION,
665 "read(${1:handle})",
666 "Read from a file handle",
667 ),
668 (
669 "read_to_string",
670 CompletionItemKind::FUNCTION,
671 "read_to_string(${1:handle})",
672 "Read entire file as a string",
673 ),
674 (
675 "write",
676 CompletionItemKind::FUNCTION,
677 "write(${1:handle}, ${2:data})",
678 "Write data to a file handle",
679 ),
680 (
681 "close",
682 CompletionItemKind::FUNCTION,
683 "close(${1:handle})",
684 "Close a file handle",
685 ),
686 (
687 "flush",
688 CompletionItemKind::FUNCTION,
689 "flush(${1:handle})",
690 "Flush buffered writes to disk",
691 ),
692 (
693 "exists",
694 CompletionItemKind::FUNCTION,
695 "exists(${1:path})",
696 "Check if a file or directory exists",
697 ),
698 (
699 "stat",
700 CompletionItemKind::FUNCTION,
701 "stat(${1:path})",
702 "Get file metadata (size, modified, is_dir, is_file)",
703 ),
704 (
705 "mkdir",
706 CompletionItemKind::FUNCTION,
707 "mkdir(${1:path})",
708 "Create a directory (recursive)",
709 ),
710 (
711 "remove",
712 CompletionItemKind::FUNCTION,
713 "remove(${1:path})",
714 "Remove a file or empty directory",
715 ),
716 (
717 "rename",
718 CompletionItemKind::FUNCTION,
719 "rename(${1:from}, ${2:to})",
720 "Rename/move a file or directory",
721 ),
722 (
723 "read_dir",
724 CompletionItemKind::FUNCTION,
725 "read_dir(${1:path})",
726 "List directory entries",
727 ),
728 (
729 "join",
730 CompletionItemKind::FUNCTION,
731 "join(${1:base}, ${2:path})",
732 "Join path components",
733 ),
734 (
735 "dirname",
736 CompletionItemKind::FUNCTION,
737 "dirname(${1:path})",
738 "Get parent directory of a path",
739 ),
740 (
741 "basename",
742 CompletionItemKind::FUNCTION,
743 "basename(${1:path})",
744 "Get file name from a path",
745 ),
746 (
747 "extension",
748 CompletionItemKind::FUNCTION,
749 "extension(${1:path})",
750 "Get file extension from a path",
751 ),
752 (
753 "resolve",
754 CompletionItemKind::FUNCTION,
755 "resolve(${1:path})",
756 "Resolve to absolute path",
757 ),
758 (
759 "tcp_connect",
760 CompletionItemKind::FUNCTION,
761 "tcp_connect(${1:addr})",
762 "Connect to a TCP server",
763 ),
764 (
765 "tcp_listen",
766 CompletionItemKind::FUNCTION,
767 "tcp_listen(${1:addr})",
768 "Bind a TCP listener",
769 ),
770 (
771 "udp_bind",
772 CompletionItemKind::FUNCTION,
773 "udp_bind(${1:addr})",
774 "Bind a UDP socket",
775 ),
776 (
777 "spawn",
778 CompletionItemKind::FUNCTION,
779 "spawn(${1:program}, ${2:args})",
780 "Spawn a child process",
781 ),
782 (
783 "exec",
784 CompletionItemKind::FUNCTION,
785 "exec(${1:program}, ${2:args})",
786 "Execute a command and collect output",
787 ),
788 (
789 "stdin",
790 CompletionItemKind::FUNCTION,
791 "stdin()",
792 "Open standard input as a handle",
793 ),
794 (
795 "stdout",
796 CompletionItemKind::FUNCTION,
797 "stdout()",
798 "Open standard output as a handle",
799 ),
800 (
801 "stderr",
802 CompletionItemKind::FUNCTION,
803 "stderr()",
804 "Open standard error as a handle",
805 ),
806 (
807 "read_line",
808 CompletionItemKind::FUNCTION,
809 "read_line(${1:handle})",
810 "Read a line from a handle or stdin",
811 ),
812 ],
813 "time" => vec![
814 (
815 "now",
816 CompletionItemKind::FUNCTION,
817 "now()",
818 "Return the current monotonic instant",
819 ),
820 (
821 "sleep",
822 CompletionItemKind::FUNCTION,
823 "sleep(${1:ms})",
824 "Sleep for ms milliseconds (async)",
825 ),
826 (
827 "sleep_sync",
828 CompletionItemKind::FUNCTION,
829 "sleep_sync(${1:ms})",
830 "Sleep for ms milliseconds (blocking)",
831 ),
832 (
833 "benchmark",
834 CompletionItemKind::FUNCTION,
835 "benchmark(${1:fn}, ${2:iterations})",
836 "Benchmark a function over N iterations",
837 ),
838 (
839 "stopwatch",
840 CompletionItemKind::FUNCTION,
841 "stopwatch()",
842 "Start a stopwatch (returns Instant)",
843 ),
844 (
845 "millis",
846 CompletionItemKind::FUNCTION,
847 "millis()",
848 "Current wall-clock time as epoch milliseconds",
849 ),
850 ],
851 _ => return None,
852 };
853
854 let completions = items
855 .into_iter()
856 .map(|(label, kind, insert, doc)| {
857 let is_snippet = insert.contains("${");
858 CompletionItem {
859 label: label.to_string(),
860 kind: Some(kind),
861 detail: Some(format!("{} member", object)),
862 documentation: Some(Documentation::String(doc.to_string())),
863 insert_text: Some(insert.to_string()),
864 insert_text_format: if is_snippet {
865 Some(InsertTextFormat::SNIPPET)
866 } else {
867 None
868 },
869 ..CompletionItem::default()
870 }
871 })
872 .collect();
873
874 Some(completions)
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 #[test]
882 fn test_resolve_property_type_struct_fields() {
883 let mut struct_fields = HashMap::new();
884 struct_fields.insert(
885 "Point".to_string(),
886 vec![
887 ("x".to_string(), "number".to_string()),
888 ("y".to_string(), "string".to_string()),
889 ],
890 );
891 assert_eq!(
892 resolve_property_type("Point", "x", &struct_fields),
893 Some("number".to_string())
894 );
895 assert_eq!(
896 resolve_property_type("Point", "y", &struct_fields),
897 Some("string".to_string())
898 );
899 }
900
901 #[test]
902 fn test_resolve_property_type_unknown_field() {
903 let mut struct_fields = HashMap::new();
904 struct_fields.insert(
905 "Point".to_string(),
906 vec![("x".to_string(), "number".to_string())],
907 );
908 assert_eq!(resolve_property_type("Point", "z", &struct_fields), None);
909 }
910
911 #[test]
912 fn test_resolve_object_type_chained() {
913 let mut type_context = HashMap::new();
914 type_context.insert("a".to_string(), "A".to_string());
915 let mut struct_fields = HashMap::new();
916 struct_fields.insert("A".to_string(), vec![("b".to_string(), "B".to_string())]);
917 struct_fields.insert(
918 "B".to_string(),
919 vec![("c".to_string(), "number".to_string())],
920 );
921 assert_eq!(
922 resolve_object_type("a.b", &type_context, &struct_fields),
923 Some("B".to_string())
924 );
925 }
926
927 #[test]
928 fn test_content_api_completions_content() {
929 let result = content_api_completions("Content");
930 assert!(result.is_some(), "Content. should produce completions");
931 let items = result.unwrap();
932 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
933 assert!(labels.contains(&"text"), "missing Content.text");
934 assert!(labels.contains(&"table"), "missing Content.table");
935 assert!(labels.contains(&"chart"), "missing Content.chart");
936 assert!(labels.contains(&"fragment"), "missing Content.fragment");
937 assert!(labels.contains(&"code"), "missing Content.code");
938 assert!(labels.contains(&"kv"), "missing Content.kv");
939 }
940
941 #[test]
942 fn test_content_api_completions_color() {
943 let result = content_api_completions("Color");
944 assert!(result.is_some(), "Color. should produce completions");
945 let items = result.unwrap();
946 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
947 assert!(labels.contains(&"red"), "missing Color.red");
948 assert!(labels.contains(&"green"), "missing Color.green");
949 assert!(labels.contains(&"rgb"), "missing Color.rgb");
950 }
951
952 #[test]
953 fn test_content_api_completions_border() {
954 let result = content_api_completions("Border");
955 assert!(result.is_some(), "Border. should produce completions");
956 let items = result.unwrap();
957 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
958 assert!(labels.contains(&"rounded"), "missing Border.rounded");
959 assert!(labels.contains(&"none"), "missing Border.none");
960 }
961
962 #[test]
963 fn test_content_api_completions_charttype() {
964 let result = content_api_completions("ChartType");
965 assert!(result.is_some(), "ChartType. should produce completions");
966 let items = result.unwrap();
967 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
968 assert!(labels.contains(&"line"), "missing ChartType.line");
969 assert!(
970 labels.contains(&"candlestick"),
971 "missing ChartType.candlestick"
972 );
973 }
974
975 #[test]
976 fn test_content_api_completions_align() {
977 let result = content_api_completions("Align");
978 assert!(result.is_some(), "Align. should produce completions");
979 let items = result.unwrap();
980 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
981 assert!(labels.contains(&"left"), "missing Align.left");
982 assert!(labels.contains(&"center"), "missing Align.center");
983 assert!(labels.contains(&"right"), "missing Align.right");
984 }
985
986 #[test]
987 fn test_content_api_completions_unknown() {
988 assert!(content_api_completions("Foo").is_none());
989 assert!(content_api_completions("content").is_none()); }
991
992 #[test]
993 fn test_namespace_completions_datetime() {
994 let result = namespace_api_completions("DateTime");
995 assert!(result.is_some(), "DateTime. should produce completions");
996 let items = result.unwrap();
997 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
998 assert!(labels.contains(&"now"), "missing DateTime.now");
999 assert!(labels.contains(&"utc"), "missing DateTime.utc");
1000 assert!(labels.contains(&"parse"), "missing DateTime.parse");
1001 assert!(
1002 labels.contains(&"from_epoch"),
1003 "missing DateTime.from_epoch"
1004 );
1005 assert!(
1006 labels.contains(&"from_parts"),
1007 "missing DateTime.from_parts"
1008 );
1009 assert!(
1010 labels.contains(&"from_unix_secs"),
1011 "missing DateTime.from_unix_secs"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_namespace_completions_io() {
1017 let result = namespace_api_completions("io");
1018 assert!(result.is_some(), "io. should produce completions");
1019 let items = result.unwrap();
1020 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1021 assert!(labels.contains(&"open"), "missing io.open");
1022 assert!(labels.contains(&"read"), "missing io.read");
1023 assert!(labels.contains(&"write"), "missing io.write");
1024 assert!(labels.contains(&"close"), "missing io.close");
1025 assert!(labels.contains(&"exists"), "missing io.exists");
1026 assert!(labels.contains(&"stat"), "missing io.stat");
1027 assert!(labels.contains(&"tcp_connect"), "missing io.tcp_connect");
1028 assert!(labels.contains(&"spawn"), "missing io.spawn");
1029 assert!(labels.contains(&"stdin"), "missing io.stdin");
1030 }
1031
1032 #[test]
1033 fn test_namespace_completions_time() {
1034 let result = namespace_api_completions("time");
1035 assert!(result.is_some(), "time. should produce completions");
1036 let items = result.unwrap();
1037 let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
1038 assert!(labels.contains(&"now"), "missing time.now");
1039 assert!(labels.contains(&"sleep"), "missing time.sleep");
1040 assert!(labels.contains(&"benchmark"), "missing time.benchmark");
1041 assert!(labels.contains(&"stopwatch"), "missing time.stopwatch");
1042 assert!(labels.contains(&"millis"), "missing time.millis");
1043 }
1044
1045 #[test]
1046 fn test_namespace_completions_unknown() {
1047 assert!(namespace_api_completions("Foo").is_none());
1048 assert!(namespace_api_completions("datetime").is_none()); }
1050}