Skip to main content

php_lsp/analysis/
inline_value.rs

1use tower_lsp::lsp_types::*;
2
3/// Return `InlineValueVariableLookup` entries for every `$variable` occurrence
4/// within `range` in `source`.
5///
6/// The debug adapter uses these to look up live variable values from the
7/// runtime when execution is paused at a breakpoint.  We return every PHP
8/// variable reference visible in the viewport so the adapter can fill them all
9/// in without the language server needing debugger integration.
10pub fn inline_values_in_range(source: &str, range: Range) -> Vec<InlineValue> {
11    let mut result = Vec::new();
12
13    // First-character predicate matches PHP's `[a-zA-Z_\x80-\xff]` — extended
14    // here to any Unicode alphabetic char so multi-byte identifiers (PHP
15    // sources are usually UTF-8 in practice) are scanned correctly.
16    let is_ident_start = |c: char| c.is_alphabetic() || c == '_';
17    let is_ident_cont = |c: char| c.is_alphanumeric() || c == '_';
18
19    for (line_idx, line) in source.lines().enumerate() {
20        let line_num = line_idx as u32;
21        if line_num < range.start.line || line_num > range.end.line {
22            continue;
23        }
24        // Per the LSP spec, the request is a Range — column boundaries on
25        // the first and last line must be respected. Mid-range lines are
26        // covered in full. Columns are UTF-16 code units.
27        let line_min_col: Option<u32> =
28            (line_num == range.start.line).then_some(range.start.character);
29        let line_max_col: Option<u32> = (line_num == range.end.line).then_some(range.end.character);
30
31        // Walk per-character so columns track UTF-16 code units correctly
32        // even when the source contains multi-byte characters.
33        let chars: Vec<(u32, char)> = {
34            let mut out = Vec::with_capacity(line.len());
35            let mut col: u32 = 0;
36            for ch in line.chars() {
37                out.push((col, ch));
38                col += ch.len_utf16() as u32;
39            }
40            out
41        };
42
43        let mut i = 0usize;
44        while i < chars.len() {
45            if chars[i].1 != '$' {
46                i += 1;
47                continue;
48            }
49            // Skip `$$` (variable variables) — too dynamic to be useful.
50            if chars.get(i + 1).map(|(_, c)| *c) == Some('$') {
51                i += 2;
52                continue;
53            }
54            let dollar_col = chars[i].0;
55            i += 1;
56            // Need at least one identifier-start character after the `$`.
57            let Some(&(_, first)) = chars.get(i) else {
58                continue;
59            };
60            if !is_ident_start(first) {
61                continue;
62            }
63            let name_start_idx = i;
64            while i < chars.len() && is_ident_cont(chars[i].1) {
65                i += 1;
66            }
67            let name_end_idx = i;
68            let var_name: String = chars[name_start_idx..name_end_idx]
69                .iter()
70                .map(|(_, c)| *c)
71                .collect();
72
73            // Omit `$this` — every method has it and it adds noise without value.
74            if var_name == "this" {
75                continue;
76            }
77
78            let end_col = chars.get(name_end_idx).map(|(c, _)| *c).unwrap_or_else(|| {
79                chars
80                    .last()
81                    .map(|(c, ch)| c + ch.len_utf16() as u32)
82                    .unwrap_or(0)
83            });
84
85            // Skip occurrences that fall outside the requested range's
86            // column boundaries on the start/end lines.
87            if let Some(min) = line_min_col
88                && dollar_col < min
89            {
90                continue;
91            }
92            if let Some(max) = line_max_col
93                && end_col > max
94            {
95                continue;
96            }
97            result.push(InlineValue::VariableLookup(InlineValueVariableLookup {
98                range: Range {
99                    start: Position {
100                        line: line_num,
101                        character: dollar_col,
102                    },
103                    end: Position {
104                        line: line_num,
105                        character: end_col,
106                    },
107                },
108                // Provide the name without '$' so the DAP adapter can look it up
109                // by name in the current stack frame.
110                variable_name: Some(var_name),
111                case_sensitive_lookup: true,
112            }));
113        }
114    }
115
116    result
117}