Skip to main content

gram_data/
symbols.rs

1//! Definition / reference index aligned with the **current** Gram tree shape (see `grammar.js` / generated parser).
2//!
3//! `queries/locals.scm` still uses `node_pattern subject: ...` patterns; the parser emits `identifier`/`labels`
4//! directly on `node_pattern`. Until queries are updated, this module uses a small AST walk so navigation works.
5
6use tree_sitter::{Node, Tree};
7
8#[derive(Debug, Default, Clone)]
9pub struct SymbolIndex {
10    /// (name, start_byte, end_byte)
11    pub defs: Vec<(String, usize, usize)>,
12    pub refs: Vec<(String, usize, usize)>,
13}
14
15impl SymbolIndex {
16    pub fn from_tree(tree: &Tree, source: &[u8]) -> Self {
17        let mut idx = SymbolIndex::default();
18        walk(tree.root_node(), source, &mut idx);
19        idx
20    }
21
22    /// First definition span in this index for `name` (stable file order).
23    pub fn first_definition(&self, name: &str) -> Option<(usize, usize)> {
24        self.defs
25            .iter()
26            .find(|(n, _, _)| n == name)
27            .map(|(_, s, e)| (*s, *e))
28    }
29
30    pub fn all_references(&self, name: &str) -> Vec<(usize, usize)> {
31        self.refs
32            .iter()
33            .filter(|(n, _, _)| n == name)
34            .map(|(_, s, e)| (*s, *e))
35            .collect()
36    }
37
38    pub fn reference_at(&self, byte_offset: usize) -> Option<&str> {
39        self.refs
40            .iter()
41            .find(|(_, s, e)| *s <= byte_offset && byte_offset < *e)
42            .map(|(n, _, _)| n.as_str())
43    }
44
45    pub fn definition_at(&self, byte_offset: usize) -> Option<&str> {
46        self.defs
47            .iter()
48            .find(|(_, s, e)| *s <= byte_offset && byte_offset < *e)
49            .map(|(n, _, _)| n.as_str())
50    }
51}
52
53fn walk(node: Node, source: &[u8], idx: &mut SymbolIndex) {
54    match node.kind() {
55        "pattern_reference" => {
56            if let Some(id) = node.child_by_field_name("identifier") {
57                if let Ok(text) = id.utf8_text(source) {
58                    idx
59                        .refs
60                        .push((text.to_string(), id.start_byte(), id.end_byte()));
61                }
62            }
63        }
64        "node_pattern" => {
65            if let Some(id) = node.child_by_field_name("identifier") {
66                if let Ok(text) = id.utf8_text(source) {
67                    let has_labels = node.child_by_field_name("labels").is_some();
68                    let has_record = node.child_by_field_name("record").is_some();
69                    if has_labels || has_record {
70                        idx.defs
71                            .push((text.to_string(), id.start_byte(), id.end_byte()));
72                    } else if relationship_parent_field(node).is_some() {
73                        // Bare endpoints on a relationship are use sites (names bind to prior introductions).
74                        idx.refs
75                            .push((text.to_string(), id.start_byte(), id.end_byte()));
76                    }
77                }
78            }
79        }
80        "subject_pattern" => {
81            if let Some(id) = node.child_by_field_name("identifier") {
82                if let Ok(text) = id.utf8_text(source) {
83                    let has_labels = node.child_by_field_name("labels").is_some();
84                    let has_record = node.child_by_field_name("record").is_some();
85                    if has_labels || has_record {
86                        idx.defs
87                            .push((text.to_string(), id.start_byte(), id.end_byte()));
88                    }
89                }
90            }
91        }
92        _ => {}
93    }
94    let mut c = node.walk();
95    for child in node.children(&mut c) {
96        walk(child, source, idx);
97    }
98}
99
100/// If `node` is a direct `left` / `right` child of a `relationship_pattern`, return that field name.
101fn relationship_parent_field(node: Node) -> Option<&'static str> {
102    let parent = node.parent()?;
103    if parent.kind() != "relationship_pattern" {
104        return None;
105    }
106    for field in ["left", "right"] {
107        if let Some(ch) = parent.child_by_field_name(field) {
108            if ch.id() == node.id() {
109                return Some(field);
110            }
111        }
112    }
113    None
114}
115