use std::path::Path;
use super::Fragment;
use super::coalesce_fragments;
use super::extract::line_of;
const MAX_TRAVERSAL_DEPTH: usize = 200;
pub(crate) fn chunk_semantic<'a>(
code: &'a str,
path: &'a Path,
max_bytes_per_chunk: usize,
language: &tree_sitter::Language,
) -> anyhow::Result<Vec<Fragment<'a>>> {
let fragments = {
let mut parser = tree_sitter::Parser::new();
parser.set_language(language)?;
let tree = parser
.parse(code, None )
.ok_or_else(|| anyhow::anyhow!("Failed to parse code"))?;
let mut cursor = tree.walk();
let nodes = split_node(
tree.root_node(),
code,
max_bytes_per_chunk,
path,
&mut cursor,
0, )?;
coalesce_fragments(nodes.into_iter(), code, max_bytes_per_chunk)
};
Ok(fragments)
}
fn split_node<'a, 'b>(
node: tree_sitter::Node<'b>,
code: &'a str,
max_bytes_per_chunk: usize,
path: &'a Path,
cursor: &mut tree_sitter::TreeCursor<'b>,
depth: usize,
) -> anyhow::Result<Vec<Fragment<'a>>> {
if depth > MAX_TRAVERSAL_DEPTH {
return Err(anyhow::anyhow!(
"Maximum traversal depth {} exceeded, falling back to naive chunking",
MAX_TRAVERSAL_DEPTH
));
}
if max_bytes_per_chunk == 0 {
let node_start = node.start_byte();
let node_end = node.end_byte();
return Ok(vec![Fragment {
content: &code[node_start..node_end],
start_line: node.start_position().row,
end_line: node.end_position().row,
start_byte_index: node_start,
end_byte_index: node_end,
file_path: path,
}]);
}
let mut current_fragment = Fragment::from_node_start(node, path);
let mut fragments = vec![];
let children: Vec<_> = node.children(cursor).collect();
for child in children {
let child_size = child.end_byte().saturating_sub(child.start_byte());
if child_size > max_bytes_per_chunk {
let mut new_fragment = Fragment::from_node_end(child, path);
std::mem::swap(&mut current_fragment, &mut new_fragment);
fragments.push(new_fragment);
if child.named_child_count() == 0 {
fragments.extend(split_leaf_by_bytes(
code,
path,
max_bytes_per_chunk,
child.start_byte(),
child.end_byte(),
));
} else {
fragments.append(&mut split_node(
child,
code,
max_bytes_per_chunk,
path,
cursor,
depth + 1,
)?);
}
} else if child_size + current_fragment.size() > max_bytes_per_chunk {
fragments.push(current_fragment);
current_fragment = Fragment::from_node_start(child, path);
current_fragment.append(&Fragment::from_node_end(child, path), code);
} else {
current_fragment.end_line = child.end_position().row;
current_fragment.end_byte_index = child.end_byte();
current_fragment.content = &code[current_fragment.start_byte_index..child.end_byte()];
}
}
fragments.push(current_fragment);
Ok(fragments)
}
fn split_leaf_by_bytes<'a>(
code: &'a str,
path: &'a Path,
max_bytes_per_chunk: usize,
start_byte: usize,
end_byte: usize,
) -> Vec<Fragment<'a>> {
let mut fragments = Vec::new();
let mut current_start = start_byte;
while current_start < end_byte {
let remaining_bytes = end_byte - current_start;
let chunk_size = std::cmp::min(remaining_bytes, max_bytes_per_chunk);
let mut chunk_end = current_start + chunk_size;
while chunk_end > current_start && !code.is_char_boundary(chunk_end) {
chunk_end -= 1;
}
if chunk_end <= current_start {
chunk_end = current_start + chunk_size;
while chunk_end < end_byte && !code.is_char_boundary(chunk_end) {
chunk_end += 1;
}
}
let start_line = line_of(code.as_bytes(), current_start);
let end_line = line_of(code.as_bytes(), chunk_end.saturating_sub(1));
fragments.push(Fragment {
content: &code[current_start..chunk_end],
start_line,
end_line,
start_byte_index: current_start,
end_byte_index: chunk_end,
file_path: path,
});
current_start = chunk_end;
}
fragments
}
impl<'a> Fragment<'a> {
fn from_node_start(node: tree_sitter::Node<'_>, path: &'a Path) -> Self {
Fragment {
content: "",
start_line: node.start_position().row,
end_line: node.start_position().row,
start_byte_index: node.start_byte(),
end_byte_index: node.start_byte(),
file_path: path,
}
}
fn from_node_end(node: tree_sitter::Node<'_>, path: &'a Path) -> Self {
Fragment {
content: "",
start_line: node.end_position().row,
end_line: node.end_position().row,
start_byte_index: node.end_byte(),
end_byte_index: node.end_byte(),
file_path: path,
}
}
}
pub(crate) fn chunk_naive<'a>(
code: &'a str,
path: &'a Path,
max_bytes_per_chunk: usize,
num_lines_per_chunk: usize,
) -> Vec<Fragment<'a>> {
let lines = line_spans(code);
let chunks = lines.chunks(num_lines_per_chunk);
chunks
.into_iter()
.flat_map(|chunk| {
let (start_line, start_range) = chunk[0];
let (end_line, end_range) =
chunk.last().expect("Chunks must have at least one element");
if max_bytes_per_chunk != 0 && (end_range.1 - start_range.0) > max_bytes_per_chunk {
let chunked_fragments = chunk.iter().flat_map(|(line, line_span)| {
chunk_line_by_bytes(code, path, max_bytes_per_chunk, *line, *line_span)
});
return coalesce_fragments(chunked_fragments, code, max_bytes_per_chunk);
}
vec![Fragment {
content: &code[start_range.0..end_range.1],
start_line,
end_line: *end_line,
file_path: path,
start_byte_index: start_range.0,
end_byte_index: end_range.1,
}]
})
.collect()
}
fn line_spans(code: &str) -> Vec<(usize, (usize, usize))> {
let bytes = code.as_bytes();
let mut spans = Vec::new();
let mut line_start = 0;
let mut line_number = 0;
for (index, &byte) in bytes.iter().enumerate() {
if byte == b'\n' {
spans.push((line_number, (line_start, index)));
line_number += 1;
line_start = index + 1;
}
}
if line_start < bytes.len() {
spans.push((line_number, (line_start, bytes.len())));
}
spans
}
fn chunk_line_by_bytes<'a>(
code: &'a str,
path: &'a Path,
max_bytes_per_chunk: usize,
line_number: usize,
line_span: (usize, usize),
) -> Vec<Fragment<'a>> {
let (line_start, line_end) = line_span;
let line_content = &code[line_start..line_end];
let line_length = line_end - line_start;
if max_bytes_per_chunk == 0 {
return vec![Fragment {
content: line_content,
start_line: line_number,
end_line: line_number,
file_path: path,
start_byte_index: line_start,
end_byte_index: line_end,
}];
}
if line_length <= max_bytes_per_chunk {
return vec![Fragment {
content: line_content,
start_line: line_number,
end_line: line_number,
file_path: path,
start_byte_index: line_start,
end_byte_index: line_end,
}];
}
let mut fragments = Vec::new();
let mut current_start = line_start;
while current_start < line_end {
let remaining_bytes = line_end - current_start;
let chunk_size = std::cmp::min(remaining_bytes, max_bytes_per_chunk);
let mut chunk_end = current_start + chunk_size;
while chunk_end > current_start && !code.is_char_boundary(chunk_end) {
chunk_end -= 1;
}
if chunk_end <= current_start {
chunk_end = current_start + chunk_size;
while chunk_end < line_end && !code.is_char_boundary(chunk_end) {
chunk_end += 1;
}
}
fragments.push(Fragment {
content: &code[current_start..chunk_end],
start_line: line_number,
end_line: line_number,
file_path: path,
start_byte_index: current_start,
end_byte_index: chunk_end,
});
current_start = chunk_end;
}
fragments
}