mod config_parser;
mod generic;
mod markdown;
mod tree_sitter_strategy;
use std::sync::Arc;
use once_cell::sync::Lazy;
use crate::config::parsers::PARSERS;
use crate::config::tokenization::TOKENIZATION;
use crate::types::Fragment;
pub trait FragmentationStrategy: Send + Sync {
fn can_handle(&self, path: &str, content: &str) -> bool;
fn fragment(&self, path: Arc<str>, content: &str) -> Vec<Fragment>;
}
static STRATEGIES: Lazy<Vec<Box<dyn FragmentationStrategy>>> = Lazy::new(|| {
vec![
Box::new(tree_sitter_strategy::TreeSitterStrategy::new()),
Box::new(markdown::MarkdownStrategy),
Box::new(config_parser::ConfigStrategy),
Box::new(generic::GenericStrategy),
]
});
pub fn fragment_file(path: Arc<str>, content: &str) -> Vec<Fragment> {
for strategy in STRATEGIES.iter() {
if strategy.can_handle(&path, content) {
let fragments = strategy.fragment(Arc::clone(&path), content);
if !fragments.is_empty() {
return fragments;
}
}
}
Vec::new()
}
fn create_snippet(lines: &[&str], start_line: u32, end_line: u32) -> Option<String> {
if start_line == 0 || end_line == 0 || start_line > end_line {
return None;
}
let start_idx = (start_line - 1) as usize;
let end_idx = end_line as usize;
if start_idx >= lines.len() || end_idx > lines.len() {
return None;
}
let mut snippet = lines[start_idx..end_idx].join("\n");
if snippet.trim().is_empty() {
return None;
}
if !snippet.ends_with('\n') {
snippet.push('\n');
}
Some(snippet)
}
fn build_covered_set(covered: &[(u32, u32)]) -> rustc_hash::FxHashSet<u32> {
let mut result = rustc_hash::FxHashSet::default();
for &(start, end) in covered {
for ln in start..=end {
result.insert(ln);
}
}
result
}
fn trim_blank_lines(lines: &[&str], mut start: u32, mut end: u32) -> (u32, u32) {
while start <= end
&& lines
.get((start - 1) as usize)
.map_or(true, |l| l.trim().is_empty())
{
start += 1;
}
while end >= start
&& lines
.get((end - 1) as usize)
.map_or(true, |l| l.trim().is_empty())
{
end -= 1;
}
(start, end)
}
fn split_long_gap(lines: &[&str], start: u32, end: u32) -> Vec<(u32, u32)> {
let threshold = PARSERS.sub_fragment_threshold_lines;
let target = PARSERS.sub_fragment_target_lines.max(1);
if end < start || end - start + 1 <= threshold {
return vec![(start, end)];
}
let is_blank = |ln: u32| {
lines
.get((ln - 1) as usize)
.is_some_and(|l| l.trim().is_empty())
};
let mut out = Vec::new();
let mut chunk_start = start;
while chunk_start <= end {
let ideal_end = (chunk_start + target - 1).min(end);
let slack = (target / 2).max(1);
let mut chunk_end = ideal_end;
if ideal_end < end {
let lo = ideal_end.saturating_sub(slack).max(chunk_start);
let hi = (ideal_end + slack).min(end);
if let Some(blank) = (lo..=hi).rev().find(|&ln| is_blank(ln)) {
chunk_end = blank;
}
}
if end - chunk_end < PARSERS.min_fragment_lines {
chunk_end = end;
}
out.push((chunk_start, chunk_end));
chunk_start = chunk_end + 1;
}
out
}
fn create_code_gap_fragments(
path: Arc<str>,
lines: &[&str],
covered: &[(u32, u32)],
) -> Vec<Fragment> {
if lines.is_empty() {
return Vec::new();
}
let covered_set = build_covered_set(covered);
let total = lines.len() as u32;
let uncovered: Vec<u32> = (1..=total).filter(|ln| !covered_set.contains(ln)).collect();
if uncovered.is_empty() {
return Vec::new();
}
let mut gaps: Vec<(u32, u32)> = Vec::new();
let mut gap_start = uncovered[0];
let mut gap_end = uncovered[0];
for &ln in &uncovered[1..] {
if ln == gap_end + 1 {
gap_end = ln;
} else {
gaps.push((gap_start, gap_end));
gap_start = ln;
gap_end = ln;
}
}
gaps.push((gap_start, gap_end));
let mut fragments = Vec::new();
for (start, end) in gaps
.into_iter()
.flat_map(|(s, e)| split_long_gap(lines, s, e))
{
let (start, end) = trim_blank_lines(lines, start, end);
if start > end || end - start + 1 < PARSERS.min_fragment_lines {
continue;
}
if let Some(snippet) = create_snippet(lines, start, end) {
let identifiers = crate::types::extract_identifiers(
&snippet,
TOKENIZATION.fragment_min_identifier_length,
);
fragments.push(Fragment {
id: crate::types::FragmentId::new(Arc::clone(&path), start, end),
kind: crate::types::FragmentKind::Chunk,
content: Arc::from(snippet),
identifiers,
token_count: 0,
symbol_name: None,
});
}
}
fragments
}
#[cfg(test)]
mod gap_tests {
use super::*;
fn numbered(n: usize) -> Vec<String> {
(1..=n).map(|i| format!("line {i}")).collect()
}
fn refs(v: &[String]) -> Vec<&str> {
v.iter().map(String::as_str).collect()
}
#[test]
fn a_short_gap_is_left_whole() {
let owned = numbered(PARSERS.sub_fragment_threshold_lines as usize);
let lines = refs(&owned);
assert_eq!(
split_long_gap(&lines, 1, lines.len() as u32),
vec![(1, lines.len() as u32)]
);
}
#[test]
fn a_long_gap_is_split_into_bounded_chunks_covering_every_line() {
let owned = numbered(300);
let lines = refs(&owned);
let chunks = split_long_gap(&lines, 1, 300);
assert!(chunks.len() > 1, "a 300-line gap was left as one fragment");
for &(start, end) in &chunks {
assert!(start <= end, "inverted chunk {start}-{end}");
assert!(
end - start + 1 <= PARSERS.sub_fragment_threshold_lines * 2,
"chunk {start}-{end} is far past the target size"
);
}
assert_eq!(chunks.first().unwrap().0, 1);
assert_eq!(chunks.last().unwrap().1, 300);
for pair in chunks.windows(2) {
assert_eq!(pair[1].0, pair[0].1 + 1, "gap or overlap at {pair:?}");
}
}
#[test]
fn a_cut_prefers_a_blank_line_near_the_target() {
let target = PARSERS.sub_fragment_target_lines as usize;
let mut owned = numbered(200);
let blank_at = target - 2;
owned[blank_at - 1] = String::new();
let lines = refs(&owned);
let chunks = split_long_gap(&lines, 1, 200);
assert_eq!(
chunks[0].1, blank_at as u32,
"the first cut ignored a blank line within reach: {chunks:?}"
);
}
#[test]
fn a_tiny_remainder_is_folded_into_the_previous_chunk() {
let target = PARSERS.sub_fragment_target_lines;
let total = target * 2 + PARSERS.min_fragment_lines.saturating_sub(1);
let owned = numbered(total as usize);
let lines = refs(&owned);
let chunks = split_long_gap(&lines, 1, total);
assert!(
chunks
.iter()
.all(|&(s, e)| e - s + 1 >= PARSERS.min_fragment_lines),
"a chunk below the minimum size survived: {chunks:?}"
);
}
}