Skip to main content

php_lsp/analysis/
document_highlight.rs

1use tower_lsp::lsp_types::{DocumentHighlight, DocumentHighlightKind, Position, Range};
2
3use crate::ast::ParsedDoc;
4use crate::util::word_at_position;
5use crate::walk::{collect_var_refs_in_scope, refs_in_stmts};
6
7/// Return all ranges in the document where the word at `position` appears.
8/// For `$variables` the search is scope-aware: only occurrences within the
9/// same function/method scope are returned, preventing unrelated variables
10/// with the same name in other scopes from being highlighted.
11pub fn document_highlights(
12    source: &str,
13    doc: &ParsedDoc,
14    position: Position,
15) -> Vec<DocumentHighlight> {
16    let word = match word_at_position(source, position) {
17        Some(w) => w,
18        None => return vec![],
19    };
20
21    let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
22    let sv = doc.view();
23
24    if word.starts_with('$') {
25        let bare = word.trim_start_matches('$');
26        let byte_off = sv.byte_of_position(position) as usize;
27        let mut var_spans = Vec::new();
28        collect_var_refs_in_scope(&doc.program().stmts, bare, byte_off, &mut var_spans);
29        var_spans
30            .into_iter()
31            .map(|(span, kind)| {
32                let start = sv.position_of(span.start);
33                let end = Position {
34                    line: start.line,
35                    character: start.character + word_utf16_len,
36                };
37                DocumentHighlight {
38                    range: Range { start, end },
39                    kind: Some(kind),
40                }
41            })
42            .collect()
43    } else {
44        // Use `doc.source()` (the string the AST was parsed from), not the
45        // caller's `source`. `refs_in_stmts` resolves AST name slices via
46        // `str_offset` pointer arithmetic; if the parameter `source` is a
47        // separate allocation, the arithmetic falls back to a content
48        // search that returns the *first* textual occurrence — including
49        // hits inside comments and string literals — instead of the actual
50        // AST node location.
51        let mut spans = Vec::new();
52        refs_in_stmts(doc.source(), &doc.program().stmts, &word, &mut spans);
53        spans
54            .into_iter()
55            .map(|span| {
56                let start = sv.position_of(span.start);
57                let end = Position {
58                    line: start.line,
59                    character: start.character + word_utf16_len,
60                };
61                DocumentHighlight {
62                    range: Range { start, end },
63                    kind: Some(DocumentHighlightKind::TEXT),
64                }
65            })
66            .collect()
67    }
68}