Skip to main content

shape_lsp/completion/
types.rs

1//! Type and property completions
2
3use 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
16/// Global method table, loaded lazily
17static 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
25/// Convert a PropertyInfo to a CompletionItem
26pub 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
36/// Get property completions based on object type (from unified metadata)
37pub 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    // Content API namespace completions (Content., Color., Border., ChartType., Align.)
44    if let Some(content_completions) = content_api_completions(object) {
45        return content_completions;
46    }
47
48    // DateTime / io / time namespace completions
49    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        // Check for Result<T> wrapper type FIRST - provide Result methods
58        if extract_result_inner(&resolved_type).is_some() {
59            return result_method_completions();
60        }
61
62        // Check for Option<T> wrapper type - provide Option methods
63        if extract_option_inner(&resolved_type).is_some() {
64            return option_method_completions();
65        }
66
67        // Try to get properties from unified metadata (derive macro source)
68        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        // Try user-defined struct fields from parsed AST
75        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        // Inline/structural object shape (e.g. "{ x: int, y: int }")
90        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        // Add methods from impl/extend/trait blocks. Try the resolved type
105        // first (e.g. `"User"` or `"Array<int>"`); also try the normalized
106        // form (e.g. `"Vec"`) so stdlib `extend Vec<T> { ... }` methods
107        // surface on `Array<T>` / `int[]` receivers. LSP-C audit close.
108        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        // Add builtin methods from MethodTable (string, number, Array, etc.)
144        // Normalize type names to match MethodTable keys
145        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            // Avoid duplicates with already-added completions
150            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        // Add methods for specific types (methods not yet in derive macro system)
170        if resolved_type.eq_ignore_ascii_case("backtestresult") {
171            // for method in LanguageMetadata::backtest_result_methods() { ... }
172            // for prop in LanguageMetadata::strategy_context_properties() { ... }
173            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        // StrategyContext (not yet annotated with derive macro)
184        if resolved_type.eq_ignore_ascii_case("strategycontext") {
185            // for prop in LanguageMetadata::strategy_context_properties() { ... }
186            return completions;
187        }
188
189        if !completions.is_empty() {
190            return completions;
191        }
192    }
193
194    // Fallback heuristics for unresolved types
195    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        // StrategyContext (not yet annotated)
203    } 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    // Generic object fallback: no synthetic properties.
209
210    completions
211}
212
213/// Get type completions from metadata API
214pub 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(&current, 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    // 1. Check user-defined struct fields
267    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    // 2. Check inline/structural object type fields
274    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    // 3. Check unified metadata for type properties
281    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    // 4. Hardcoded fallback for BacktestResult (legacy)
289    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
304/// Get completions for pipe target position (`expr |> <cursor>`).
305/// Shows methods applicable to the piped type, plus general functions.
306pub 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        // Show impl/extend methods for the piped type
315        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        // Show builtin methods from MethodTable for the piped type
337        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    // Always show common pipe-friendly functions
361    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
378/// Normalize a type name to match MethodTable keys.
379/// E.g., "int[]" → "Array", "int" → "number", "decimal" → "number"
380fn 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
405/// Get completions for Content API namespaces (Content., Color., Border., ChartType., Align.)
406fn 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
614/// Get completions for DateTime / io / time namespaces
615fn 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()); // case-sensitive
990    }
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()); // case-sensitive
1049    }
1050}