use std::collections::BTreeMap;
use serde_json::Value;
pub(super) const MAX_STRUCTURED_ITEMS: usize = 200;
pub(super) const MAX_STRUCTURED_CHARS: usize = 20_000;
const MAX_RESULT_LINES: usize = 500;
const MAX_RESULT_CHARS: usize = 30_000;
pub(super) fn render_grep_content(
output_mode: &str,
pattern: &str,
payload: &Value,
show_line_numbers: bool,
head_limited: bool,
) -> String {
let summary = &payload["summary"];
let total_matches = summary["total_matches"].as_u64().unwrap_or_default();
let files_with_matches = summary["files_with_matches"].as_u64().unwrap_or_default();
match output_mode {
"files_with_matches" => {
let files = payload["files"].as_array().cloned().unwrap_or_default();
let mut lines = vec![format!(
"Found {files_with_matches} files matching pattern {pattern:?}"
)];
if files.is_empty() {
lines.push("No matches found.".to_string());
} else {
if head_limited {
lines.push(format!("Showing first {} files.", files.len()));
}
lines.extend(
files
.into_iter()
.filter_map(|file| file.as_str().map(str::to_string)),
);
}
lines.join("\n")
}
"count" => {
let mut lines = vec![format!("Match counts for pattern {pattern:?}")];
if head_limited {
lines.push(format!(
"Showing first {} files.",
payload["file_counts"]
.as_object()
.map_or(0, |items| items.len())
));
}
if let Some(counts) = payload["file_counts"].as_object() {
for (file, count) in counts {
lines.push(format!("{}: {}", file, count.as_u64().unwrap_or_default()));
}
}
lines.push(format!(
"Total: {total_matches} matches in {files_with_matches} files"
));
lines.join("\n")
}
_ => {
let mut lines = vec![format!(
"Found {total_matches} matches in {files_with_matches} files for pattern {pattern:?}"
)];
let rows = payload["matches"].as_array().cloned().unwrap_or_default();
if rows.is_empty() {
lines.push("No matches found.".to_string());
return lines.join("\n");
}
if head_limited {
lines.push(format!("Showing first {} rows.", rows.len()));
}
let mut current_file = String::new();
for row in rows {
let row_path = row["path"].as_str().unwrap_or_default();
if current_file != row_path {
lines.push(format!("File: {row_path}"));
current_file = row_path.to_string();
}
let marker = if row["is_match"].as_bool().unwrap_or(false) {
""
} else {
"-"
};
let text = row["text"].as_str().unwrap_or_default();
if show_line_numbers {
let line = row["line"].as_u64().unwrap_or_default();
lines.push(format!(" {marker}{line}: {text}"));
} else {
lines.push(format!(" {marker}{text}"));
}
}
lines.join("\n")
}
}
}
pub(super) fn truncate_result_text(
result_text: String,
total_matches: usize,
files_with_matches: usize,
) -> (String, bool) {
let line_count = result_text.lines().count();
if line_count <= MAX_RESULT_LINES && result_text.len() <= MAX_RESULT_CHARS {
return (result_text, false);
}
let truncated = if result_text.len() > MAX_RESULT_CHARS {
let mut end = MAX_RESULT_CHARS.min(result_text.len());
while !result_text.is_char_boundary(end) {
end -= 1;
}
let candidate = &result_text[..end];
match candidate.rfind('\n') {
Some(last_newline) if last_newline > MAX_RESULT_CHARS * 4 / 5 => {
candidate[..last_newline].to_string()
}
_ => candidate.to_string(),
}
} else {
result_text
.lines()
.take(MAX_RESULT_LINES)
.collect::<Vec<_>>()
.join("\n")
};
let shown_lines = truncated.lines().count();
let truncated_info = format!(
"\n\n--- TRUNCATED ---\n\
Shown: {shown_lines} lines, {} characters\n\
Total matches: {total_matches} in {files_with_matches} files\n\
Use a narrower pattern/path/glob/type/head_limit for more focused output.",
truncated.len()
);
(format!("{truncated}{truncated_info}"), true)
}
pub(super) fn cap_file_paths(items: Vec<String>) -> (Vec<String>, bool) {
cap_structured_items(items, |path| path.len() + 4)
}
pub(super) fn cap_file_counts(
file_counts: BTreeMap<String, usize>,
) -> (BTreeMap<String, usize>, bool) {
let count_items = file_counts.into_iter().collect::<Vec<_>>();
let (capped_items, capped) = cap_structured_items(count_items, |(path, count)| {
path.len() + count.to_string().len() + 8
});
(capped_items.into_iter().collect(), capped)
}
pub(super) fn cap_match_rows(rows: Vec<Value>) -> (Vec<Value>, bool) {
cap_structured_items(rows, |row| {
row["path"].as_str().map_or(0, str::len)
+ row["line"]
.as_u64()
.map_or(0, |line| line.to_string().len())
+ row["text"].as_str().map_or(0, str::len)
+ 32
})
}
fn cap_structured_items<T>(items: Vec<T>, estimator: impl Fn(&T) -> usize) -> (Vec<T>, bool) {
let mut capped = Vec::new();
let mut used_chars = 0usize;
for item in items {
let item_size = estimator(&item).max(1);
if !capped.is_empty()
&& (capped.len() >= MAX_STRUCTURED_ITEMS
|| used_chars.saturating_add(item_size) > MAX_STRUCTURED_CHARS)
{
return (capped, true);
}
capped.push(item);
used_chars = used_chars.saturating_add(item_size);
}
(capped, false)
}