use std::path::{Path, PathBuf};
use crate::token_count::estimate_content_tokens;
#[derive(Debug, Clone)]
pub enum SkeletonItem {
Function {
name: String,
signature: String,
line: usize,
},
Struct {
name: String,
fields_summary: String,
line: usize,
},
Enum {
name: String,
variants_summary: String,
line: usize,
},
Trait {
name: String,
methods: Vec<String>,
line: usize,
},
Impl {
target: String,
methods: Vec<String>,
line: usize,
},
Module {
name: String,
line: usize,
},
Const {
name: String,
type_hint: String,
line: usize,
},
Use {
path: String,
line: usize,
},
}
#[derive(Debug, Clone)]
pub struct FileSkeleton {
pub path: PathBuf,
pub items: Vec<SkeletonItem>,
pub token_count: usize,
}
impl FileSkeleton {
pub fn render(&self) -> String {
let mut out = format!("// {}\n", self.path.display());
for item in &self.items {
match item {
SkeletonItem::Function {
name: _,
signature,
line,
} => {
out.push_str(&format!("L{}: {}\n", line, signature));
}
SkeletonItem::Struct {
name,
fields_summary,
line,
} => {
out.push_str(&format!(
"L{}: struct {} {{ {} }}\n",
line, name, fields_summary
));
}
SkeletonItem::Enum {
name,
variants_summary,
line,
} => {
out.push_str(&format!(
"L{}: enum {} {{ {} }}\n",
line, name, variants_summary
));
}
SkeletonItem::Trait {
name,
methods,
line,
} => {
out.push_str(&format!(
"L{}: trait {} {{ {} }}\n",
line,
name,
methods.join("; ")
));
}
SkeletonItem::Impl {
target,
methods,
line,
} => {
out.push_str(&format!(
"L{}: impl {} {{ {} }}\n",
line,
target,
methods.join("; ")
));
}
SkeletonItem::Module { name, line } => {
out.push_str(&format!("L{}: mod {}\n", line, name));
}
SkeletonItem::Const {
name,
type_hint,
line,
} => {
out.push_str(&format!("L{}: const {}: {}\n", line, name, type_hint));
}
SkeletonItem::Use { path, line } => {
out.push_str(&format!("L{}: {}\n", line, path));
}
}
}
out
}
}
pub fn extract_rust_skeleton(path: &Path, content: &str) -> FileSkeleton {
let mut items = Vec::new();
let lines: Vec<&str> = content.lines().collect();
for (line_num_0, line) in lines.iter().enumerate() {
let line_num = line_num_0 + 1;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
continue;
}
if strip_visibility(trimmed).starts_with("use ") {
items.push(SkeletonItem::Use {
path: trimmed.trim_end_matches(';').to_string(),
line: line_num,
});
continue;
}
if strip_visibility(trimmed).starts_with("mod ")
&& (trimmed.ends_with(';') || trimmed.ends_with('{'))
{
let name = extract_name_after(trimmed, "mod ");
items.push(SkeletonItem::Module {
name,
line: line_num,
});
continue;
}
if is_fn_line(trimmed) {
let name = extract_fn_name(trimmed);
items.push(SkeletonItem::Function {
name,
signature: capture_signature(&lines, line_num_0),
line: line_num,
});
continue;
}
{
let rest = strip_visibility(trimmed);
if rest.starts_with("const ") || rest.starts_with("static ") {
let (name, type_hint) = extract_const_parts(trimmed);
items.push(SkeletonItem::Const {
name,
type_hint,
line: line_num,
});
continue;
}
}
if is_struct_line(trimmed) {
let name = extract_name_after(trimmed, "struct ");
items.push(SkeletonItem::Struct {
name,
fields_summary: "...".to_string(),
line: line_num,
});
continue;
}
if is_enum_line(trimmed) {
let name = extract_name_after(trimmed, "enum ");
items.push(SkeletonItem::Enum {
name,
variants_summary: "...".to_string(),
line: line_num,
});
continue;
}
if is_trait_line(trimmed) {
let name = extract_name_after(trimmed, "trait ");
items.push(SkeletonItem::Trait {
name,
methods: vec![],
line: line_num,
});
continue;
}
if is_impl_line(trimmed) {
let target = extract_impl_target(trimmed);
items.push(SkeletonItem::Impl {
target,
methods: vec![],
line: line_num,
});
continue;
}
}
let rendered = {
let skel = FileSkeleton {
path: path.to_path_buf(),
items: items.clone(),
token_count: 0,
};
skel.render()
};
let token_count = estimate_content_tokens(&rendered);
FileSkeleton {
path: path.to_path_buf(),
items,
token_count,
}
}
pub fn extract_symbol_source(content: &str, symbol: &str) -> Option<(usize, usize)> {
let skeleton = extract_rust_skeleton(Path::new(""), content);
let decl_line = skeleton.items.iter().find_map(|item| match item {
SkeletonItem::Function { name, line, .. } if name == symbol => Some(*line),
SkeletonItem::Struct { name, line, .. } if name == symbol => Some(*line),
SkeletonItem::Enum { name, line, .. } if name == symbol => Some(*line),
SkeletonItem::Trait { name, line, .. } if name == symbol => Some(*line),
SkeletonItem::Module { name, line } if name == symbol => Some(*line),
SkeletonItem::Const { name, line, .. } if name == symbol => Some(*line),
SkeletonItem::Impl { target, line, .. }
if target == symbol || target.split_whitespace().last() == Some(symbol) =>
{
Some(*line)
}
SkeletonItem::Use { path, line }
if path
.trim_end_matches(';')
.rsplit("::")
.next()
.is_some_and(|last| {
last.trim_matches(|c| ['{', '}', ' '].contains(&c)) == symbol
}) =>
{
Some(*line)
}
_ => None,
})?;
let lines: Vec<&str> = content.lines().collect();
let start = decl_line - 1;
let end = block_end_line(&lines, start);
Some((decl_line, end + 1))
}
fn block_end_line(lines: &[&str], start: usize) -> usize {
let mut depth = 0i32;
let mut opened = false;
for (idx, line) in lines.iter().enumerate().skip(start) {
let bytes = line.as_bytes();
let mut i = 0usize;
let mut in_str = false;
let mut escaped = false;
while i < bytes.len() {
let c = bytes[i];
if in_str {
if escaped {
escaped = false;
} else if c == b'\\' {
escaped = true;
} else if c == b'"' {
in_str = false;
}
} else if c == b'"' {
in_str = true;
} else if c == b'/' && bytes.get(i + 1) == Some(&b'/') {
break; } else if c == b'{' {
depth += 1;
opened = true;
} else if c == b'}' {
depth -= 1;
if opened && depth == 0 {
return idx;
}
} else if c == b';' && !opened {
return idx; }
i += 1;
}
}
if opened {
lines.len().saturating_sub(1)
} else {
start
}
}
fn strip_visibility(line: &str) -> &str {
if let Some(rest) = line.strip_prefix("pub(") {
match rest.find(')') {
Some(close) => rest[close + 1..].trim_start(),
None => line,
}
} else {
line.strip_prefix("pub ").unwrap_or(line)
}
}
fn paren_balance(s: &str) -> i32 {
s.chars()
.map(|c| match c {
'(' => 1,
')' => -1,
_ => 0,
})
.sum()
}
fn capture_signature(lines: &[&str], start: usize) -> String {
const MAX_SIGNATURE_LINES: usize = 8;
let mut sig = lines[start].trim().to_string();
let mut balance = paren_balance(&sig);
let mut idx = start;
while balance > 0 && idx + 1 < lines.len() && idx - start + 1 < MAX_SIGNATURE_LINES {
idx += 1;
let cont = lines[idx].trim();
sig.push(' ');
sig.push_str(cont);
balance += paren_balance(cont);
}
sig.trim_end_matches('{').trim().to_string()
}
fn is_fn_line(line: &str) -> bool {
let rest = strip_visibility(line);
rest.starts_with("fn ")
|| rest.starts_with("async fn ")
|| rest.starts_with("unsafe fn ")
|| rest.starts_with("const fn ")
}
fn extract_fn_name(line: &str) -> String {
let line = strip_visibility(line);
if let Some(fn_idx) = line.find("fn ") {
let after_fn = &line[fn_idx + 3..];
let end = after_fn
.find(|c: char| ['(', '<', ' '].contains(&c))
.unwrap_or(after_fn.len());
after_fn[..end].to_string()
} else {
"?".to_string()
}
}
fn is_struct_line(line: &str) -> bool {
strip_visibility(line).starts_with("struct ") && !line.contains("impl")
}
fn is_enum_line(line: &str) -> bool {
strip_visibility(line).starts_with("enum ")
}
fn is_trait_line(line: &str) -> bool {
strip_visibility(line).starts_with("trait ")
}
fn is_impl_line(line: &str) -> bool {
line.starts_with("impl ") || line.starts_with("impl<")
}
fn extract_name_after(line: &str, keyword: &str) -> String {
if let Some(idx) = line.find(keyword) {
let after = &line[idx + keyword.len()..];
let end = after
.find(|c: char| ['<', '{', '(', ';', ' '].contains(&c))
.unwrap_or(after.len());
after[..end].trim().to_string()
} else {
"?".to_string()
}
}
fn extract_const_parts(line: &str) -> (String, String) {
let after_const = if let Some(idx) = line.find("const ") {
&line[idx + 6..]
} else if let Some(idx) = line.find("static ") {
&line[idx + 7..]
} else {
return ("?".into(), "?".into());
};
let parts: Vec<&str> = after_const.splitn(2, ':').collect();
let name = parts
.first()
.map(|s| s.trim().to_string())
.unwrap_or_default();
let type_hint = parts
.get(1)
.map(|s| {
s.split('=')
.next()
.unwrap_or("")
.trim()
.trim_end_matches(';')
.to_string()
})
.unwrap_or_default();
(name, type_hint)
}
fn extract_impl_target(line: &str) -> String {
let after_impl = if let Some(rest) = line.strip_prefix("impl<") {
if let Some(gt_pos) = rest.find('>') {
&rest[gt_pos + 1..]
} else {
rest
}
} else if let Some(rest) = line.strip_prefix("impl ") {
rest
} else {
&line[5..]
};
after_impl
.trim_end_matches('{')
.trim_end_matches("where")
.trim()
.to_string()
}
#[cfg(test)]
#[path = "../../tests/unit/evolve/skeleton_test.rs"]
mod skeleton_test;