use serde::{Deserialize, Serialize};
use crate::query::query_where_symbol;
use crate::snapshot::Snapshot;
pub const DEFAULT_BODY_LINE_CAP: usize = 200;
const FALLBACK_WINDOW: usize = 40;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolBody {
pub symbol: String,
pub file: String,
pub start_line: usize,
pub end_line: usize,
pub language: String,
pub source: String,
pub truncated: bool,
pub total_lines: usize,
pub line_cap: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BodyResult {
pub symbol: String,
pub bodies: Vec<SymbolBody>,
}
fn language_of(path: &str) -> String {
path.rsplit('.')
.next()
.filter(|ext| *ext != path)
.map(|ext| ext.to_lowercase())
.unwrap_or_else(|| "unknown".to_string())
}
fn assignment_collection_rhs(line: &str) -> Option<usize> {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'=' {
let next = bytes.get(i + 1).copied().unwrap_or(b' ');
if next == b'=' {
i += 2;
continue;
}
let prev = if i > 0 { bytes[i - 1] } else { b' ' };
if matches!(
prev,
b'!' | b'<'
| b'>'
| b'+'
| b'-'
| b'*'
| b'/'
| b'%'
| b'&'
| b'|'
| b'^'
| b'@'
| b':'
| b'~'
| b'='
) {
return None;
}
let rhs = line[i + 1..].trim_start();
if rhs.starts_with('(') || rhs.starts_with('[') {
return Some(i + 1);
}
return None;
}
i += 1;
}
None
}
fn extract_bracket_balanced(
lines: &[&str],
start_idx: usize,
first_byte_offset: usize,
language: &str,
) -> Option<usize> {
let rust_quotes = language == "rs";
let mut depth: i32 = 0;
let mut started = false;
let mut in_string: Option<char> = None;
for (i, line) in lines.iter().enumerate().skip(start_idx) {
let chars: Vec<char> = line.chars().collect();
let mut escaped = false;
let mut idx = if i == start_idx {
line[..first_byte_offset.min(line.len())].chars().count()
} else {
0
};
while idx < chars.len() {
let ch = chars[idx];
if let Some(q) = in_string {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == q {
in_string = None;
}
idx += 1;
continue;
}
match ch {
'"' | '`' => in_string = Some(ch),
'\'' => {
if rust_quotes {
let opens_char_literal =
chars.get(idx + 1) == Some(&'\\') || chars.get(idx + 2) == Some(&'\'');
if opens_char_literal {
in_string = Some('\'');
}
} else {
in_string = Some('\'');
}
}
'#' => break,
'/' if chars.get(idx + 1) == Some(&'/') => break,
'(' | '[' | '{' => {
depth += 1;
started = true;
}
')' | ']' | '}' => {
depth -= 1;
if started && depth == 0 {
return Some(i);
}
}
_ => {}
}
idx += 1;
}
}
None
}
fn extract_body(
lines: &[&str],
start_idx: usize,
line_cap: usize,
language: &str,
) -> (usize, String, bool, usize) {
let assign_collection_end = assignment_collection_rhs(lines[start_idx])
.and_then(|off| extract_bracket_balanced(lines, start_idx, off, language));
let mut brace_open_idx: Option<usize> = None;
let lookahead_end = (start_idx + 10).min(lines.len());
'outer: for (offset, line) in lines[start_idx..lookahead_end].iter().enumerate() {
if line.contains('{') {
brace_open_idx = Some(start_idx + offset);
break 'outer;
}
}
let end_idx = if let Some(close_idx) = assign_collection_end {
close_idx
} else if let Some(open_idx) = brace_open_idx {
let rust_quotes = language == "rs";
let mut depth: i32 = 0;
let mut found_end = open_idx;
let mut in_string: Option<char> = None;
let mut closed = false;
'scan: for (i, line) in lines.iter().enumerate().skip(open_idx) {
let chars: Vec<char> = line.chars().collect();
let mut escaped = false;
let mut idx = 0;
while idx < chars.len() {
let ch = chars[idx];
if let Some(q) = in_string {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == q {
in_string = None;
}
idx += 1;
continue;
}
match ch {
'"' | '`' => in_string = Some(ch),
'\'' => {
if rust_quotes {
let opens_char_literal = chars.get(idx + 1) == Some(&'\\')
|| chars.get(idx + 2) == Some(&'\'');
if opens_char_literal {
in_string = Some('\'');
}
} else {
in_string = Some('\'');
}
}
'/' if chars.get(idx + 1) == Some(&'/') => break,
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
found_end = i;
closed = true;
break 'scan;
}
}
_ => {}
}
idx += 1;
}
}
if closed {
found_end
} else {
(start_idx + FALLBACK_WINDOW).min(lines.len().saturating_sub(1))
}
} else {
(start_idx + FALLBACK_WINDOW).min(lines.len().saturating_sub(1))
};
let total_lines = end_idx - start_idx + 1;
let capped_end_idx = (start_idx + line_cap - 1).min(end_idx);
let truncated = capped_end_idx < end_idx;
let source = lines[start_idx..=capped_end_idx].join("\n");
(capped_end_idx + 1, source, truncated, total_lines)
}
fn read_source(snapshot: &Snapshot, file: &str) -> Option<String> {
if let Ok(content) = std::fs::read_to_string(file) {
return Some(content);
}
let path = std::path::Path::new(file);
if path.is_relative() {
for root in &snapshot.metadata.roots {
if let Ok(content) = std::fs::read_to_string(std::path::Path::new(root).join(path)) {
return Some(content);
}
}
}
None
}
pub fn query_symbol_body(snapshot: &Snapshot, symbol: &str, line_cap: Option<usize>) -> BodyResult {
let cap = line_cap.unwrap_or(DEFAULT_BODY_LINE_CAP).max(1);
let where_result = query_where_symbol(snapshot, symbol);
let mut bodies = Vec::new();
let mut seen: std::collections::HashSet<(String, usize)> = std::collections::HashSet::new();
for m in &where_result.results {
let Some(line) = m.line else { continue };
if line == 0 {
continue;
}
let key = (m.file.clone(), line);
if !seen.insert(key) {
continue;
}
let Some(content) = read_source(snapshot, &m.file) else {
continue;
};
let lines: Vec<&str> = content.lines().collect();
let start_idx = line - 1;
if start_idx >= lines.len() {
continue;
}
let language = language_of(&m.file);
let (end_line, source, truncated, total_lines) =
extract_body(&lines, start_idx, cap, &language);
bodies.push(SymbolBody {
symbol: symbol.to_string(),
file: m.file.clone(),
start_line: line,
end_line,
language,
source,
truncated,
total_lines,
line_cap: cap,
});
}
BodyResult {
symbol: symbol.to_string(),
bodies,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_language_of() {
assert_eq!(language_of("src/foo.rs"), "rs");
assert_eq!(language_of("a/b/Thing.TSX"), "tsx");
assert_eq!(language_of("Makefile"), "unknown");
}
#[test]
fn test_extract_brace_balanced() {
let src =
"fn outer() {\n let x = 1;\n if x > 0 {\n return;\n }\n}\ntrailing";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, truncated, total) = extract_body(&lines, 0, 200, "rs");
assert_eq!(end_line, 6, "closing brace is on line 6");
assert!(body.contains("fn outer()"));
assert!(body.ends_with("}"));
assert!(!body.contains("trailing"));
assert!(!truncated);
assert_eq!(total, 6);
}
#[test]
fn test_extract_respects_cap() {
let src = "fn big() {\n a;\n b;\n c;\n d;\n}";
let lines: Vec<&str> = src.lines().collect();
let (_end, body, truncated, total) = extract_body(&lines, 0, 3, "rs");
assert!(truncated);
assert_eq!(total, 6);
assert_eq!(body.lines().count(), 3);
}
#[test]
fn test_extract_fallback_window_no_brace() {
let src = "def thing():\n return 1\n # more";
let lines: Vec<&str> = src.lines().collect();
let (_end, body, _truncated, _total) = extract_body(&lines, 0, 200, "py");
assert!(body.contains("def thing():"));
}
#[test]
fn test_extract_stops_at_boundary_despite_char_escape_literal() {
let src = " fn normalize(&self, raw: &str) -> String {\n raw.replace('\\\\', \"/\").to_string()\n }\n\n fn sibling(&self) {\n println!(\"sibling\");\n }";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, truncated, total) = extract_body(&lines, 0, 200, "rs");
assert_eq!(end_line, 3, "body must close at the method's own brace");
assert_eq!(total, 3);
assert!(!truncated);
assert!(!body.contains("sibling"), "must not overshoot into sibling");
}
#[test]
fn test_extract_rust_lifetime_and_label_not_string_openers() {
let src = "fn pick<'a>(&'a self, raw: &'a str) -> &'a str {\n 'outer: loop {\n break 'outer;\n }\n raw\n}\nfn after() {}";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, truncated, total) = extract_body(&lines, 0, 200, "rs");
assert_eq!(end_line, 6, "lifetimes/labels must not derail brace scan");
assert_eq!(total, 6);
assert!(!truncated);
assert!(!body.contains("fn after"));
}
#[test]
fn test_extract_ignores_braces_in_line_comments() {
let src = "fn doc() {\n // unmatched { in a comment\n let x = 1;\n}\nfn next() {}";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, _truncated, _total) = extract_body(&lines, 0, 200, "rs");
assert_eq!(end_line, 4);
assert!(!body.contains("fn next"));
}
#[test]
fn test_extract_js_single_quote_string_still_shields_braces() {
let src = "function f() {\n const s = '}';\n return s;\n}\nconst after = 1;";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, _truncated, _total) = extract_body(&lines, 0, 200, "js");
assert_eq!(end_line, 4, "JS '}}' string literal must not close the fn");
assert!(!body.contains("after"));
}
#[test]
fn test_extract_balances_assignment_collection_not_fixed_window() {
let src =
"FRAMEWORK_LAUNCHER_MARKERS = (\n \"a\",\n \"b\",\n)\n\nOTHER = 1\nmore = 2";
let lines: Vec<&str> = src.lines().collect();
let (end_line, body, truncated, total) = extract_body(&lines, 0, 200, "py");
assert_eq!(end_line, 4, "tuple closes on line 4 (the `)`)");
assert_eq!(total, 4);
assert!(!truncated);
assert!(body.contains("FRAMEWORK_LAUNCHER_MARKERS"));
assert!(body.trim_end().ends_with(')'));
assert!(!body.contains("OTHER"), "must not overshoot past the tuple");
}
#[test]
fn test_extract_def_paren_is_not_treated_as_assignment_collection() {
let src = "def thing(a, b):\n return a + b\n # trailing";
let lines: Vec<&str> = src.lines().collect();
let (_end, body, _truncated, _total) = extract_body(&lines, 0, 200, "py");
assert!(body.contains("def thing(a, b):"));
assert!(
body.contains("return a + b"),
"def body must still be captured, not just the signature"
);
}
#[test]
fn test_query_symbol_body_resolves_python_module_const() {
let tmp = tempfile::tempdir().expect("temp dir");
let source_path = tmp.path().join("markers.py");
std::fs::write(
&source_path,
"FRAMEWORK_LAUNCHER_MARKERS = (\n \"vibecrafted\",\n \"loctree\",\n)\n",
)
.expect("write source");
let mut snapshot = Snapshot::new(vec![tmp.path().to_string_lossy().to_string()]);
let mut file = crate::types::FileAnalysis::new(source_path.to_string_lossy().to_string());
file.local_symbols.push(crate::types::LocalSymbol {
name: "FRAMEWORK_LAUNCHER_MARKERS".to_string(),
kind: "const".to_string(),
line: Some(1),
context: "FRAMEWORK_LAUNCHER_MARKERS = (".to_string(),
is_exported: false,
});
snapshot.files.push(file);
let result = query_symbol_body(&snapshot, "FRAMEWORK_LAUNCHER_MARKERS", None);
assert_eq!(result.bodies.len(), 1, "module const must resolve a body");
assert!(
result.bodies[0]
.source
.contains("FRAMEWORK_LAUNCHER_MARKERS")
);
assert!(result.bodies[0].source.contains("loctree"));
assert_eq!(
result.bodies[0].end_line, 4,
"body bounded to the tuple close"
);
}
#[test]
fn test_query_symbol_body_resolves_rust_impl_method() {
let tmp = tempfile::tempdir().expect("temp dir");
let source_path = tmp.path().join("recorder.rs");
std::fs::write(
&source_path,
"struct Recorder;\n\nimpl Recorder {\n pub fn start(&self) {\n println!(\"start\");\n }\n}\n",
)
.expect("write source");
let mut snapshot = Snapshot::new(vec![tmp.path().to_string_lossy().to_string()]);
let mut file = crate::types::FileAnalysis::new(source_path.to_string_lossy().to_string());
file.impl_methods.push(crate::types::ImplMethod {
name: "start".to_string(),
qualifier: "Recorder".to_string(),
line: Some(4),
visibility: crate::types::Visibility::Public,
..Default::default()
});
snapshot.files.push(file);
let result = query_symbol_body(&snapshot, "Recorder::start", None);
assert_eq!(result.bodies.len(), 1);
assert!(result.bodies[0].source.contains("pub fn start(&self)"));
assert!(result.bodies[0].source.contains("println!"));
}
}