use anyhow::Result;
use probe_code::models::SearchResult;
use probe_code::search::search_tokens::count_tokens;
use serde::Serialize;
use std::fmt::Write as FmtWrite;
use std::path::Path;
fn format_extraction_internal(
results: &[SearchResult],
format: &str,
original_input: Option<&str>,
system_prompt: Option<&str>,
user_instructions: Option<&str>,
is_dry_run: bool,
) -> Result<String> {
let mut output = String::new();
match format {
"json" => {
if is_dry_run {
#[derive(Serialize)]
struct JsonDryRunResult<'a> {
file: &'a str,
#[serde(serialize_with = "serialize_lines_as_array")]
lines: (usize, usize),
node_type: &'a str,
}
fn serialize_lines_as_array<S>(
lines: &(usize, usize),
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&lines.0)?;
seq.serialize_element(&lines.1)?;
seq.end()
}
let json_results: Vec<JsonDryRunResult> = results
.iter()
.map(|r| JsonDryRunResult {
file: &r.file,
lines: r.lines,
node_type: &r.node_type,
})
.collect();
let mut wrapper = serde_json::json!({
"results": json_results,
"summary": {
"count": results.len(),
}
});
if let Some(prompt) = system_prompt {
wrapper["system_prompt"] = serde_json::Value::String(prompt.to_string());
}
if let Some(instructions) = user_instructions {
wrapper["user_instructions"] =
serde_json::Value::String(instructions.to_string());
}
if let Some(input) = original_input {
wrapper["original_input"] = serde_json::Value::String(input.to_string());
}
write!(output, "{}", serde_json::to_string_pretty(&wrapper)?)?;
} else {
#[derive(Serialize)]
struct JsonResult<'a> {
file: &'a str,
#[serde(serialize_with = "serialize_lines_as_array")]
lines: (usize, usize),
node_type: &'a str,
code: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
original_input: Option<&'a str>,
}
fn serialize_lines_as_array<S>(
lines: &(usize, usize),
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(&lines.0)?;
seq.serialize_element(&lines.1)?;
seq.end()
}
let json_results: Vec<JsonResult> = results
.iter()
.map(|r| JsonResult {
file: &r.file,
lines: r.lines,
node_type: &r.node_type,
code: &r.code,
original_input: None,
})
.collect();
let mut wrapper = serde_json::json!({
"results": json_results,
"summary": {
"count": results.len(),
"total_bytes": results.iter().map(|r| r.code.len()).sum::<usize>(),
"total_tokens": results.iter().map(|r| count_tokens(&r.code)).sum::<usize>(),
}
});
if let Some(input) = original_input {
wrapper["original_input"] = serde_json::Value::String(input.to_string());
}
if let Some(prompt) = system_prompt {
wrapper["system_prompt"] = serde_json::Value::String(prompt.to_string());
}
if let Some(instructions) = user_instructions {
wrapper["user_instructions"] =
serde_json::Value::String(instructions.to_string());
}
write!(output, "{}", serde_json::to_string_pretty(&wrapper)?)?;
}
}
"xml" => {
writeln!(output, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
writeln!(output, "<probe_results>")?;
if is_dry_run {
for result in results {
writeln!(output, " <result>")?;
writeln!(output, " <file>{}</file>", escape_xml(&result.file))?;
if result.node_type != "file" {
writeln!(output, " <lines>")?;
writeln!(output, " <start>{}</start>", result.lines.0)?;
writeln!(output, " <end>{}</end>", result.lines.1)?;
writeln!(output, " </lines>")?;
}
if result.node_type != "file" && result.node_type != "context" {
writeln!(
output,
" <node_type>{}</node_type>",
escape_xml(&result.node_type)
)?;
}
writeln!(output, " </result>")?;
}
writeln!(output, " <summary>")?;
writeln!(output, " <count>{}</count>", results.len())?;
writeln!(output, " </summary>")?;
} else {
for result in results {
writeln!(output, " <result>")?;
writeln!(output, " <file>{}</file>", escape_xml(&result.file))?;
if result.node_type != "file" {
writeln!(output, " <lines>")?;
writeln!(output, " <start>{}</start>", result.lines.0)?;
writeln!(output, " <end>{}</end>", result.lines.1)?;
writeln!(output, " </lines>")?;
}
if result.node_type != "file" && result.node_type != "context" {
writeln!(output, " <node_type>{}</node_type>", &result.node_type)?;
}
writeln!(output, " <code><![CDATA[{}]]></code>", &result.code)?;
writeln!(output, " </result>")?;
}
writeln!(output, " <summary>")?;
writeln!(output, " <count>{}</count>", results.len())?;
writeln!(
output,
" <total_bytes>{}</total_bytes>",
results.iter().map(|r| r.code.len()).sum::<usize>()
)?;
writeln!(
output,
" <total_tokens>{}</total_tokens>",
results.iter().map(|r| count_tokens(&r.code)).sum::<usize>()
)?;
writeln!(output, " </summary>")?;
}
if let Some(input) = original_input {
writeln!(
output,
" <original_input><![CDATA[{input}]]></original_input>"
)?;
}
if let Some(prompt) = system_prompt {
writeln!(
output,
" <system_prompt><![CDATA[{prompt}]]></system_prompt>"
)?;
}
if let Some(instructions) = user_instructions {
writeln!(
output,
" <user_instructions><![CDATA[{instructions}]]></user_instructions>"
)?;
}
writeln!(output, "</probe_results>")?;
}
_ => {
use colored::*;
if results.is_empty() {
writeln!(output, "{}", "No results found.".yellow().bold())?;
} else {
for result in results {
if format == "markdown" {
writeln!(output, "## File: {}", result.file.yellow())?;
} else {
writeln!(output, "File: {}", result.file.yellow())?;
}
if result.node_type != "file" {
if format == "markdown" {
writeln!(output, "### Lines: {}-{}", result.lines.0, result.lines.1)?;
} else {
writeln!(output, "Lines: {}-{}", result.lines.0, result.lines.1)?;
}
}
if result.node_type != "file" && result.node_type != "context" {
if format == "markdown" {
writeln!(output, "### Type: {}", result.node_type.cyan())?;
} else {
writeln!(output, "Type: {}", result.node_type.cyan())?;
}
}
if !is_dry_run {
let extension = Path::new(&result.file)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
let language = get_language_from_extension(extension);
match format {
"markdown" => {
if !language.is_empty() {
writeln!(output, "```{language}")?;
} else {
writeln!(output, "```")?;
}
writeln!(output, "{}", result.code)?;
writeln!(output, "```")?;
}
"plain" => {
writeln!(output)?;
writeln!(output, "{}", result.code)?;
writeln!(output)?;
writeln!(output, "----------------------------------------")?;
writeln!(output)?;
}
"color" => {
if !language.is_empty() {
writeln!(output, "```{language}")?;
} else {
writeln!(output, "```")?;
}
writeln!(output, "{}", result.code)?;
writeln!(output, "```")?;
}
_ => {
if !language.is_empty() {
writeln!(output, "```{language}")?;
} else {
writeln!(output, "```")?;
}
writeln!(output, "{}", result.code)?;
writeln!(output, "```")?;
}
}
}
writeln!(output)?;
}
}
if let Some(input) = original_input {
writeln!(output, "{}", "Original Input:".yellow().bold())?;
writeln!(output, "{input}")?;
}
if let Some(prompt) = system_prompt {
writeln!(output)?;
writeln!(output, "{}", "System Prompt:".yellow().bold())?;
writeln!(output, "{prompt}")?;
}
if let Some(instructions) = user_instructions {
writeln!(output)?;
writeln!(output, "{}", "User Instructions:".yellow().bold())?;
writeln!(output, "{instructions}")?;
}
if !["json", "xml"].contains(&format) && !results.is_empty() {
writeln!(output)?;
if is_dry_run {
writeln!(
output,
"{} {} {}",
"Would extract".green().bold(),
results.len(),
if results.len() == 1 {
"result"
} else {
"results"
}
)?;
} else {
writeln!(
output,
"{} {} {}",
"Extracted".green().bold(),
results.len(),
if results.len() == 1 {
"result"
} else {
"results"
}
)?;
let total_bytes: usize = results.iter().map(|r| r.code.len()).sum();
let total_tokens: usize = results.iter().map(|r| count_tokens(&r.code)).sum();
writeln!(output, "Total bytes returned: {total_bytes}")?;
writeln!(output, "Total tokens returned: {total_tokens}")?;
}
}
}
}
Ok(output)
}
pub fn format_extraction_dry_run(
results: &[SearchResult],
format: &str,
original_input: Option<&str>,
system_prompt: Option<&str>,
user_instructions: Option<&str>,
) -> Result<String> {
format_extraction_internal(
results,
format,
original_input,
system_prompt,
user_instructions,
true, )
}
pub fn format_extraction_results(
results: &[SearchResult],
format: &str,
original_input: Option<&str>,
system_prompt: Option<&str>,
user_instructions: Option<&str>,
) -> Result<String> {
format_extraction_internal(
results,
format,
original_input,
system_prompt,
user_instructions,
false, )
}
#[allow(dead_code)]
pub fn format_and_print_extraction_results(
results: &[SearchResult],
format: &str,
original_input: Option<&str>,
system_prompt: Option<&str>,
user_instructions: Option<&str>,
) -> Result<()> {
let output = format_extraction_results(
results,
format,
original_input,
system_prompt,
user_instructions,
)?;
println!("{output}");
Ok(())
}
fn escape_xml(s: &str) -> String {
s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
pub fn get_language_from_extension(extension: &str) -> &'static str {
match extension {
"rs" => "rust",
"py" => "python",
"js" => "javascript",
"ts" => "typescript",
"go" => "go",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" => "cpp",
"java" => "java",
"rb" => "ruby",
"php" => "php",
"sh" => "bash",
"md" => "markdown",
"json" => "json",
"yaml" | "yml" => "yaml",
"html" => "html",
"css" => "css",
"sql" => "sql",
"kt" | "kts" => "kotlin",
"swift" => "swift",
"scala" => "scala",
"dart" => "dart",
"ex" | "exs" => "elixir",
"hs" => "haskell",
"clj" => "clojure",
"lua" => "lua",
"r" => "r",
"pl" | "pm" => "perl",
"proto" => "protobuf",
_ => "",
}
}