git_topology/chunking/
parser.rs1use anyhow::{Context, Result};
2use tree_sitter::Parser;
3
4use super::languages::SupportedLanguage;
5use super::CodeChunk;
6
7const BODY_NODE_TYPES: &[&str] = &[
8 "block",
9 "statement_block",
10 "compound_statement",
11 "class_body",
12 "enum_body",
13 "field_declaration_list",
14 "declaration_list",
15];
16
17const CHUNK_NODE_TYPES: &[&str] = &[
18 "function_item",
19 "function_declaration",
20 "function_definition",
21 "method_declaration",
22 "method_definition",
23 "class_declaration",
24 "class_definition",
25 "impl_item",
26 "struct_item",
27 "enum_item",
28 "trait_item",
29];
30
31pub fn extract_signature(text: &str, language: SupportedLanguage) -> String {
32 let mut parser = tree_sitter::Parser::new();
33 let ts_language = language.tree_sitter_language();
34 if parser.set_language(&ts_language).is_err() {
35 return first_line_fallback(text);
36 }
37
38 let tree = match parser.parse(text, None) {
39 Some(t) => t,
40 None => return first_line_fallback(text),
41 };
42
43 let root = tree.root_node();
44 let mut cursor = root.walk();
45
46 let top = root
47 .children(&mut cursor)
48 .find(|n| !n.is_extra() && n.child_count() > 0);
49
50 let node = match top {
51 Some(n) => n,
52 None => return first_line_fallback(text),
53 };
54
55 let mut body_start_byte: Option<usize> = None;
56 let mut node_cursor = node.walk();
57 for child in node.children(&mut node_cursor) {
58 if BODY_NODE_TYPES.contains(&child.kind()) {
59 body_start_byte = Some(child.start_byte());
60 break;
61 }
62 }
63
64 match body_start_byte {
65 Some(body_start) => {
66 let sig = text[..body_start].trim_end_matches([' ', '\t', '{', ':']);
67 sig.trim_end().to_string()
68 }
69 None => text.trim_end_matches('{').trim_end().to_string(),
70 }
71}
72
73pub fn extract_name(text: &str, language: SupportedLanguage) -> String {
74 let mut parser = tree_sitter::Parser::new();
75 let ts_language = language.tree_sitter_language();
76 if parser.set_language(&ts_language).is_err() {
77 return first_line_fallback(text);
78 }
79
80 let tree = match parser.parse(text, None) {
81 Some(t) => t,
82 None => return first_line_fallback(text),
83 };
84
85 let root = tree.root_node();
86 let mut cursor = root.walk();
87
88 let top = root
89 .children(&mut cursor)
90 .find(|n| !n.is_extra() && n.child_count() > 0);
91
92 let node = match top {
93 Some(n) => n,
94 None => return first_line_fallback(text),
95 };
96
97 let mut node_cursor = node.walk();
98 for child in node.children(&mut node_cursor) {
99 if child.kind() == "name"
100 || child.kind() == "identifier"
101 || child.kind() == "type_identifier"
102 {
103 if let Ok(name) = child.utf8_text(text.as_bytes()) {
104 return name.to_string();
105 }
106 }
107 }
108
109 first_line_fallback(text)
110}
111
112fn first_line_fallback(text: &str) -> String {
113 text.lines()
114 .next()
115 .unwrap_or("")
116 .trim_end_matches(['{', ':'])
117 .trim_end()
118 .to_string()
119}
120
121pub fn parse(src: &str, language: SupportedLanguage) -> Option<tree_sitter::Tree> {
122 let mut parser = tree_sitter::Parser::new();
123 parser.set_language(&language.tree_sitter_language()).ok()?;
124 parser.parse(src, None)
125}
126
127pub fn parse_with_tree_sitter(text: &str, language: SupportedLanguage) -> Result<Vec<CodeChunk>> {
128 let mut parser = Parser::new();
129 let ts_language = language.tree_sitter_language();
130
131 parser
132 .set_language(&ts_language)
133 .context("Failed to set tree-sitter language")?;
134
135 let tree = parser
136 .parse(text, None)
137 .context("Failed to parse code with tree-sitter")?;
138
139 let root_node = tree.root_node();
140 let mut chunks = Vec::new();
141
142 walk_tree(text, root_node, &mut chunks);
143
144 if chunks.is_empty() {
145 chunks.push(CodeChunk {
146 text: text.to_string(),
147 start_line: 0,
148 end_line: text.lines().count(),
149 });
150 } else {
151 let first_start = chunks.iter().map(|c| c.start_line).min().unwrap_or(0);
152 if first_start > 0 {
153 let preamble: String = text
154 .lines()
155 .take(first_start)
156 .collect::<Vec<_>>()
157 .join("\n");
158 if !preamble.trim().is_empty() {
159 chunks.insert(
160 0,
161 CodeChunk {
162 text: preamble,
163 start_line: 0,
164 end_line: first_start,
165 },
166 );
167 }
168 }
169 }
170
171 Ok(chunks)
172}
173
174fn walk_tree(text: &str, node: tree_sitter::Node, chunks: &mut Vec<CodeChunk>) {
175 let node_kind = node.kind();
176
177 if CHUNK_NODE_TYPES.contains(&node_kind) || is_top_level_definition(&node) {
178 let start_line = node.start_position().row;
179 let end_line = node.end_position().row;
180 let start_byte = node.start_byte();
181 let end_byte = node.end_byte();
182
183 if let Some(chunk_text) = text.get(start_byte..end_byte) {
184 chunks.push(CodeChunk {
185 text: chunk_text.to_string(),
186 start_line,
187 end_line,
188 });
189 return;
190 }
191 }
192
193 let mut cursor = node.walk();
194 for child in node.children(&mut cursor) {
195 walk_tree(text, child, chunks);
196 }
197}
198
199fn is_top_level_definition(node: &tree_sitter::Node) -> bool {
200 matches!(
201 node.kind(),
202 "function"
203 | "class"
204 | "method"
205 | "struct"
206 | "enum"
207 | "trait"
208 | "impl"
209 | "interface"
210 | "type_alias"
211 | "const_declaration"
212 )
213}