Skip to main content

php_lsp/hover/
formatting.rs

1use php_ast::{MethodDecl, Param, Visibility};
2use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
3
4use crate::document::ast::format_type_hint;
5use crate::lang::php_names::{is_php_builtin, php_doc_url};
6use crate::types::resolve::Declaration;
7
8/// Format an expression literal value.
9pub(crate) fn format_expr_literal(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
10    use php_ast::ExprKind;
11    match &expr.kind {
12        ExprKind::Int(n) => Some(n.to_string()),
13        ExprKind::Float(f) => Some(f.to_string()),
14        ExprKind::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
15        ExprKind::String(s) => Some(format!("'{}'", s)),
16        _ => None,
17    }
18}
19
20/// Format a class/interface/enum constant declaration for hover display.
21pub(crate) fn format_class_const(c: &php_ast::ClassConstDecl<'_, '_>) -> String {
22    use php_ast::ExprKind;
23    let type_str = c
24        .type_hint
25        .as_ref()
26        .map(|t| format!("{} ", format_type_hint(t)))
27        .or_else(|| match &c.value.kind {
28            ExprKind::Int(_) => Some("int ".to_string()),
29            ExprKind::String(_) => Some("string ".to_string()),
30            ExprKind::Float(_) => Some("float ".to_string()),
31            ExprKind::Bool(_) => Some("bool ".to_string()),
32            _ => None,
33        })
34        .unwrap_or_default();
35    let value_str = format_expr_literal(&c.value)
36        .map(|v| format!(" = {v}"))
37        .unwrap_or_default();
38    format!("const {}{}{}", type_str, c.name, value_str)
39}
40
41pub fn format_params_str(params: &[Param<'_, '_>]) -> String {
42    format_params(params)
43}
44
45pub(crate) fn format_params(params: &[Param<'_, '_>]) -> String {
46    params
47        .iter()
48        .map(|p| {
49            let mut s = String::new();
50            if p.by_ref {
51                s.push('&');
52            }
53            if let Some(t) = &p.type_hint {
54                s.push_str(&format!("{} ", format_type_hint(t)));
55            }
56            if p.variadic {
57                s.push_str("...");
58            }
59            s.push_str(&format!("${}", p.name));
60            if let Some(default) = &p.default {
61                s.push_str(&format!(" = {}", format_default_value(default)));
62            }
63            s
64        })
65        .collect::<Vec<_>>()
66        .join(", ")
67}
68
69/// Format a default parameter value for display in signatures.
70pub(crate) fn format_default_value(expr: &php_ast::Expr<'_, '_>) -> String {
71    use php_ast::ExprKind;
72    match &expr.kind {
73        ExprKind::Int(n) => n.to_string(),
74        ExprKind::Float(f) => f.to_string(),
75        ExprKind::String(s) => format!("'{}'", s),
76        ExprKind::Bool(b) => {
77            if *b {
78                "true".to_string()
79            } else {
80                "false".to_string()
81            }
82        }
83        ExprKind::Null => "null".to_string(),
84        ExprKind::Array(items) => {
85            if items.is_empty() {
86                "[]".to_string()
87            } else {
88                "[...]".to_string()
89            }
90        }
91        _ => "...".to_string(),
92    }
93}
94
95pub(crate) fn wrap_php(sig: &str) -> String {
96    format!("```php\n{}\n```", sig)
97}
98
99/// Format a method/function-style member signature, e.g.
100/// `public static function foo(int $x): void`.
101pub(crate) fn method_signature(m: &MethodDecl<'_, '_>) -> String {
102    let prefix = format_method_prefix(
103        m.visibility.as_ref(),
104        m.is_static,
105        m.is_abstract,
106        m.is_final,
107    );
108    let params = format_params(&m.params);
109    let ret = m
110        .return_type
111        .as_ref()
112        .map(|r| format!(": {}", format_type_hint(r)))
113        .unwrap_or_default();
114    format!("{}function {}({}){}", prefix, m.name, params, ret)
115}
116
117/// Render the hover signature for a resolved declaration. Returns `None` for
118/// kinds rendered elsewhere (properties via mir-primary path).
119pub(crate) fn declaration_signature(decl: &Declaration<'_>, word: &str) -> Option<String> {
120    let sig = match decl {
121        Declaration::Function { decl: f, .. } => {
122            let params = format_params(&f.params);
123            let ret = f
124                .return_type
125                .as_ref()
126                .map(|r| format!(": {}", format_type_hint(r)))
127                .unwrap_or_default();
128            format!("function {}({}){}", word, params, ret)
129        }
130        Declaration::Class { decl: c, .. } => {
131            let kw = if c.modifiers.is_abstract {
132                "abstract class"
133            } else if c.modifiers.is_final {
134                "final class"
135            } else if c.modifiers.is_readonly {
136                "readonly class"
137            } else {
138                "class"
139            };
140            let mut sig = format!("{} {}", kw, word);
141            if let Some(ext) = &c.extends {
142                sig.push_str(&format!(" extends {}", ext.to_string_repr()));
143            }
144            if !c.implements.is_empty() {
145                let ifaces: Vec<String> = c
146                    .implements
147                    .iter()
148                    .map(|i| i.to_string_repr().into_owned())
149                    .collect();
150                sig.push_str(&format!(" implements {}", ifaces.join(", ")));
151            }
152            sig
153        }
154        Declaration::Interface { .. } => format!("interface {}", word),
155        Declaration::Trait { .. } => format!("trait {}", word),
156        Declaration::Enum { decl: e, .. } => {
157            let mut sig = if let Some(scalar) = &e.scalar_type {
158                format!("enum {}: {}", word, scalar.to_string_repr())
159            } else {
160                format!("enum {}", word)
161            };
162            if !e.implements.is_empty() {
163                let ifaces: Vec<String> = e
164                    .implements
165                    .iter()
166                    .map(|i| i.to_string_repr().into_owned())
167                    .collect();
168                sig.push_str(&format!(" implements {}", ifaces.join(", ")));
169            }
170            sig
171        }
172        Declaration::Method { method, .. } => method_signature(method),
173        Declaration::ClassConst { konst, .. } => format_class_const(konst),
174        Declaration::EnumCase {
175            case, enum_name, ..
176        } => {
177            let value_str = case
178                .value
179                .as_ref()
180                .and_then(format_expr_literal)
181                .map(|v| format!(" = {v}"))
182                .unwrap_or_default();
183            format!("case {}::{}{}", enum_name, case.name, value_str)
184        }
185        Declaration::Property { .. } | Declaration::PromotedParam { .. } => return None,
186    };
187    Some(sig)
188}
189
190fn visibility_str(v: &Visibility) -> &'static str {
191    match v {
192        Visibility::Public => "public",
193        Visibility::Protected => "protected",
194        Visibility::Private => "private",
195    }
196}
197
198pub(crate) fn format_method_prefix(
199    visibility: Option<&Visibility>,
200    is_static: bool,
201    is_abstract: bool,
202    is_final: bool,
203) -> String {
204    let mut parts: Vec<&str> = Vec::new();
205    if let Some(v) = visibility {
206        parts.push(visibility_str(v));
207    }
208    if is_abstract {
209        parts.push("abstract");
210    }
211    if is_final {
212        parts.push("final");
213    }
214    if is_static {
215        parts.push("static");
216    }
217    if parts.is_empty() {
218        String::new()
219    } else {
220        parts.join(" ") + " "
221    }
222}
223
224pub(crate) fn format_prop_prefix(
225    visibility: Option<&Visibility>,
226    is_static: bool,
227    is_readonly: bool,
228) -> String {
229    let mut parts: Vec<&str> = Vec::new();
230    if let Some(v) = visibility {
231        parts.push(visibility_str(v));
232    }
233    if is_static {
234        parts.push("static");
235    }
236    if is_readonly {
237        parts.push("readonly");
238    }
239    if parts.is_empty() {
240        String::new()
241    } else {
242        parts.join(" ") + " "
243    }
244}
245
246/// Return a function/method signature string from a `FileIndex` slice.
247pub fn signature_for_symbol_from_index(
248    name: &str,
249    indexes: &[(
250        tower_lsp::lsp_types::Url,
251        std::sync::Arc<crate::index::file_index::FileIndex>,
252    )],
253) -> Option<String> {
254    for (_, idx) in indexes {
255        for f in &idx.functions {
256            if f.name.as_ref() == name {
257                let params_str = f
258                    .params
259                    .iter()
260                    .map(|p| {
261                        let mut s = String::new();
262                        if let Some(t) = &p.type_hint {
263                            s.push_str(&format!("{} ", t));
264                        }
265                        if p.variadic {
266                            s.push_str("...");
267                        }
268                        s.push_str(&format!("${}", p.name));
269                        s
270                    })
271                    .collect::<Vec<_>>()
272                    .join(", ");
273                let ret = f
274                    .return_type
275                    .as_deref()
276                    .map(|r| format!(": {}", r))
277                    .unwrap_or_default();
278                return Some(format!("function {}({}){}", name, params_str, ret));
279            }
280        }
281        for cls in &idx.classes {
282            for m in &cls.methods {
283                if m.name.as_ref() == name {
284                    let params_str = m
285                        .params
286                        .iter()
287                        .map(|p| {
288                            let mut s = String::new();
289                            if let Some(t) = &p.type_hint {
290                                s.push_str(&format!("{} ", t));
291                            }
292                            if p.variadic {
293                                s.push_str("...");
294                            }
295                            s.push_str(&format!("${}", p.name));
296                            s
297                        })
298                        .collect::<Vec<_>>()
299                        .join(", ");
300                    let ret = m
301                        .return_type
302                        .as_deref()
303                        .map(|r| format!(": {}", r))
304                        .unwrap_or_default();
305                    return Some(format!("function {}({}){}", name, params_str, ret));
306                }
307            }
308        }
309    }
310    None
311}
312
313/// Return hover documentation for a symbol from a `FileIndex` slice.
314pub fn docs_for_symbol_from_index(
315    name: &str,
316    indexes: &[(
317        tower_lsp::lsp_types::Url,
318        std::sync::Arc<crate::index::file_index::FileIndex>,
319    )],
320) -> Option<String> {
321    if let Some(sig) = signature_for_symbol_from_index(name, indexes) {
322        let mut value = wrap_php(&sig);
323        for (_, idx) in indexes {
324            for f in &idx.functions {
325                if f.name.as_ref() == name {
326                    if let Some(raw) = &f.docblock {
327                        let db = crate::lang::docblock::parse_docblock(raw);
328                        let md = db.to_markdown();
329                        if !md.is_empty() {
330                            value.push_str("\n\n---\n\n");
331                            value.push_str(&md);
332                        }
333                    }
334                    break;
335                }
336            }
337            for cls in &idx.classes {
338                for m in &cls.methods {
339                    if m.name.as_ref() == name {
340                        if let Some(raw) = &m.docblock {
341                            let db = crate::lang::docblock::parse_docblock(raw);
342                            let md = db.to_markdown();
343                            if !md.is_empty() {
344                                value.push_str("\n\n---\n\n");
345                                value.push_str(&md);
346                            }
347                        }
348                        break;
349                    }
350                }
351            }
352        }
353        if is_php_builtin(name) {
354            value.push_str(&format!(
355                "\n\n[php.net documentation]({})",
356                php_doc_url(name)
357            ));
358        }
359        return Some(value);
360    }
361    if is_php_builtin(name) {
362        return Some(format!(
363            "```php\nfunction {}()\n```\n\n[php.net documentation]({})",
364            name,
365            php_doc_url(name)
366        ));
367    }
368    None
369}
370
371/// Build a hover for a static method call found by class short name + method name
372/// in the workspace index. Used when the primary mir path cannot resolve a cross-file
373/// static call (e.g. `Str::camel(…)` where `Str` is only known through a `use`-import).
374pub fn method_hover_from_index(
375    class_name: &str,
376    method_name: &str,
377    indexes: &[(
378        tower_lsp::lsp_types::Url,
379        std::sync::Arc<crate::index::file_index::FileIndex>,
380    )],
381) -> Option<Hover> {
382    for (_, idx) in indexes {
383        for cls in &idx.classes {
384            if cls.name.as_ref() != class_name
385                && crate::text::fqn_short_name(cls.fqn.as_ref()) != class_name
386            {
387                continue;
388            }
389            for m in &cls.methods {
390                if m.name.as_ref() != method_name {
391                    continue;
392                }
393                let params_str = m
394                    .params
395                    .iter()
396                    .map(|p| {
397                        let mut s = String::new();
398                        if let Some(t) = &p.type_hint {
399                            s.push_str(&format!("{} ", t));
400                        }
401                        if p.variadic {
402                            s.push_str("...");
403                        }
404                        s.push_str(&format!("${}", p.name));
405                        s
406                    })
407                    .collect::<Vec<_>>()
408                    .join(", ");
409                let ret = m
410                    .return_type
411                    .as_deref()
412                    .map(|r| format!(": {}", r))
413                    .unwrap_or_default();
414                let sig = format!("{}::{}({}){}", class_name, method_name, params_str, ret);
415                let mut value = wrap_php(&sig);
416                if let Some(raw) = &m.docblock {
417                    let db = crate::lang::docblock::parse_docblock(raw);
418                    let md = db.to_markdown();
419                    if !md.is_empty() {
420                        value.push_str("\n\n---\n\n");
421                        value.push_str(&md);
422                    }
423                }
424                return Some(Hover {
425                    contents: HoverContents::Markup(MarkupContent {
426                        kind: MarkupKind::Markdown,
427                        value,
428                    }),
429                    range: None,
430                });
431            }
432        }
433    }
434    None
435}
436
437/// Build a hover for a class/interface/trait/enum found by short name in the workspace index.
438pub fn class_hover_from_index(
439    word: &str,
440    indexes: &[(
441        tower_lsp::lsp_types::Url,
442        std::sync::Arc<crate::index::file_index::FileIndex>,
443    )],
444) -> Option<Hover> {
445    use crate::index::file_index::ClassKind;
446
447    for (_, idx) in indexes {
448        for cls in &idx.classes {
449            if cls.name.as_ref() == word || cls.fqn.as_ref().trim_start_matches('\\') == word {
450                let kw = match cls.kind {
451                    ClassKind::Interface => "interface",
452                    ClassKind::Trait => "trait",
453                    ClassKind::Enum => "enum",
454                    ClassKind::Class => {
455                        if cls.is_abstract {
456                            "abstract class"
457                        } else {
458                            "class"
459                        }
460                    }
461                };
462                let mut sig = format!("{} {}", kw, &cls.name.to_string());
463                if let Some(parent) = &cls.parent {
464                    sig.push_str(&format!(" extends {}", parent));
465                }
466                if !cls.implements.is_empty() {
467                    let list: Vec<&str> = cls.implements.iter().map(|s| s.as_ref()).collect();
468                    sig.push_str(&format!(" implements {}", list.join(", ")));
469                }
470                return Some(Hover {
471                    contents: HoverContents::Markup(MarkupContent {
472                        kind: MarkupKind::Markdown,
473                        value: wrap_php(&sig),
474                    }),
475                    range: None,
476                });
477            }
478        }
479    }
480    None
481}