Skip to main content

shape_lsp/completion/
methods.rs

1//! Method completions for Result, Option, and other types
2
3use shape_runtime::metadata::MethodInfo;
4use tower_lsp_server::ls_types::{
5    CompletionItem, CompletionItemKind, Documentation, InsertTextFormat,
6};
7
8pub fn method_completion_item(method: &MethodInfo) -> CompletionItem {
9    let detail = if method.implemented {
10        method.signature.clone()
11    } else {
12        format!("{} (unimplemented)", method.signature)
13    };
14
15    let doc = if method.implemented {
16        method.description.clone()
17    } else {
18        format!("{}\n\nNOTE: Not yet implemented.", method.description)
19    };
20
21    let has_params = !method.signature.contains("()");
22    let insert_text = if has_params {
23        Some(format!("{}(${{1}})", method.name))
24    } else {
25        Some(format!("{}()", method.name))
26    };
27
28    CompletionItem {
29        label: method.name.clone(),
30        kind: Some(CompletionItemKind::METHOD),
31        detail: Some(detail),
32        documentation: Some(Documentation::String(doc)),
33        insert_text,
34        insert_text_format: Some(InsertTextFormat::SNIPPET),
35        ..CompletionItem::default()
36    }
37}
38
39/// Check if type is Result<T> and return the inner type T
40pub fn extract_result_inner(type_name: &str) -> Option<String> {
41    parse_generic_type(type_name).and_then(|(base, args)| {
42        if base.eq_ignore_ascii_case("result") {
43            args.into_iter().next()
44        } else {
45            None
46        }
47    })
48}
49
50/// Check if type is Option<T> and return the inner type T
51pub fn extract_option_inner(type_name: &str) -> Option<String> {
52    // Check for Option<T> syntax
53    if let Some((base, args)) = parse_generic_type(type_name) {
54        if base.eq_ignore_ascii_case("option") {
55            return args.into_iter().next();
56        }
57    }
58    // Check for T? syntax (trailing question mark)
59    if type_name.ends_with('?') {
60        return Some(type_name[..type_name.len() - 1].to_string());
61    }
62    None
63}
64
65/// Get method completions for Result type
66pub fn result_method_completions() -> Vec<CompletionItem> {
67    vec![
68        CompletionItem {
69            label: "unwrap".to_string(),
70            kind: Some(CompletionItemKind::METHOD),
71            detail: Some("T".to_string()),
72            documentation: None,
73            insert_text: Some("unwrap()".to_string()),
74            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
75            ..CompletionItem::default()
76        },
77        CompletionItem {
78            label: "unwrap_or".to_string(),
79            kind: Some(CompletionItemKind::METHOD),
80            detail: Some("T".to_string()),
81            documentation: None,
82            insert_text: Some("unwrap_or(${1:default})".to_string()),
83            insert_text_format: Some(InsertTextFormat::SNIPPET),
84            ..CompletionItem::default()
85        },
86        CompletionItem {
87            label: "is_ok".to_string(),
88            kind: Some(CompletionItemKind::METHOD),
89            detail: Some("Boolean".to_string()),
90            documentation: None,
91            insert_text: Some("is_ok()".to_string()),
92            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
93            ..CompletionItem::default()
94        },
95        CompletionItem {
96            label: "is_err".to_string(),
97            kind: Some(CompletionItemKind::METHOD),
98            detail: Some("Boolean".to_string()),
99            documentation: None,
100            insert_text: Some("is_err()".to_string()),
101            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
102            ..CompletionItem::default()
103        },
104    ]
105}
106
107/// Get method completions for Option type
108pub fn option_method_completions() -> Vec<CompletionItem> {
109    vec![
110        CompletionItem {
111            label: "unwrap".to_string(),
112            kind: Some(CompletionItemKind::METHOD),
113            detail: Some("T".to_string()),
114            documentation: None,
115            insert_text: Some("unwrap()".to_string()),
116            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
117            ..CompletionItem::default()
118        },
119        CompletionItem {
120            label: "unwrap_or".to_string(),
121            kind: Some(CompletionItemKind::METHOD),
122            detail: Some("T".to_string()),
123            documentation: None,
124            insert_text: Some("unwrap_or(${1:default})".to_string()),
125            insert_text_format: Some(InsertTextFormat::SNIPPET),
126            ..CompletionItem::default()
127        },
128        CompletionItem {
129            label: "is_some".to_string(),
130            kind: Some(CompletionItemKind::METHOD),
131            detail: Some("Boolean".to_string()),
132            documentation: None,
133            insert_text: Some("is_some()".to_string()),
134            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
135            ..CompletionItem::default()
136        },
137        CompletionItem {
138            label: "is_none".to_string(),
139            kind: Some(CompletionItemKind::METHOD),
140            detail: Some("Boolean".to_string()),
141            documentation: None,
142            insert_text: Some("is_none()".to_string()),
143            insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
144            ..CompletionItem::default()
145        },
146    ]
147}
148
149pub fn parse_generic_type(type_name: &str) -> Option<(String, Vec<String>)> {
150    let start = type_name.find('<')?;
151    let end = type_name.rfind('>')?;
152    if end <= start {
153        return None;
154    }
155    let base = type_name[..start].trim().to_string();
156    let inner = type_name[start + 1..end].trim();
157    if inner.is_empty() {
158        return Some((base, Vec::new()));
159    }
160    let args = split_top_level_commas(inner);
161    Some((base, args))
162}
163
164fn split_top_level_commas(input: &str) -> Vec<String> {
165    let mut args = Vec::new();
166    let mut start = 0usize;
167    let mut angle_depth = 0usize;
168    let mut paren_depth = 0usize;
169    let mut bracket_depth = 0usize;
170    let mut brace_depth = 0usize;
171
172    for (idx, ch) in input.char_indices() {
173        match ch {
174            '<' => angle_depth += 1,
175            '>' => angle_depth = angle_depth.saturating_sub(1),
176            '(' => paren_depth += 1,
177            ')' => paren_depth = paren_depth.saturating_sub(1),
178            '[' => bracket_depth += 1,
179            ']' => bracket_depth = bracket_depth.saturating_sub(1),
180            '{' => brace_depth += 1,
181            '}' => brace_depth = brace_depth.saturating_sub(1),
182            ',' => {
183                if angle_depth == 0 && paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 {
184                    let part = input[start..idx].trim();
185                    if !part.is_empty() {
186                        args.push(part.to_string());
187                    }
188                    start = idx + ch.len_utf8();
189                }
190            }
191            _ => {}
192        }
193    }
194
195    let tail = input[start..].trim();
196    if !tail.is_empty() {
197        args.push(tail.to_string());
198    }
199
200    args
201}
202
203/// Extract a specific generic argument by index from a type name
204/// Example: extract_generic_arg("Table<Row>", 0) -> Some("Row")
205/// Example: extract_generic_arg("Map<String, Number>", 1) -> Some("Number")
206pub fn extract_generic_arg(type_name: &str, index: usize) -> Option<String> {
207    let (_, args) = parse_generic_type(type_name)?;
208    args.get(index).cloned()
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_extract_result_inner() {
217        // Test the helper function directly
218        assert_eq!(
219            extract_result_inner("Result<Instrument>"),
220            Some("Instrument".to_string())
221        );
222        assert_eq!(
223            extract_result_inner("Result<Number>"),
224            Some("Number".to_string())
225        );
226        assert_eq!(extract_result_inner("Instrument"), None);
227        assert_eq!(extract_result_inner("Option<Number>"), None);
228    }
229
230    #[test]
231    fn test_extract_option_inner() {
232        // Test the helper function directly
233        assert_eq!(
234            extract_option_inner("Option<Number>"),
235            Some("Number".to_string())
236        );
237        assert_eq!(extract_option_inner("Number?"), Some("Number".to_string()));
238        assert_eq!(extract_option_inner("Number"), None);
239        assert_eq!(extract_option_inner("Result<Number>"), None);
240    }
241
242    #[test]
243    fn test_parse_generic_type_with_structural_arg() {
244        let parsed = parse_generic_type("Table<{ open: number, close: number }>")
245            .expect("should parse generic type");
246        assert_eq!(parsed.0, "Table");
247        assert_eq!(parsed.1, vec!["{ open: number, close: number }"]);
248    }
249
250    #[test]
251    fn test_parse_generic_type_nested_args() {
252        let parsed =
253            parse_generic_type("Map<string, List<int>>").expect("should parse nested generic type");
254        assert_eq!(parsed.0, "Map");
255        assert_eq!(parsed.1, vec!["string", "List<int>"]);
256    }
257}