Skip to main content

php_lsp/completion/
symbols.rs

1use php_ast::{ClassMemberKind, EnumMemberKind, ExprKind, NamespaceBody, Stmt, StmtKind};
2use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind};
3
4use crate::ast::{ParsedDoc, SourceView};
5
6use super::{build_function_sig, callable_item, docblock_docs, named_arg_item};
7
8pub fn symbol_completions(doc: &ParsedDoc) -> Vec<CompletionItem> {
9    let mut items = Vec::new();
10    collect_from_statements_with_doc(&doc.program().stmts, &mut items, Some(doc));
11    items
12}
13
14/// Like `symbol_completions` but only includes variables declared at or before `line`.
15/// Non-variable items (functions, classes, etc.) are always included.
16pub fn symbol_completions_before(doc: &ParsedDoc, line: u32) -> Vec<CompletionItem> {
17    let sv = doc.view();
18    let mut items = Vec::new();
19    collect_from_statements_before(&doc.program().stmts, &mut items, line, sv, Some(doc));
20    items
21}
22
23fn collect_from_statements_before(
24    stmts: &[Stmt<'_, '_>],
25    items: &mut Vec<CompletionItem>,
26    line: u32,
27    sv: SourceView<'_>,
28    doc: Option<&ParsedDoc>,
29) {
30    for stmt in stmts {
31        match &stmt.kind {
32            StmtKind::Expression(e) => {
33                // Only add variables if they appear at or before the cursor line
34                let stmt_line = sv.position_of(stmt.span.start).line;
35                if stmt_line <= line {
36                    collect_from_expression(e, items);
37                }
38            }
39            StmtKind::Namespace(ns) => {
40                if let NamespaceBody::Braced(inner) = &ns.body {
41                    collect_from_statements_before(inner, items, line, sv, doc);
42                }
43            }
44            // Non-variable items: always include
45            _ => {
46                collect_from_statements_with_doc(std::slice::from_ref(stmt), items, doc);
47            }
48        }
49    }
50}
51
52fn collect_from_statements_with_doc(
53    stmts: &[Stmt<'_, '_>],
54    items: &mut Vec<CompletionItem>,
55    doc: Option<&ParsedDoc>,
56) {
57    for stmt in stmts {
58        match &stmt.kind {
59            StmtKind::Function(f) => {
60                let sig = build_function_sig(f.name, &f.params, f.return_type.as_ref());
61                let documentation = doc.and_then(|d| docblock_docs(d, f.name));
62                let mut item =
63                    callable_item(f.name, CompletionItemKind::FUNCTION, !f.params.is_empty());
64                item.detail = Some(sig);
65                item.documentation = documentation;
66                items.push(item);
67                if let Some(named) = named_arg_item(f.name, CompletionItemKind::FUNCTION, &f.params)
68                {
69                    items.push(named);
70                }
71                for param in f.params.iter() {
72                    items.push(CompletionItem {
73                        label: format!("${}", param.name),
74                        kind: Some(CompletionItemKind::VARIABLE),
75                        ..Default::default()
76                    });
77                }
78            }
79            StmtKind::Class(c) => {
80                let class_name = c.name.unwrap_or("");
81                if !class_name.is_empty() {
82                    items.push(CompletionItem {
83                        label: class_name.to_string(),
84                        kind: Some(CompletionItemKind::CLASS),
85                        ..Default::default()
86                    });
87                }
88                for member in c.members.iter() {
89                    match &member.kind {
90                        ClassMemberKind::Method(m) => {
91                            let sig = build_function_sig(m.name, &m.params, m.return_type.as_ref());
92                            let documentation = doc.and_then(|d| docblock_docs(d, m.name));
93                            let mut item = callable_item(
94                                m.name,
95                                CompletionItemKind::METHOD,
96                                !m.params.is_empty(),
97                            );
98                            item.detail = Some(sig);
99                            item.documentation = documentation;
100                            items.push(item);
101                            if let Some(named) =
102                                named_arg_item(m.name, CompletionItemKind::METHOD, &m.params)
103                            {
104                                items.push(named);
105                            }
106                        }
107                        ClassMemberKind::Property(p) => {
108                            items.push(CompletionItem {
109                                label: format!("${}", p.name),
110                                kind: Some(CompletionItemKind::PROPERTY),
111                                ..Default::default()
112                            });
113                        }
114                        ClassMemberKind::ClassConst(c) => {
115                            items.push(CompletionItem {
116                                label: c.name.to_string(),
117                                kind: Some(CompletionItemKind::CONSTANT),
118                                ..Default::default()
119                            });
120                        }
121                        _ => {}
122                    }
123                }
124            }
125            StmtKind::Interface(i) => {
126                items.push(CompletionItem {
127                    label: i.name.to_string(),
128                    kind: Some(CompletionItemKind::INTERFACE),
129                    ..Default::default()
130                });
131            }
132            StmtKind::Trait(t) => {
133                items.push(CompletionItem {
134                    label: t.name.to_string(),
135                    kind: Some(CompletionItemKind::CLASS),
136                    ..Default::default()
137                });
138            }
139            StmtKind::Enum(e) => {
140                items.push(CompletionItem {
141                    label: e.name.to_string(),
142                    kind: Some(CompletionItemKind::ENUM),
143                    ..Default::default()
144                });
145                for member in e.members.iter() {
146                    if let EnumMemberKind::Case(c) = &member.kind {
147                        items.push(CompletionItem {
148                            label: format!("{}::{}", e.name, c.name),
149                            kind: Some(CompletionItemKind::ENUM_MEMBER),
150                            ..Default::default()
151                        });
152                    }
153                }
154            }
155            StmtKind::Namespace(ns) => {
156                if let NamespaceBody::Braced(inner) = &ns.body {
157                    collect_from_statements_with_doc(inner, items, doc);
158                }
159            }
160            StmtKind::Expression(e) => {
161                collect_from_expression(e, items);
162            }
163            _ => {}
164        }
165    }
166}
167
168fn collect_from_expression(expr: &php_ast::Expr<'_, '_>, items: &mut Vec<CompletionItem>) {
169    if let ExprKind::Assign(assign) = &expr.kind {
170        match &assign.target.kind {
171            ExprKind::Variable(name) => {
172                let label = format!("${}", name.as_str());
173                if label != "$this" {
174                    items.push(CompletionItem {
175                        label,
176                        kind: Some(CompletionItemKind::VARIABLE),
177                        ..Default::default()
178                    });
179                }
180            }
181            // Array destructuring: [$a, $b] = ... or list($a, $b) = ...
182            ExprKind::Array(elements) => {
183                for elem in elements.iter() {
184                    if let ExprKind::Variable(name) = &elem.value.kind {
185                        let label = format!("${}", name.as_str());
186                        if label != "$this" {
187                            items.push(CompletionItem {
188                                label,
189                                kind: Some(CompletionItemKind::VARIABLE),
190                                ..Default::default()
191                            });
192                        }
193                    }
194                }
195            }
196            _ => {}
197        }
198        collect_from_expression(assign.value, items);
199    }
200}
201
202const PHP_BUILTINS: &[&str] = &[
203    // string
204    "strlen",
205    "strpos",
206    "strrpos",
207    "substr",
208    "str_replace",
209    "str_contains",
210    "str_starts_with",
211    "str_ends_with",
212    "str_split",
213    "explode",
214    "implode",
215    "join",
216    "trim",
217    "ltrim",
218    "rtrim",
219    "strtolower",
220    "strtoupper",
221    "ucfirst",
222    "lcfirst",
223    "ucwords",
224    "sprintf",
225    "printf",
226    "vsprintf",
227    "number_format",
228    "nl2br",
229    "htmlspecialchars",
230    "htmlentities",
231    "strip_tags",
232    "addslashes",
233    "stripslashes",
234    "str_pad",
235    "str_repeat",
236    "str_word_count",
237    "strcmp",
238    "strcasecmp",
239    "strncmp",
240    "strncasecmp",
241    "substr_count",
242    "substr_replace",
243    "strstr",
244    "stristr",
245    "preg_match",
246    "preg_match_all",
247    "preg_replace",
248    "preg_split",
249    "preg_quote",
250    "md5",
251    "sha1",
252    "hash",
253    "base64_encode",
254    "base64_decode",
255    "urlencode",
256    "urldecode",
257    "rawurlencode",
258    "rawurldecode",
259    "http_build_query",
260    "parse_str",
261    "parse_url",
262    // array
263    "count",
264    "array_key_exists",
265    "in_array",
266    "array_search",
267    "array_merge",
268    "array_replace",
269    "array_push",
270    "array_pop",
271    "array_shift",
272    "array_unshift",
273    "array_splice",
274    "array_slice",
275    "array_chunk",
276    "array_combine",
277    "array_diff",
278    "array_intersect",
279    "array_unique",
280    "array_flip",
281    "array_reverse",
282    "array_keys",
283    "array_values",
284    "array_map",
285    "array_filter",
286    "array_reduce",
287    "array_walk",
288    "array_fill",
289    "array_fill_keys",
290    "array_pad",
291    "sort",
292    "rsort",
293    "asort",
294    "arsort",
295    "ksort",
296    "krsort",
297    "usort",
298    "uasort",
299    "uksort",
300    "compact",
301    "extract",
302    "list",
303    "range",
304    // math
305    "abs",
306    "ceil",
307    "floor",
308    "round",
309    "max",
310    "min",
311    "pow",
312    "sqrt",
313    "log",
314    "exp",
315    "rand",
316    "mt_rand",
317    "random_int",
318    "fmod",
319    "intdiv",
320    "intval",
321    "floatval",
322    "is_nan",
323    "is_infinite",
324    "is_finite",
325    "pi",
326    "sin",
327    "cos",
328    "tan",
329    "asin",
330    "acos",
331    "atan",
332    "atan2",
333    // type / var
334    "isset",
335    "empty",
336    "unset",
337    "is_null",
338    "is_bool",
339    "is_int",
340    "is_integer",
341    "is_long",
342    "is_float",
343    "is_double",
344    "is_string",
345    "is_array",
346    "is_object",
347    "is_callable",
348    "is_numeric",
349    "is_a",
350    "instanceof",
351    "gettype",
352    "settype",
353    "intval",
354    "floatval",
355    "strval",
356    "boolval",
357    "var_dump",
358    "var_export",
359    "print_r",
360    "serialize",
361    "unserialize",
362    // file / io
363    "file_get_contents",
364    "file_put_contents",
365    "file_exists",
366    "is_file",
367    "is_dir",
368    "is_readable",
369    "is_writable",
370    "mkdir",
371    "rmdir",
372    "unlink",
373    "rename",
374    "copy",
375    "realpath",
376    "dirname",
377    "basename",
378    "pathinfo",
379    "glob",
380    "scandir",
381    "opendir",
382    "readdir",
383    "closedir",
384    "fopen",
385    "fclose",
386    "fread",
387    "fwrite",
388    "fgets",
389    "fputs",
390    "feof",
391    "fseek",
392    "ftell",
393    "rewind",
394    // date / time
395    "time",
396    "microtime",
397    "mktime",
398    "strtotime",
399    "date",
400    "date_create",
401    "date_format",
402    "date_diff",
403    "date_add",
404    "date_sub",
405    "checkdate",
406    // misc
407    "defined",
408    "define",
409    "constant",
410    "class_exists",
411    "interface_exists",
412    "function_exists",
413    "method_exists",
414    "property_exists",
415    "get_class",
416    "get_parent_class",
417    "is_subclass_of",
418    "header",
419    "headers_sent",
420    "setcookie",
421    "session_start",
422    "session_destroy",
423    "ob_start",
424    "ob_get_clean",
425    "ob_end_clean",
426    "json_encode",
427    "json_decode",
428    "call_user_func",
429    "call_user_func_array",
430    "array_walk_recursive",
431    "array_map",
432    "compact",
433    "extract",
434    "sleep",
435    "usleep",
436    "exit",
437    "die",
438];
439
440pub fn builtin_completions() -> Vec<CompletionItem> {
441    let mut seen = std::collections::HashSet::new();
442    PHP_BUILTINS
443        .iter()
444        .filter(|&&f| seen.insert(f))
445        .map(|f| callable_item(f, CompletionItemKind::FUNCTION, true))
446        .collect()
447}
448
449const PHP_SUPERGLOBALS: &[&str] = &[
450    "$_SERVER",
451    "$_GET",
452    "$_POST",
453    "$_FILES",
454    "$_COOKIE",
455    "$_SESSION",
456    "$_REQUEST",
457    "$_ENV",
458    "$GLOBALS",
459];
460
461pub fn superglobal_completions() -> Vec<CompletionItem> {
462    PHP_SUPERGLOBALS
463        .iter()
464        .map(|&name| CompletionItem {
465            label: name.to_string(),
466            kind: Some(CompletionItemKind::VARIABLE),
467            detail: Some("superglobal".to_string()),
468            ..Default::default()
469        })
470        .collect()
471}