use gitcortex_core::graph::DefinitionText;
use tree_sitter::Node as TsNode;
const MAX_BODY_BYTES: usize = 2 * 1024;
pub(crate) fn capture(source: &[u8], ts_node: TsNode<'_>) -> DefinitionText {
let start = ts_node.start_byte();
let end = ts_node.end_byte();
let raw = source.get(start..end).unwrap_or(&[]);
let body_full = std::str::from_utf8(raw).unwrap_or("");
let body = truncate_to_char_boundary(body_full, MAX_BODY_BYTES).to_owned();
let signature = extract_signature(&body).to_owned();
let doc_comment = preceding_doc_comment(source, ts_node)
.or_else(|| inline_docstring(&body));
DefinitionText {
signature,
body,
doc_comment,
start_byte: start as u32,
end_byte: end as u32,
}
}
fn extract_signature(body: &str) -> &str {
let mut consumed: usize = 0;
for line in body.split_inclusive('\n') {
let trimmed = line.trim_end();
consumed += line.len();
if trimmed.ends_with('{')
|| trimmed.ends_with(':')
|| trimmed.ends_with("=>")
|| trimmed.ends_with('=')
{
let sig = body[..consumed].trim_end();
let sig = sig.strip_suffix('{').unwrap_or(sig).trim_end();
return sig;
}
}
body.lines().next().unwrap_or("").trim_end()
}
fn truncate_to_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while !s.is_char_boundary(end) && end > 0 {
end -= 1;
}
&s[..end]
}
fn preceding_doc_comment(source: &[u8], ts_node: TsNode<'_>) -> Option<String> {
let parent = ts_node.parent()?;
let mut cursor = parent.walk();
let siblings: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
let pos = siblings.iter().position(|n| n.id() == ts_node.id())?;
if pos == 0 {
return None;
}
let mut comments: Vec<&str> = Vec::new();
for sib in siblings[..pos].iter().rev() {
let kind = sib.kind();
if kind == "line_comment" || kind == "block_comment" || kind == "comment" {
let text = sib.utf8_text(source).unwrap_or("");
if is_doc_style(text) {
comments.push(text);
} else {
break;
}
} else {
break;
}
}
if comments.is_empty() {
return None;
}
comments.reverse();
Some(comments.join("\n"))
}
fn inline_docstring(body: &str) -> Option<String> {
let mut after_sig = body;
for (idx, line) in body.split_inclusive('\n').enumerate() {
if line.trim_end().ends_with(':') {
let consumed: usize = body.split_inclusive('\n').take(idx + 1).map(str::len).sum();
after_sig = &body[consumed..];
break;
}
}
let trimmed = after_sig.trim_start();
for marker in ["\"\"\"", "'''"] {
if let Some(rest) = trimmed.strip_prefix(marker) {
if let Some(end) = rest.find(marker) {
let inner = rest[..end].trim();
if !inner.is_empty() {
return Some(inner.to_owned());
}
}
}
}
None
}
fn is_doc_style(text: &str) -> bool {
let t = text.trim_start();
t.starts_with("///")
|| t.starts_with("//!")
|| t.starts_with("/**")
|| t.starts_with("\"\"\"")
|| t.starts_with("'''")
|| t.starts_with("//")
|| t.starts_with("#")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_stops_at_brace() {
assert_eq!(
extract_signature("fn foo(a: u32) -> u32 {\n a + 1\n}"),
"fn foo(a: u32) -> u32"
);
}
#[test]
fn signature_python_def() {
assert_eq!(
extract_signature("def greet(name):\n return f'hi {name}'"),
"def greet(name):"
);
}
#[test]
fn signature_falls_back_to_first_line() {
assert_eq!(extract_signature("const X = 42;"), "const X = 42;");
}
#[test]
fn python_docstring_extracted() {
let body = "def greet(name):\n \"\"\"Return a greeting.\"\"\"\n return name\n";
assert_eq!(
inline_docstring(body).as_deref(),
Some("Return a greeting.")
);
}
#[test]
fn python_docstring_single_quotes() {
let body = "def greet(name):\n '''hi'''\n";
assert_eq!(inline_docstring(body).as_deref(), Some("hi"));
}
#[test]
fn no_docstring_returns_none() {
let body = "def greet(name):\n return name\n";
assert!(inline_docstring(body).is_none());
}
#[test]
fn truncate_respects_char_boundary() {
let s = "héllo"; let out = truncate_to_char_boundary(s, 2);
assert!(s.starts_with(out));
assert!(out.is_char_boundary(out.len()));
}
}