1#[cfg(feature = "tree-sitter")]
10use tree_sitter::{Node, Parser, Query, QueryCursor, StreamingIterator};
11
12use super::bm25_index::{ChunkKind, CodeChunk};
13
14#[cfg(feature = "tree-sitter")]
15const CHUNK_QUERY_RUST: &str = r"
16(function_item name: (identifier) @name) @chunk
17(struct_item name: (type_identifier) @name) @chunk
18(enum_item name: (type_identifier) @name) @chunk
19(trait_item name: (type_identifier) @name) @chunk
20(impl_item type: (type_identifier) @name) @chunk
21(const_item name: (identifier) @name) @chunk
22";
23
24#[cfg(feature = "tree-sitter")]
25const CHUNK_QUERY_TYPESCRIPT: &str = r"
26(function_declaration name: (identifier) @name) @chunk
27(class_declaration name: (type_identifier) @name) @chunk
28(abstract_class_declaration name: (type_identifier) @name) @chunk
29(interface_declaration name: (type_identifier) @name) @chunk
30(type_alias_declaration name: (type_identifier) @name) @chunk
31(method_definition name: (property_identifier) @name) @chunk
32(variable_declarator name: (identifier) @name value: (arrow_function)) @chunk
33";
34
35#[cfg(feature = "tree-sitter")]
36const CHUNK_QUERY_JAVASCRIPT: &str = r"
37(function_declaration name: (identifier) @name) @chunk
38(class_declaration name: (identifier) @name) @chunk
39(method_definition name: (property_identifier) @name) @chunk
40(variable_declarator name: (identifier) @name value: (arrow_function)) @chunk
41";
42
43#[cfg(feature = "tree-sitter")]
44const CHUNK_QUERY_PYTHON: &str = r"
45(function_definition name: (identifier) @name) @chunk
46(class_definition name: (identifier) @name) @chunk
47";
48
49#[cfg(feature = "tree-sitter")]
50const CHUNK_QUERY_GO: &str = r"
51(function_declaration name: (identifier) @name) @chunk
52(method_declaration name: (field_identifier) @name) @chunk
53(type_spec name: (type_identifier) @name) @chunk
54";
55
56#[cfg(feature = "tree-sitter")]
57const CHUNK_QUERY_JAVA: &str = r"
58(method_declaration name: (identifier) @name) @chunk
59(class_declaration name: (identifier) @name) @chunk
60(interface_declaration name: (identifier) @name) @chunk
61(enum_declaration name: (identifier) @name) @chunk
62(constructor_declaration name: (identifier) @name) @chunk
63";
64
65#[cfg(feature = "tree-sitter")]
66const CHUNK_QUERY_C: &str = r"
67(function_definition
68 declarator: (function_declarator
69 declarator: (identifier) @name)) @chunk
70(struct_specifier name: (type_identifier) @name) @chunk
71(enum_specifier name: (type_identifier) @name) @chunk
72";
73
74#[cfg(feature = "tree-sitter")]
75const CHUNK_QUERY_CPP: &str = r"
76(function_definition
77 declarator: (function_declarator
78 declarator: (_) @name)) @chunk
79(struct_specifier name: (type_identifier) @name) @chunk
80(class_specifier name: (type_identifier) @name) @chunk
81(enum_specifier name: (type_identifier) @name) @chunk
82(namespace_definition name: (identifier) @name) @chunk
83";
84
85#[cfg(feature = "tree-sitter")]
90fn get_cached_query(file_ext: &str) -> Option<&'static Query> {
91 use std::collections::HashMap;
92 use std::sync::OnceLock;
93
94 static QUERY_CACHE: OnceLock<HashMap<&'static str, Query>> = OnceLock::new();
95
96 let cache = QUERY_CACHE.get_or_init(|| {
97 let mut map = HashMap::new();
98 let exts: &[&str] = &[
99 "rs", "ts", "tsx", "js", "jsx", "py", "go", "java", "c", "h", "cpp", "cc", "cxx", "hpp",
100 ];
101 for &ext in exts {
102 if let (Some(lang), Some(src)) = (get_language(ext), get_chunk_query(ext))
103 && let Ok(q) = Query::new(&lang, src)
104 {
105 map.insert(ext, q);
106 }
107 }
108 map
109 });
110
111 cache.get(file_ext)
112}
113
114#[cfg(feature = "tree-sitter")]
119pub(crate) fn for_each_chunk_node(
120 content: &str,
121 file_ext: &str,
122 mut visitor: impl FnMut(Node, &str, ChunkKind, usize, usize),
123) -> Option<()> {
124 for_each_chunk_node_while(
125 content,
126 file_ext,
127 || true,
128 |node, name, kind, start, end| {
129 visitor(node, name, kind, start, end);
130 true
131 },
132 )
133}
134
135#[cfg(feature = "tree-sitter")]
137pub(crate) fn for_each_chunk_node_while(
138 content: &str,
139 file_ext: &str,
140 mut continue_scan: impl FnMut() -> bool,
141 mut visitor: impl FnMut(Node, &str, ChunkKind, usize, usize) -> bool,
142) -> Option<()> {
143 let language = get_language(file_ext)?;
144
145 thread_local! {
146 static PARSER: std::cell::RefCell<Parser> = std::cell::RefCell::new(Parser::new());
147 }
148
149 let tree = PARSER.with(|p| {
150 let mut parser = p.borrow_mut();
151 let _ = parser.set_language(&language);
152 parser.parse(content, None)
153 })?;
154
155 let query = get_cached_query(file_ext)?;
156 let chunk_idx = find_capture_index(query, "chunk")?;
157 let name_idx = find_capture_index(query, "name")?;
158
159 let source = content.as_bytes();
160 let mut cursor = QueryCursor::new();
161 let mut matches = cursor.matches(query, tree.root_node(), source);
162 let mut seen_ranges = Vec::new();
163
164 while let Some(m) = matches.next() {
165 if !continue_scan() {
167 break;
168 }
169 let mut chunk_node: Option<Node> = None;
170 let mut name_text = String::new();
171
172 for cap in m.captures {
173 if cap.index == chunk_idx {
174 chunk_node = Some(cap.node);
175 } else if cap.index == name_idx
176 && let Ok(text) = cap.node.utf8_text(source)
177 {
178 name_text = text.to_string();
179 }
180 }
181
182 if let Some(node) = chunk_node {
183 if name_text.is_empty() {
184 continue;
185 }
186
187 let start_row0 = node.start_position().row;
188 let end_row0 = node.end_position().row;
189
190 let range = (start_row0, end_row0);
191 if seen_ranges
192 .iter()
193 .any(|&(s, e)| s <= start_row0 && end_row0 <= e && range != (s, e))
194 {
195 continue;
196 }
197 seen_ranges.push(range);
198
199 let kind = node_kind_to_chunk_kind(node.kind());
200 if !visitor(node, name_text.as_str(), kind, start_row0 + 1, end_row0 + 1) {
201 break;
202 }
203 }
204 }
205
206 Some(())
207}
208
209#[cfg(feature = "tree-sitter")]
210pub fn extract_chunks_ts(file_path: &str, content: &str, file_ext: &str) -> Option<Vec<CodeChunk>> {
211 let mut chunks = Vec::new();
212
213 for_each_chunk_node(
214 content,
215 file_ext,
216 |node, name_text, kind, start_line, end_line| {
217 let block = match node.utf8_text(content.as_bytes()) {
218 Ok(text) => text.to_owned(),
219 Err(_) => return,
220 };
221 let token_count = super::bm25_index::tokenize_for_index(&block).len();
222
223 chunks.push(CodeChunk {
224 input_origin: None,
225 file_path: file_path.to_string(),
226 symbol_name: name_text.to_string(),
227 kind,
228 start_line,
229 end_line,
230 content: block,
231 tokens: Vec::new(),
232 token_count,
233 });
234 },
235 )?;
236
237 if chunks.is_empty() {
238 return None;
239 }
240
241 chunks.sort_by_key(|c| c.start_line);
242 Some(chunks)
243}
244
245#[cfg(not(feature = "tree-sitter"))]
246pub fn extract_chunks_ts(
247 _file_path: &str,
248 _content: &str,
249 _file_ext: &str,
250) -> Option<Vec<CodeChunk>> {
251 None
252}
253
254#[cfg(feature = "tree-sitter")]
255use lean_ctx_syntax::chunk_grammar::chunk_language as get_language;
256
257#[cfg(feature = "tree-sitter")]
258fn get_chunk_query(ext: &str) -> Option<&'static str> {
259 Some(match ext {
260 "rs" => CHUNK_QUERY_RUST,
261 "ts" | "tsx" => CHUNK_QUERY_TYPESCRIPT,
262 "js" | "jsx" => CHUNK_QUERY_JAVASCRIPT,
263 "py" => CHUNK_QUERY_PYTHON,
264 "go" => CHUNK_QUERY_GO,
265 "java" => CHUNK_QUERY_JAVA,
266 "c" | "h" => CHUNK_QUERY_C,
267 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => CHUNK_QUERY_CPP,
268 _ => return None,
269 })
270}
271
272#[cfg(feature = "tree-sitter")]
273fn find_capture_index(query: &Query, name: &str) -> Option<u32> {
274 query
275 .capture_names()
276 .iter()
277 .position(|n| *n == name)
278 .map(|i| i as u32)
279}
280
281fn node_kind_to_chunk_kind(kind: &str) -> ChunkKind {
282 match kind {
283 "function_item"
284 | "function_declaration"
285 | "function_definition"
286 | "method_declaration"
287 | "method_definition"
288 | "constructor_declaration"
289 | "variable_declarator" => ChunkKind::Function,
290
291 "struct_item"
292 | "struct_specifier"
293 | "struct_declaration"
294 | "enum_item"
295 | "enum_specifier"
296 | "enum_declaration"
297 | "trait_item"
298 | "interface_declaration"
299 | "type_alias_declaration"
300 | "type_spec" => ChunkKind::Struct,
301
302 "impl_item" => ChunkKind::Impl,
303
304 "class_declaration"
305 | "abstract_class_declaration"
306 | "class_specifier"
307 | "class_definition" => ChunkKind::Class,
308
309 "namespace_definition" | "namespace_declaration" => ChunkKind::Module,
310
311 _ => ChunkKind::Other,
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn extract_rust_chunks() {
321 let src = r#"use std::io;
322
323pub fn process(input: &str) -> String {
324 input.to_uppercase()
325}
326
327pub struct Config {
328 pub name: String,
329 pub port: u16,
330}
331
332impl Config {
333 pub fn new() -> Self {
334 Self { name: "default".into(), port: 8080 }
335 }
336}
337
338fn helper() -> bool {
339 true
340}
341"#;
342 let chunks = extract_chunks_ts("main.rs", src, "rs").unwrap();
343 assert!(
344 chunks.len() >= 4,
345 "expected >=4 chunks, got {}",
346 chunks.len()
347 );
348
349 let names: Vec<&str> = chunks.iter().map(|c| c.symbol_name.as_str()).collect();
350 assert!(names.contains(&"process"), "got {names:?}");
351 assert!(names.contains(&"Config"), "got {names:?}");
352 assert!(names.contains(&"helper"), "got {names:?}");
353
354 let process = chunks.iter().find(|c| c.symbol_name == "process").unwrap();
355 assert!(matches!(process.kind, ChunkKind::Function));
356 assert!(process.content.contains("to_uppercase"));
357 }
358
359 #[test]
360 fn extract_typescript_chunks() {
361 let src = r"
362export function greet(name: string): string {
363 return `Hello ${name}`;
364}
365
366export class UserService {
367 findUser(id: number): User {
368 return db.find(id);
369 }
370}
371
372const handler = async (req: Request): Promise<Response> => {
373 return new Response();
374};
375";
376 let chunks = extract_chunks_ts("app.ts", src, "ts").unwrap();
377 assert!(
378 chunks.len() >= 3,
379 "expected >=3 chunks, got {}",
380 chunks.len()
381 );
382
383 let names: Vec<&str> = chunks.iter().map(|c| c.symbol_name.as_str()).collect();
384 assert!(names.contains(&"greet"), "got {names:?}");
385 assert!(names.contains(&"UserService"), "got {names:?}");
386 }
387
388 #[test]
389 fn extract_python_chunks() {
390 let src = r"
391class AuthService:
392 def __init__(self, db):
393 self.db = db
394
395 def authenticate(self, email: str) -> bool:
396 user = self.db.find(email)
397 return user is not None
398
399def create_app():
400 return Flask(__name__)
401";
402 let chunks = extract_chunks_ts("app.py", src, "py").unwrap();
403 assert!(
404 chunks.len() >= 2,
405 "expected >=2 chunks, got {}",
406 chunks.len()
407 );
408
409 let names: Vec<&str> = chunks.iter().map(|c| c.symbol_name.as_str()).collect();
410 assert!(names.contains(&"AuthService"), "got {names:?}");
411 assert!(names.contains(&"create_app"), "got {names:?}");
412
413 let auth = chunks
414 .iter()
415 .find(|c| c.symbol_name == "AuthService")
416 .unwrap();
417 assert!(auth.content.contains("authenticate"));
418 }
419
420 #[test]
421 fn chunks_contain_full_body() {
422 let src = r#"
423pub fn complex(x: i32, y: i32) -> Result<String, Error> {
424 let sum = x + y;
425 let result = format!("Sum: {}", sum);
426 if sum > 100 {
427 return Err(Error::new("too large"));
428 }
429 Ok(result)
430}
431"#;
432 let chunks = extract_chunks_ts("math.rs", src, "rs").unwrap();
433 let complex = chunks.iter().find(|c| c.symbol_name == "complex").unwrap();
434 assert!(complex.content.contains("sum > 100"));
435 assert!(complex.content.contains("Ok(result)"));
436 }
437
438 #[test]
439 fn unsupported_language_returns_none() {
440 assert!(extract_chunks_ts("file.xyz", "content", "xyz").is_none());
441 }
442
443 #[test]
444 fn empty_file_returns_none() {
445 assert!(extract_chunks_ts("empty.rs", "", "rs").is_none());
446 }
447
448 #[test]
449 fn javascript_same_line_functions_use_exact_node_content() {
450 let src = "function alpha(){return 1;}function beta(){return 2;}";
451 let chunks = extract_chunks_ts("min.js", src, "js").unwrap();
452
453 let alpha = chunks.iter().find(|c| c.symbol_name == "alpha").unwrap();
454 let beta = chunks.iter().find(|c| c.symbol_name == "beta").unwrap();
455
456 assert_eq!(alpha.content, "function alpha(){return 1;}");
457 assert_eq!(beta.content, "function beta(){return 2;}");
458 }
459
460 #[test]
461 fn chunks_sorted_by_line() {
462 let src = r"
463fn b_func() {}
464fn a_func() {}
465";
466 let chunks = extract_chunks_ts("sort.rs", src, "rs").unwrap();
467 assert!(chunks[0].start_line <= chunks[1].start_line);
468 }
469}