use async_trait::async_trait;
use std::fmt::Write as _;
use super::Compressor;
use crate::types::{CompressInput, CompressOptions, CompressOutput, CompressorKind};
pub const MIN_BODY_LINES_TO_COLLAPSE: usize = 4;
const KEEP_MARKERS: &[&str] = &["TODO", "FIXME", "XXX", "error", "panic", "unsafe"];
pub struct CodeCompressor;
#[async_trait]
impl Compressor for CodeCompressor {
fn kind(&self) -> CompressorKind {
CompressorKind::Code
}
async fn compress(
&self,
input: &CompressInput<'_>,
_opts: &CompressOptions,
) -> Option<CompressOutput> {
#[cfg(feature = "tokenjuice-treesitter")]
if let Some(ext) = input.hint.extension.as_deref()
&& let Some(out) = treesitter::compress(input.content, ext)
{
return Some(out);
}
compress_heuristic(input.content)
}
}
pub fn compress_heuristic(content: &str) -> Option<CompressOutput> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 12 {
return None;
}
let mut out = String::with_capacity(content.len() / 2 + 64);
let mut depth: i32 = 0;
let mut collapsed: Vec<&str> = Vec::new();
let flush = |out: &mut String, collapsed: &mut Vec<&str>| {
if collapsed.is_empty() {
return;
}
if collapsed.len() >= MIN_BODY_LINES_TO_COLLAPSE {
let _ = writeln!(out, " {{ … {} line(s) … }}", collapsed.len());
} else {
for l in collapsed.iter() {
let _ = writeln!(out, "{l}");
}
}
collapsed.clear();
};
for line in &lines {
let (opens, closes) = brace_delta(line);
let start_depth = depth;
let force_keep = KEEP_MARKERS.iter().any(|m| line.contains(m));
if start_depth == 0 || force_keep {
flush(&mut out, &mut collapsed);
let _ = writeln!(out, "{line}");
} else {
collapsed.push(line);
}
depth += opens - closes;
if depth < 0 {
depth = 0;
}
}
flush(&mut out, &mut collapsed);
let out = out.trim_end().to_string();
if out.len() >= content.len() {
return None;
}
log::debug!(
"[tokenjuice][code] heuristic {} -> {} bytes ({} lines)",
content.len(),
out.len(),
lines.len()
);
Some(CompressOutput::lossy(out, CompressorKind::Code))
}
fn brace_delta(line: &str) -> (i32, i32) {
let mut opens = 0i32;
let mut closes = 0i32;
let mut in_str: Option<char> = None;
let mut prev = '\0';
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
match in_str {
Some(q) => {
if c == q && prev != '\\' {
in_str = None;
}
}
None => match c {
'"' | '\'' | '`' => in_str = Some(c),
'/' if chars.peek() == Some(&'/') => break, '#' => break, '{' => opens += 1,
'}' => closes += 1,
_ => {}
},
}
prev = c;
}
(opens, closes)
}
#[cfg(feature = "tokenjuice-treesitter")]
mod treesitter {
use super::{CompressOutput, CompressorKind, MIN_BODY_LINES_TO_COLLAPSE};
use tree_sitter::{Node, Parser};
fn language_for(ext: &str) -> Option<(tree_sitter::Language, bool)> {
let ext = ext.to_ascii_lowercase();
match ext.as_str() {
"rs" => Some((tree_sitter_rust::LANGUAGE.into(), true)),
"ts" | "mts" | "cts" => {
Some((tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), true))
}
"tsx" => Some((tree_sitter_typescript::LANGUAGE_TSX.into(), true)),
"js" | "jsx" | "mjs" | "cjs" => {
Some((tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), true))
}
"py" | "pyi" => Some((tree_sitter_python::LANGUAGE.into(), false)),
_ => None,
}
}
const BODY_PARENTS: &[&str] = &[
"function_item",
"function_declaration",
"function_definition",
"method_definition",
"function",
"arrow_function",
"generator_function_declaration",
];
pub fn compress(content: &str, ext: &str) -> Option<CompressOutput> {
let (language, braced) = language_for(ext)?;
let mut parser = Parser::new();
parser.set_language(&language).ok()?;
let tree = parser.parse(content, None)?;
let src = content.as_bytes();
let mut ranges: Vec<(usize, usize)> = Vec::new();
collect_bodies(tree.root_node(), src, &mut ranges);
ranges.sort_by_key(|r| r.0);
let mut merged: Vec<(usize, usize)> = Vec::new();
for r in ranges {
if let Some(last) = merged.last()
&& r.0 < last.1
{
continue; }
merged.push(r);
}
if merged.is_empty() {
return None;
}
let mut out = String::with_capacity(content.len());
let mut cursor = 0usize;
for (start, end) in merged {
if start < cursor {
continue;
}
out.push_str(&content[cursor..start]);
let body = &content[start..end];
let n_lines = body.lines().count();
if n_lines < MIN_BODY_LINES_TO_COLLAPSE {
out.push_str(body);
} else if braced {
out.push_str(&format!("{{ … {n_lines} line(s) … }}"));
} else {
out.push_str(&format!("... # {n_lines} line(s) collapsed"));
}
cursor = end;
}
out.push_str(&content[cursor..]);
let out = out.trim_end().to_string();
if out.len() >= content.len() {
return None;
}
log::debug!(
"[tokenjuice][code] tree-sitter ext={} {} -> {} bytes",
ext,
content.len(),
out.len()
);
Some(CompressOutput::lossy(out, CompressorKind::Code))
}
fn collect_bodies(node: Node, src: &[u8], out: &mut Vec<(usize, usize)>) {
if BODY_PARENTS.contains(&node.kind())
&& let Some(body) = node.child_by_field_name("body")
{
out.push((body.start_byte(), body.end_byte()));
let _ = src;
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_bodies(child, src, out);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_signatures_collapses_bodies() {
let mut src = String::from("use std::collections::HashMap;\n\n");
src.push_str("pub fn process(items: &[i32]) -> i32 {\n");
for i in 0..30 {
src.push_str(&format!(
" let tmp_{i} = items.iter().sum::<i32>() + {i};\n"
));
}
src.push_str(" tmp_0\n}\n\n");
src.push_str("struct Config {\n name: String,\n size: usize,\n}\n");
let out = compress_heuristic(&src).expect("compresses");
assert!(out.lossy);
assert!(
out.text.contains("pub fn process"),
"signature kept:\n{}",
out.text
);
assert!(out.text.contains("struct Config"));
assert!(
out.text.contains("line(s) …"),
"body collapsed:\n{}",
out.text
);
assert!(
!out.text.contains("tmp_15"),
"deep body should be collapsed"
);
assert!(out.text.len() < src.len());
}
#[test]
fn short_file_passes_through() {
let src = "fn a() {}\nfn b() {}\n";
assert!(compress_heuristic(src).is_none());
}
#[cfg(feature = "tokenjuice-treesitter")]
#[test]
fn treesitter_collapses_rust_body_keeps_struct() {
let mut src = String::from("use std::collections::HashMap;\n\n");
src.push_str("pub fn process(items: &[i32]) -> i32 {\n");
for i in 0..30 {
src.push_str(&format!(
" let tmp_{i} = items.iter().sum::<i32>() + {i};\n"
));
}
src.push_str(" tmp_0\n}\n\n");
src.push_str("pub struct Config {\n pub name: String,\n pub size: usize,\n}\n");
let out = treesitter::compress(&src, "rs").expect("compresses");
assert!(
out.text.contains("pub fn process(items: &[i32]) -> i32"),
"{}",
out.text
);
assert!(out.text.contains("pub name: String"), "{}", out.text);
assert!(out.text.contains("pub size: usize"));
assert!(out.text.contains("line(s) …"), "{}", out.text);
assert!(!out.text.contains("tmp_15"));
assert!(out.text.len() < src.len());
}
#[cfg(feature = "tokenjuice-treesitter")]
#[test]
fn treesitter_collapses_python_body() {
let mut src = String::from("import os\n\ndef handler(event):\n");
for i in 0..30 {
src.push_str(&format!(" x_{i} = compute(event, {i})\n"));
}
src.push_str(" return x_0\n");
let out = treesitter::compress(&src, "py").expect("compresses");
assert!(out.text.contains("def handler(event):"), "{}", out.text);
assert!(out.text.contains("collapsed"), "{}", out.text);
assert!(!out.text.contains("x_15"));
}
#[test]
fn keeps_marker_lines_in_body() {
let mut src = String::from("fn f() {\n");
for i in 0..20 {
if i == 10 {
src.push_str(" // TODO: handle the edge case here\n");
} else {
src.push_str(&format!(" do_thing({i});\n"));
}
}
src.push_str("}\n");
let out = compress_heuristic(&src).expect("compresses");
assert!(out.text.contains("TODO"), "marker line kept:\n{}", out.text);
}
}