use crate::hook::{cut_to, open_for_hook};
use crate::intercept::is_identifier;
use core_api::repograph::{context_with, sanitize, ContextOptions};
use core_api::Value;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::Path;
pub const MAX_CONTEXT_BYTES: usize = 800;
const MAX_SYMBOLS: usize = 5;
const MAX_CANDIDATES: usize = 40;
fn candidate_text(input: &serde_json::Value) -> Vec<String> {
let mut out = Vec::new();
if let Some(p) = input["tool_input"]["pattern"].as_str() {
out.push(p.to_string());
}
collect_strings(&input["tool_response"], &mut out);
out
}
fn collect_strings(v: &serde_json::Value, out: &mut Vec<String>) {
match v {
serde_json::Value::String(s) => out.push(s.clone()),
serde_json::Value::Array(a) => a.iter().for_each(|x| collect_strings(x, out)),
serde_json::Value::Object(o) => o.values().for_each(|x| collect_strings(x, out)),
_ => {}
}
}
fn colons_are_paired(token: &str) -> bool {
let b = token.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b':' {
if b.get(i + 1) != Some(&b':') {
return false;
}
i += 2;
} else {
i += 1;
}
}
true
}
fn candidates(texts: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for text in texts {
for token in text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == ':')) {
if !is_identifier(token) || !colons_are_paired(token) {
continue;
}
if !out.iter().any(|t| t == token) {
out.push(token.to_string());
}
if out.len() >= MAX_CANDIDATES {
return out;
}
}
}
out
}
fn name_index(db: &crate::structure::Db) -> HashMap<String, Vec<String>> {
let mut out: HashMap<String, Vec<String>> = HashMap::new();
for node in db.nodes_with_label("Symbol") {
if let Some(Value::Str(name)) = node.prop("name") {
out.entry(name).or_default().push(sanitize(node.key()));
}
}
for keys in out.values_mut() {
keys.sort();
}
out
}
fn describe(db: &crate::structure::Db, key: &str) -> String {
let report = context_with(db, None, key, &ContextOptions { source: false });
let mut out = String::new();
let _ = write!(out, "{key} — ");
match (report.file.is_empty(), report.lines) {
(false, Some((line, _))) => {
let _ = write!(out, "defined at {}:{line}", report.file);
}
(false, None) => {
let _ = write!(out, "defined in {}", report.file);
}
(true, _) => out.push_str("defined somewhere the graph did not record"),
}
let callers = report.callers.len() + report.callers_not_shown;
let _ = write!(out, ", {callers} callers");
if let Some(owner) = &report.owner {
let _ = write!(out, ", owner {owner}");
}
out
}
#[must_use]
pub fn run(db_dir: &Path, payload: &str) -> Option<String> {
let input: serde_json::Value = serde_json::from_str(payload).ok()?;
let tokens = candidates(&candidate_text(&input));
if tokens.is_empty() {
return None;
}
let db = open_for_hook(db_dir)?;
let by_name = name_index(&db);
let mut keys: Vec<&str> = Vec::new();
for token in &tokens {
if let Some([key]) = by_name.get(token).map(Vec::as_slice) {
keys.push(key);
}
if keys.len() >= MAX_SYMBOLS {
break;
}
}
if keys.is_empty() {
return None;
}
let mut out = String::from("about the symbols grep found: ");
for (i, key) in keys.iter().enumerate() {
let line = describe(&db, key);
let sep = if i > 0 { "; " } else { "" };
if out.len() + sep.len() + line.len() > MAX_CONTEXT_BYTES {
break;
}
out.push_str(sep);
out.push_str(&line);
}
if out.ends_with(": ") {
return None;
}
Some(cut_to(out, MAX_CONTEXT_BYTES))
}
#[cfg(test)]
mod tests {
use super::{candidate_text, candidates, colons_are_paired};
#[test]
fn qualified_names_survive_but_grep_line_prefixes_do_not() {
assert!(colons_are_paired("Type::method"));
assert!(colons_are_paired("render_map"));
assert!(!colons_are_paired("rs:1:fn"));
assert!(!colons_are_paired("a:::b"));
}
#[test]
fn the_pattern_comes_first_and_repeats_are_dropped() {
let payload = serde_json::json!({
"tool_input": {"pattern": "render_map"},
"tool_response": {"content": "a.rs:1:fn render_map()\nb.rs:2:render_map();"},
});
let found = candidates(&candidate_text(&payload));
assert_eq!(found.first().map(String::as_str), Some("render_map"));
assert_eq!(
found.iter().filter(|t| *t == "render_map").count(),
1,
"{found:?}"
);
assert!(
!found.iter().any(|t| t.contains(':') && !t.contains("::")),
"a `path:line:text` fragment is not a name: {found:?}"
);
assert!(!found.iter().any(|t| t == "1"), "{found:?}");
}
#[test]
fn a_payload_with_no_result_still_offers_the_pattern() {
let payload = serde_json::json!({"tool_input": {"pattern": "render_map"}});
assert_eq!(candidates(&candidate_text(&payload)), vec!["render_map"]);
assert!(candidates(&candidate_text(&serde_json::json!({}))).is_empty());
}
}