1use std::cell::RefCell;
13use std::sync::OnceLock;
14
15use streaming_iterator::StreamingIterator as _;
16use tree_sitter::{Language, Parser, Query, QueryCursor};
17
18use crate::outline::{Sym, SymbolOutline, ROOT};
19use crate::Lang;
20
21fn grammar(lang: Lang) -> Language {
22 match lang {
23 Lang::Rust => tree_sitter_rust::LANGUAGE.into(),
24 Lang::Python => tree_sitter_python::LANGUAGE.into(),
25 Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
26 Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
27 Lang::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
28 Lang::Go => tree_sitter_go::LANGUAGE.into(),
29 }
30}
31
32fn tags_source(lang: Lang) -> String {
33 match lang {
34 Lang::Rust => tree_sitter_rust::TAGS_QUERY.to_owned(),
35 Lang::Python => tree_sitter_python::TAGS_QUERY.to_owned(),
36 Lang::TypeScript | Lang::Tsx => format!(
41 "{}\n{}",
42 tree_sitter_javascript::TAGS_QUERY,
43 tree_sitter_typescript::TAGS_QUERY
44 ),
45 Lang::JavaScript => tree_sitter_javascript::TAGS_QUERY.to_owned(),
46 Lang::Go => tree_sitter_go::TAGS_QUERY.to_owned(),
47 }
48}
49
50fn query(lang: Lang) -> Option<&'static Query> {
53 static QUERIES: [OnceLock<Option<Query>>; 6] = [const { OnceLock::new() }; 6];
54 let slot = match lang {
55 Lang::Rust => 0,
56 Lang::Python => 1,
57 Lang::TypeScript => 2,
58 Lang::Tsx => 3,
59 Lang::JavaScript => 4,
60 Lang::Go => 5,
61 };
62 QUERIES[slot]
63 .get_or_init(|| Query::new(&grammar(lang), &tags_source(lang)).ok())
64 .as_ref()
65}
66
67thread_local! {
68 static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
70}
71
72#[must_use]
77pub fn extract(text: &str, lang: Lang) -> SymbolOutline {
78 let Some(q) = query(lang) else {
79 return SymbolOutline::default();
80 };
81 PARSER.with(|p| {
82 let mut parser = p.borrow_mut();
83 if parser.set_language(&grammar(lang)).is_err() {
84 return SymbolOutline::default();
85 }
86 let Some(tree) = parser.parse(text, None) else {
87 return SymbolOutline::default();
88 };
89
90 let names = q.capture_names();
92 let mut raw: Vec<(usize, usize, u32, u32, &str, &str)> = Vec::new();
93 let mut cursor = QueryCursor::new();
95 let mut matches = cursor.matches(q, tree.root_node(), text.as_bytes());
96 while let Some(m) = matches.next() {
97 let mut def = None;
98 let mut ident = None;
99 for cap in m.captures {
100 let cap_name = names[cap.index as usize];
101 if let Some(kind) = cap_name.strip_prefix("definition.") {
102 def = Some((cap.node, kind));
103 } else if cap_name == "name" {
104 ident = Some(cap.node);
105 }
106 }
107 let (Some((node, kind)), Some(ident)) = (def, ident) else {
108 continue; };
110 let Ok(name) = ident.utf8_text(text.as_bytes()) else {
111 continue;
112 };
113 #[allow(clippy::cast_possible_truncation)] raw.push((
115 node.start_byte(),
116 node.end_byte(),
117 node.start_position().row as u32 + 1,
118 node.end_position().row as u32 + 1,
119 name,
120 kind,
121 ));
122 }
123 build_arena(raw)
124 })
126}
127
128fn build_arena(mut raw: Vec<(usize, usize, u32, u32, &str, &str)>) -> SymbolOutline {
131 raw.sort_by_key(|&(start, end, ..)| (start, usize::MAX - end));
134 raw.dedup_by_key(|&mut (start, end, _, _, name, _)| (start, end, name.to_owned()));
135
136 let mut out = SymbolOutline::default();
137 let mut stack: Vec<(usize, usize, u32)> = Vec::new(); for (start, end, start_line, end_line, name, kind) in raw {
139 while stack
140 .last()
141 .is_some_and(|&(s, e, _)| !(s <= start && end <= e))
142 {
143 stack.pop();
144 }
145 let parent = stack.last().map_or(ROOT, |&(_, _, i)| i);
146 let depth = u8::try_from(stack.len().min(u8::MAX as usize)).unwrap_or(u8::MAX);
147 let kind_id = out.kinds.iter().position(|k| k == kind).unwrap_or_else(|| {
148 out.kinds.push(kind.to_owned());
149 out.kinds.len() - 1
150 });
151 let name_off = u32::try_from(out.names.len()).unwrap_or(u32::MAX);
152 let name_len = u16::try_from(name.len().min(u16::MAX as usize)).unwrap_or(u16::MAX);
153 out.names.push_str(&name[..name_len as usize]);
154 let idx = u32::try_from(out.syms.len()).unwrap_or(u32::MAX);
155 out.syms.push(Sym {
156 name: (name_off, name_len),
157 kind: u8::try_from(kind_id.min(u8::MAX as usize)).unwrap_or(u8::MAX),
158 start_line,
159 end_line,
160 parent,
161 depth,
162 });
163 stack.push((start, end, idx));
164 }
165 out
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 const RUST_SRC: &str = r"
173/// A thing.
174pub struct Contract {
175 regions: Vec<u32>,
176}
177
178impl Contract {
179 /// Judge the bits.
180 pub fn evaluate(&self, bits: u8) -> bool {
181 bits != 0
182 }
183}
184
185fn helper() -> u8 {
186 7
187}
188";
189
190 #[test]
191 fn rust_definitions_with_nesting_and_ranges() {
192 let o = extract(RUST_SRC, Lang::Rust);
193 assert!(!o.is_empty());
194 let eval = o.find("evaluate");
195 assert_eq!(eval.len(), 1, "outline: {o:?}");
196 let (start, end, kind) = o.lines_of(eval[0]).unwrap();
197 assert!(start >= 8 && end > start, "def extent covers the body");
198 assert_eq!(kind, "method");
202 let helper = o.find("helper")[0];
203 assert_eq!(o.lines_of(helper).unwrap().2, "function");
204 assert!(!o.find("Contract").is_empty());
205 }
206
207 #[test]
208 fn python_nesting_is_containment_based() {
209 let py = extract(
210 "class A:\n def m(self):\n pass\n\ndef f():\n pass\n",
211 Lang::Python,
212 );
213 let (m, f) = (py.find("m")[0], py.find("f")[0]);
214 assert!(py.syms[m].depth > py.syms[f].depth, "{py:?}");
215 assert_eq!(py.syms[m].parent, 0, "m's parent is class A in the arena");
216 }
217
218 #[test]
219 fn python_and_typescript_and_go_extract() {
220 let py = extract(
221 "class A:\n def m(self):\n pass\n\ndef f():\n pass\n",
222 Lang::Python,
223 );
224 assert!(py.find("m").len() == 1 && py.find("f").len() == 1, "{py:?}");
225
226 let ts = extract(
227 "export class Store {\n get(k: string): string { return k; }\n}\nfunction main() {}\n",
228 Lang::TypeScript,
229 );
230 assert!(!ts.find("main").is_empty(), "{ts:?}");
231
232 let go = extract(
233 "package p\n\nfunc Handle(x int) int {\n\treturn x\n}\n",
234 Lang::Go,
235 );
236 assert!(!go.find("Handle").is_empty(), "{go:?}");
237 }
238
239 #[test]
240 fn broken_code_still_yields_what_parses() {
241 let o = extract("fn good() {}\n\nfn broken( {{{\n", Lang::Rust);
244 assert!(!o.find("good").is_empty());
245 }
246
247 #[test]
248 fn extraction_is_deterministic() {
249 let a = extract(RUST_SRC, Lang::Rust).to_wire();
250 let b = extract(RUST_SRC, Lang::Rust).to_wire();
251 assert_eq!(a, b, "same bytes, same outline — CAS dedupes for free");
252 }
253}