Skip to main content

php_lsp/analysis/
document_highlight.rs

1use tower_lsp_server::ls_types::{DocumentHighlight, DocumentHighlightKind, Position, Range};
2
3use crate::document::ast::ParsedDoc;
4use crate::lang::is_unresolvable_bareword_at;
5use crate::navigation::walk::{collect_var_refs_in_scope, refs_in_stmts};
6use crate::text::word_at_position;
7
8/// Return all ranges in the document where the word at `position` appears.
9/// For `$variables` the search is scope-aware: only occurrences within the
10/// same function/method scope are returned, preventing unrelated variables
11/// with the same name in other scopes from being highlighted.
12pub fn document_highlights(
13    source: &str,
14    doc: &ParsedDoc,
15    position: Position,
16) -> Vec<DocumentHighlight> {
17    let word = match word_at_position(source, position) {
18        Some(w) => w,
19        None => return vec![],
20    };
21
22    // A bare keyword (`final`, `public`, ...) is never a real reference —
23    // `refs_in_stmts` below matches by bare name with no keyword awareness,
24    // so a modifier token would otherwise highlight unrelated call/reference
25    // sites of a same-named method elsewhere in the file (semi-reserved
26    // words are valid method names). Also feeds `linked_editing_range`,
27    // which calls this function directly.
28    if is_unresolvable_bareword_at(source, position, &word) {
29        return vec![];
30    }
31
32    let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
33    let sv = doc.view();
34
35    if word.starts_with('$') {
36        let bare = word.trim_start_matches('$');
37        let byte_off = sv.byte_of_position(position) as usize;
38        let mut var_spans = Vec::new();
39        collect_var_refs_in_scope(&doc.program().stmts, bare, byte_off, &mut var_spans);
40        let starts: Vec<u32> = var_spans.iter().map(|(span, _)| span.start).collect();
41        let positions = sv.positions_of_offsets(&starts);
42        var_spans
43            .into_iter()
44            .zip(positions)
45            .map(|((_, kind), start)| {
46                let end = Position {
47                    line: start.line,
48                    character: start.character + word_utf16_len,
49                };
50                DocumentHighlight {
51                    range: Range { start, end },
52                    kind: Some(kind),
53                }
54            })
55            .collect()
56    } else {
57        // Use `doc.source()` (the string the AST was parsed from), not the
58        // caller's `source`. `refs_in_stmts` resolves AST name slices via
59        // `str_offset` pointer arithmetic; if the parameter `source` is a
60        // separate allocation, the arithmetic falls back to a content
61        // search that returns the *first* textual occurrence — including
62        // hits inside comments and string literals — instead of the actual
63        // AST node location.
64        let mut spans = Vec::new();
65        refs_in_stmts(doc.source(), &doc.program().stmts, &word, &mut spans);
66        let starts: Vec<u32> = spans.iter().map(|span| span.start).collect();
67        let positions = sv.positions_of_offsets(&starts);
68        spans
69            .into_iter()
70            .zip(positions)
71            .map(|(_, start)| {
72                let end = Position {
73                    line: start.line,
74                    character: start.character + word_utf16_len,
75                };
76                DocumentHighlight {
77                    range: Range { start, end },
78                    kind: Some(DocumentHighlightKind::TEXT),
79                }
80            })
81            .collect()
82    }
83}