diffler_core/syntax/
scope.rs1use tree_sitter::{QueryCursor, StreamingIterator};
6
7use crate::syntax::registry::LanguageRegistry;
8use crate::syntax::{MAX_PARSE_BYTES, parse};
9
10#[derive(Debug, Clone, Default)]
13pub struct ScopeIndex {
14 defs: Vec<Def>,
15}
16
17#[derive(Debug, Clone)]
18struct Def {
19 start_row: usize,
20 end_row: usize,
21 name: String,
22}
23
24impl ScopeIndex {
25 pub fn is_empty(&self) -> bool {
26 self.defs.is_empty()
27 }
28
29 pub fn def_starts(&self) -> Vec<usize> {
32 let mut rows: Vec<usize> = self.defs.iter().map(|d| d.start_row).collect();
33 rows.sort_unstable();
34 rows.dedup();
35 rows
36 }
37
38 pub fn def_span(&self, name: &str) -> Option<(usize, usize)> {
42 self.defs
43 .iter()
44 .find(|def| def.name == name)
45 .map(|def| (def.start_row, def.end_row))
46 }
47
48 pub fn crumbs(&self, line: usize) -> Vec<String> {
51 let mut hits: Vec<&Def> = self
52 .defs
53 .iter()
54 .filter(|d| d.start_row <= line && line <= d.end_row)
55 .collect();
56 hits.sort_by(|a, b| {
57 a.start_row
58 .cmp(&b.start_row)
59 .then(b.end_row.cmp(&a.end_row))
60 });
61 hits.into_iter().map(|d| d.name.clone()).collect()
62 }
63}
64
65impl LanguageRegistry {
66 pub fn scope_index(&self, path: &str, content: &str) -> ScopeIndex {
70 if content.len() > MAX_PARSE_BYTES {
71 return ScopeIndex::default();
72 }
73 let Some(entry) = self.for_path(path) else {
74 return ScopeIndex::default();
75 };
76 let Some(query) = entry.tags() else {
77 return ScopeIndex::default();
78 };
79 let Some(tree) = parse(entry, content) else {
80 return ScopeIndex::default();
81 };
82
83 let names = query.capture_names();
84 let bytes = content.as_bytes();
85 let mut cursor = QueryCursor::new();
86 let mut matches = cursor.matches(query, tree.root_node(), bytes);
87 let mut defs = Vec::new();
88 while let Some(m) = matches.next() {
89 let mut span: Option<(usize, usize)> = None;
90 let mut name: Option<String> = None;
91 for cap in m.captures {
92 let cname = names.get(cap.index as usize).copied().unwrap_or("");
93 if cname.starts_with("definition.") {
94 span = Some((cap.node.start_position().row, cap.node.end_position().row));
95 } else if cname == "name" {
96 name = cap.node.utf8_text(bytes).ok().map(str::to_owned);
97 }
98 }
99 if let (Some((start_row, end_row)), Some(name)) = (span, name) {
100 defs.push(Def {
101 start_row,
102 end_row,
103 name,
104 });
105 }
106 }
107 ScopeIndex { defs }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn nested_python_scope_reads_class_then_method() {
117 let reg = LanguageRegistry::build();
118 let src = "class A:\n def method(self):\n x = 1\n return x\n";
119 let crumbs = reg.scope_index("a.py", src).crumbs(2);
120 let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
121 assert_eq!(names, ["A", "method"]);
122 }
123
124 #[test]
125 fn rust_function_scope() {
126 let reg = LanguageRegistry::build();
127 let src = "fn outer() {\n let y = 2;\n}\n";
128 let crumbs = reg.scope_index("a.rs", src).crumbs(1);
129 let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
130 assert_eq!(names, ["outer"]);
131 }
132
133 #[test]
134 fn scope_index_empty_for_unsupported_language() {
135 let reg = LanguageRegistry::build();
136 assert!(reg.scope_index("a.zzz-unknown", "whatever\n").is_empty());
137 }
138
139 #[test]
140 fn line_outside_any_definition_has_no_crumbs() {
141 let reg = LanguageRegistry::build();
142 let src = "import os\n\ndef f():\n pass\n";
143 assert!(reg.scope_index("a.py", src).crumbs(0).is_empty());
144 }
145}