1use std::collections::HashSet;
4
5use tree_sitter::{Node, Query, QueryCursor, StreamingIterator, Tree};
6
7use crate::syntax::registry::{LangEntry, LanguageRegistry};
8use crate::syntax::{MAX_PARSE_BYTES, parse};
9
10#[derive(Debug, Clone, Default)]
11pub struct ScopeIndex {
12 defs: Vec<Def>,
13}
14
15#[derive(Debug, Clone)]
16struct Def {
17 start_row: usize,
18 end_row: usize,
19 name: String,
20}
21
22impl ScopeIndex {
23 pub fn is_empty(&self) -> bool {
24 self.defs.is_empty()
25 }
26
27 pub fn def_starts(&self) -> Vec<usize> {
29 let mut rows: Vec<usize> = self.defs.iter().map(|d| d.start_row).collect();
30 rows.sort_unstable();
31 rows.dedup();
32 rows
33 }
34
35 pub fn def_span(&self, name: &str) -> Option<(usize, usize)> {
37 self.defs
38 .iter()
39 .find(|def| def.name == name)
40 .map(|def| (def.start_row, def.end_row))
41 }
42
43 pub fn enclosing(&self, line: usize) -> Option<(&str, usize, usize)> {
46 self.defs
47 .iter()
48 .filter(|def| def.start_row <= line && line <= def.end_row)
49 .min_by_key(|def| def.end_row - def.start_row)
50 .map(|def| (def.name.as_str(), def.start_row, def.end_row))
51 }
52
53 pub fn crumbs(&self, line: usize) -> Vec<String> {
56 let mut hits: Vec<&Def> = self
57 .defs
58 .iter()
59 .filter(|d| d.start_row <= line && line <= d.end_row)
60 .collect();
61 hits.sort_by(|a, b| {
62 a.start_row
63 .cmp(&b.start_row)
64 .then(b.end_row.cmp(&a.end_row))
65 });
66 hits.into_iter().map(|d| d.name.clone()).collect()
67 }
68}
69
70impl LanguageRegistry {
71 pub fn scope_index(&self, path: &str, content: &str) -> ScopeIndex {
74 Self::scope_index_as(self.for_file(path, content), content)
75 }
76
77 pub fn scope_index_as(entry: Option<&LangEntry>, content: &str) -> ScopeIndex {
79 if content.len() > MAX_PARSE_BYTES {
80 return ScopeIndex::default();
81 }
82 let Some(entry) = entry else {
83 return ScopeIndex::default();
84 };
85 let Some(query) = entry.tags() else {
86 return ScopeIndex::default();
87 };
88 let Some(tree) = parse(entry, content) else {
89 return ScopeIndex::default();
90 };
91 tag_pass(query, &tree, content).0
92 }
93}
94
95pub(crate) fn tag_pass(query: &Query, tree: &Tree, content: &str) -> (ScopeIndex, HashSet<usize>) {
98 let names = query.capture_names();
99 let bytes = content.as_bytes();
100 let mut cursor = QueryCursor::new();
101 let mut matches = cursor.matches(query, tree.root_node(), bytes);
102 let mut defs = Vec::new();
103 let mut captured = HashSet::new();
104 while let Some(m) = matches.next() {
105 let mut span: Option<(usize, usize)> = None;
106 let mut name: Option<String> = None;
107 let mut qualified: Option<String> = None;
108 for cap in m.captures {
109 let cname = names.get(cap.index as usize).copied().unwrap_or("");
110 if cname.starts_with("definition.") {
111 let node = whole_definition(cap.node);
112 span = Some((node.start_position().row, node.end_position().row));
113 } else if cname == "name" {
114 captured.insert(cap.node.start_byte());
115 name = cap.node.utf8_text(bytes).ok().map(str::to_owned);
116 qualified = cap
117 .node
118 .parent()
119 .filter(|parent| parent.kind() == "qualified_identifier")
120 .and_then(|parent| parent.utf8_text(bytes).ok())
121 .map(str::to_owned);
122 }
123 }
124 if let (Some((start_row, end_row)), Some(name)) = (span, name) {
125 for name in std::iter::once(name).chain(qualified) {
126 defs.push(Def {
127 start_row,
128 end_row,
129 name,
130 });
131 }
132 }
133 }
134 (ScopeIndex { defs }, captured)
135}
136
137fn whole_definition(node: Node<'_>) -> Node<'_> {
141 let mut current = node;
142 while let Some(parent) = current.parent() {
143 match parent.kind() {
144 "function_definition" => return parent,
145 kind if kind.ends_with("declarator") => current = parent,
146 _ => break,
147 }
148 }
149 node
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn nested_python_scope_reads_class_then_method() {
158 let reg = LanguageRegistry::build();
159 let src = "class A:\n def method(self):\n x = 1\n return x\n";
160 let crumbs = reg.scope_index("a.py", src).crumbs(2);
161 let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
162 assert_eq!(names, ["A", "method"]);
163 }
164
165 #[test]
166 fn rust_function_scope() {
167 let reg = LanguageRegistry::build();
168 let src = "fn outer() {\n let y = 2;\n}\n";
169 let crumbs = reg.scope_index("a.rs", src).crumbs(1);
170 let names: Vec<&str> = crumbs.iter().map(String::as_str).collect();
171 assert_eq!(names, ["outer"]);
172 }
173
174 #[test]
175 fn scope_index_empty_for_unsupported_language() {
176 let reg = LanguageRegistry::build();
177 assert!(reg.scope_index("a.zzz-unknown", "whatever\n").is_empty());
178 }
179
180 #[test]
181 fn line_outside_any_definition_has_no_crumbs() {
182 let reg = LanguageRegistry::build();
183 let src = "import os\n\ndef f():\n pass\n";
184 assert!(reg.scope_index("a.py", src).crumbs(0).is_empty());
185 }
186
187 #[test]
188 fn every_tagged_language_spans_a_whole_function() {
189 let reg = LanguageRegistry::build();
190 let samples: &[(&str, &str, &str, (usize, usize))] = &[
191 ("a.rs", "fn f() {\n let x = 1;\n x;\n}\n", "f", (0, 3)),
192 ("a.py", "def f():\n x = 1\n return x\n", "f", (0, 2)),
193 (
194 "a.js",
195 "function f() {\n const x = 1;\n return x;\n}\n",
196 "f",
197 (0, 3),
198 ),
199 (
200 "a.ts",
201 "function f(): number {\n const x = 1;\n return x;\n}\n",
202 "f",
203 (0, 3),
204 ),
205 (
206 "a.tsx",
207 "function f() {\n const x = 1;\n return x;\n}\n",
208 "f",
209 (0, 3),
210 ),
211 (
212 "a.go",
213 "package main\n\nfunc f() int {\n\tx := 1\n\treturn x\n}\n",
214 "f",
215 (2, 5),
216 ),
217 (
218 "a.c",
219 "static int f(int a,\n int b) {\n int x = a;\n return x + b;\n}\n",
220 "f",
221 (0, 4),
222 ),
223 (
224 "a.cpp",
225 "static bool f(const int* a) {\n int x = 1;\n return x;\n}\n",
226 "f",
227 (0, 3),
228 ),
229 (
230 "A.java",
231 "class A {\n int f() {\n int x = 1;\n return x;\n }\n}\n",
232 "f",
233 (1, 4),
234 ),
235 (
236 "a.cs",
237 "class A {\n int F() {\n int x = 1;\n return x;\n }\n}\n",
238 "F",
239 (1, 4),
240 ),
241 ("a.rb", "def f\n x = 1\n x\nend\n", "f", (0, 3)),
242 (
243 "a.php",
244 "<?php\nfunction f() {\n $x = 1;\n return $x;\n}\n",
245 "f",
246 (1, 4),
247 ),
248 (
249 "a.lua",
250 "function f()\n local x = 1\n return x\nend\n",
251 "f",
252 (0, 3),
253 ),
254 (
255 "a.swift",
256 "func f() -> Int {\n let x = 1\n return x\n}\n",
257 "f",
258 (0, 3),
259 ),
260 (
261 "a.ex",
262 "defmodule A do\n def f do\n x = 1\n x\n end\nend\n",
263 "f",
264 (1, 4),
265 ),
266 (
267 "a.dart",
268 "int f() {\n var x = 1;\n return x;\n}\n",
269 "f",
270 (0, 3),
271 ),
272 ];
273 let wrong: Vec<String> = samples
274 .iter()
275 .filter_map(|(path, src, name, want)| {
276 let got = reg.scope_index(path, src).def_span(name);
277 (got != Some(*want)).then(|| format!("{path}: {got:?}, want {want:?}"))
278 })
279 .collect();
280 assert!(wrong.is_empty(), "{wrong:#?}");
281 }
282
283 #[test]
284 fn a_cpp_method_defined_outside_its_class_answers_to_its_qualified_name() {
285 let reg = LanguageRegistry::build();
286 let src = "void Cache::Link(int a) {\n int x = a;\n use(x);\n}\n";
287 let index = reg.scope_index("a.cpp", src);
288 assert_eq!(index.def_span("Cache::Link"), Some((0, 3)));
289 assert_eq!(index.def_span("Link"), Some((0, 3)));
290 }
291
292 #[test]
293 fn a_c_prototype_keeps_its_own_line() {
294 let reg = LanguageRegistry::build();
295 let src = "int f(int a);\n\nint g(void) {\n return f(1);\n}\n";
296 let index = reg.scope_index("a.c", src);
297 assert_eq!(index.def_span("g"), Some((2, 4)));
298 assert_eq!(index.def_span("f"), Some((0, 0)));
299 }
300}