Skip to main content

php_lsp/hover/
formatting.rs

1use php_ast::{Param, Visibility};
2use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
3
4use crate::ast::format_type_hint;
5use crate::util::{is_php_builtin, php_doc_url};
6
7/// Format an expression literal value.
8pub(crate) fn format_expr_literal(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
9    use php_ast::ExprKind;
10    match &expr.kind {
11        ExprKind::Int(n) => Some(n.to_string()),
12        ExprKind::Float(f) => Some(f.to_string()),
13        ExprKind::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
14        ExprKind::String(s) => Some(format!("'{}'", s)),
15        _ => None,
16    }
17}
18
19/// Format a class/interface/enum constant declaration for hover display.
20pub(crate) fn format_class_const(c: &php_ast::ClassConstDecl<'_, '_>) -> String {
21    use php_ast::ExprKind;
22    let type_str = c
23        .type_hint
24        .as_ref()
25        .map(|t| format!("{} ", format_type_hint(t)))
26        .or_else(|| match &c.value.kind {
27            ExprKind::Int(_) => Some("int ".to_string()),
28            ExprKind::String(_) => Some("string ".to_string()),
29            ExprKind::Float(_) => Some("float ".to_string()),
30            ExprKind::Bool(_) => Some("bool ".to_string()),
31            _ => None,
32        })
33        .unwrap_or_default();
34    let value_str = format_expr_literal(&c.value)
35        .map(|v| format!(" = {v}"))
36        .unwrap_or_default();
37    format!("const {}{}{}", type_str, c.name, value_str)
38}
39
40pub fn format_params_str(params: &[Param<'_, '_>]) -> String {
41    format_params(params)
42}
43
44pub(crate) fn format_params(params: &[Param<'_, '_>]) -> String {
45    params
46        .iter()
47        .map(|p| {
48            let mut s = String::new();
49            if p.by_ref {
50                s.push('&');
51            }
52            if let Some(t) = &p.type_hint {
53                s.push_str(&format!("{} ", format_type_hint(t)));
54            }
55            if p.variadic {
56                s.push_str("...");
57            }
58            s.push_str(&format!("${}", p.name));
59            if let Some(default) = &p.default {
60                s.push_str(&format!(" = {}", format_default_value(default)));
61            }
62            s
63        })
64        .collect::<Vec<_>>()
65        .join(", ")
66}
67
68/// Format a default parameter value for display in signatures.
69pub(crate) fn format_default_value(expr: &php_ast::Expr<'_, '_>) -> String {
70    use php_ast::ExprKind;
71    match &expr.kind {
72        ExprKind::Int(n) => n.to_string(),
73        ExprKind::Float(f) => f.to_string(),
74        ExprKind::String(s) => format!("'{}'", s),
75        ExprKind::Bool(b) => {
76            if *b {
77                "true".to_string()
78            } else {
79                "false".to_string()
80            }
81        }
82        ExprKind::Null => "null".to_string(),
83        ExprKind::Array(items) => {
84            if items.is_empty() {
85                "[]".to_string()
86            } else {
87                "[...]".to_string()
88            }
89        }
90        _ => "...".to_string(),
91    }
92}
93
94pub(crate) fn wrap_php(sig: &str) -> String {
95    format!("```php\n{}\n```", sig)
96}
97
98fn visibility_str(v: &Visibility) -> &'static str {
99    match v {
100        Visibility::Public => "public",
101        Visibility::Protected => "protected",
102        Visibility::Private => "private",
103    }
104}
105
106pub(crate) fn format_method_prefix(
107    visibility: Option<&Visibility>,
108    is_static: bool,
109    is_abstract: bool,
110    is_final: bool,
111) -> String {
112    let mut parts: Vec<&str> = Vec::new();
113    if let Some(v) = visibility {
114        parts.push(visibility_str(v));
115    }
116    if is_abstract {
117        parts.push("abstract");
118    }
119    if is_final {
120        parts.push("final");
121    }
122    if is_static {
123        parts.push("static");
124    }
125    if parts.is_empty() {
126        String::new()
127    } else {
128        parts.join(" ") + " "
129    }
130}
131
132pub(crate) fn format_prop_prefix(
133    visibility: Option<&Visibility>,
134    is_static: bool,
135    is_readonly: bool,
136) -> String {
137    let mut parts: Vec<&str> = Vec::new();
138    if let Some(v) = visibility {
139        parts.push(visibility_str(v));
140    }
141    if is_static {
142        parts.push("static");
143    }
144    if is_readonly {
145        parts.push("readonly");
146    }
147    if parts.is_empty() {
148        String::new()
149    } else {
150        parts.join(" ") + " "
151    }
152}
153
154/// Return a function/method signature string from a `FileIndex` slice.
155pub fn signature_for_symbol_from_index(
156    name: &str,
157    indexes: &[(
158        tower_lsp::lsp_types::Url,
159        std::sync::Arc<crate::file_index::FileIndex>,
160    )],
161) -> Option<String> {
162    for (_, idx) in indexes {
163        for f in &idx.functions {
164            if f.name.as_ref() == name {
165                let params_str = f
166                    .params
167                    .iter()
168                    .map(|p| {
169                        let mut s = String::new();
170                        if let Some(t) = &p.type_hint {
171                            s.push_str(&format!("{} ", t));
172                        }
173                        if p.variadic {
174                            s.push_str("...");
175                        }
176                        s.push_str(&format!("${}", p.name));
177                        s
178                    })
179                    .collect::<Vec<_>>()
180                    .join(", ");
181                let ret = f
182                    .return_type
183                    .as_deref()
184                    .map(|r| format!(": {}", r))
185                    .unwrap_or_default();
186                return Some(format!("function {}({}){}", name, params_str, ret));
187            }
188        }
189        for cls in &idx.classes {
190            for m in &cls.methods {
191                if m.name.as_ref() == name {
192                    let params_str = m
193                        .params
194                        .iter()
195                        .map(|p| {
196                            let mut s = String::new();
197                            if let Some(t) = &p.type_hint {
198                                s.push_str(&format!("{} ", t));
199                            }
200                            if p.variadic {
201                                s.push_str("...");
202                            }
203                            s.push_str(&format!("${}", p.name));
204                            s
205                        })
206                        .collect::<Vec<_>>()
207                        .join(", ");
208                    let ret = m
209                        .return_type
210                        .as_deref()
211                        .map(|r| format!(": {}", r))
212                        .unwrap_or_default();
213                    return Some(format!("function {}({}){}", name, params_str, ret));
214                }
215            }
216        }
217    }
218    None
219}
220
221/// Return hover documentation for a symbol from a `FileIndex` slice.
222pub fn docs_for_symbol_from_index(
223    name: &str,
224    indexes: &[(
225        tower_lsp::lsp_types::Url,
226        std::sync::Arc<crate::file_index::FileIndex>,
227    )],
228) -> Option<String> {
229    if let Some(sig) = signature_for_symbol_from_index(name, indexes) {
230        let mut value = wrap_php(&sig);
231        for (_, idx) in indexes {
232            for f in &idx.functions {
233                if f.name.as_ref() == name {
234                    if let Some(raw) = &f.doc {
235                        let db = crate::docblock::parse_docblock(raw);
236                        let md = db.to_markdown();
237                        if !md.is_empty() {
238                            value.push_str("\n\n---\n\n");
239                            value.push_str(&md);
240                        }
241                    }
242                    break;
243                }
244            }
245            for cls in &idx.classes {
246                for m in &cls.methods {
247                    if m.name.as_ref() == name {
248                        if let Some(raw) = &m.doc {
249                            let db = crate::docblock::parse_docblock(raw);
250                            let md = db.to_markdown();
251                            if !md.is_empty() {
252                                value.push_str("\n\n---\n\n");
253                                value.push_str(&md);
254                            }
255                        }
256                        break;
257                    }
258                }
259            }
260        }
261        if is_php_builtin(name) {
262            value.push_str(&format!(
263                "\n\n[php.net documentation]({})",
264                php_doc_url(name)
265            ));
266        }
267        return Some(value);
268    }
269    if is_php_builtin(name) {
270        return Some(format!(
271            "```php\nfunction {}()\n```\n\n[php.net documentation]({})",
272            name,
273            php_doc_url(name)
274        ));
275    }
276    None
277}
278
279/// Build a hover for a class/interface/trait/enum found by short name in the workspace index.
280pub fn class_hover_from_index(
281    word: &str,
282    indexes: &[(
283        tower_lsp::lsp_types::Url,
284        std::sync::Arc<crate::file_index::FileIndex>,
285    )],
286) -> Option<Hover> {
287    use crate::file_index::ClassKind;
288
289    for (_, idx) in indexes {
290        for cls in &idx.classes {
291            if cls.name.as_ref() == word || cls.fqn.as_ref().trim_start_matches('\\') == word {
292                let kw = match cls.kind {
293                    ClassKind::Interface => "interface",
294                    ClassKind::Trait => "trait",
295                    ClassKind::Enum => "enum",
296                    ClassKind::Class => {
297                        if cls.is_abstract {
298                            "abstract class"
299                        } else {
300                            "class"
301                        }
302                    }
303                };
304                let mut sig = format!("{} {}", kw, &cls.name.to_string());
305                if let Some(parent) = &cls.parent {
306                    sig.push_str(&format!(" extends {}", parent));
307                }
308                if !cls.implements.is_empty() {
309                    let list: Vec<&str> = cls.implements.iter().map(|s| s.as_ref()).collect();
310                    sig.push_str(&format!(" implements {}", list.join(", ")));
311                }
312                return Some(Hover {
313                    contents: HoverContents::Markup(MarkupContent {
314                        kind: MarkupKind::Markdown,
315                        value: wrap_php(&sig),
316                    }),
317                    range: None,
318                });
319            }
320        }
321    }
322    None
323}