use crate::tui::app::state::AppState;
const PASTE_SUMMARIZE_LINES: usize = 5;
const PASTE_SUMMARIZE_BYTES: usize = 400;
#[derive(Debug, Clone)]
pub struct PendingTextPaste {
pub id: u32,
pub content: String,
}
impl PendingTextPaste {
pub fn lines(&self) -> usize {
self.content.lines().count().max(1)
}
pub fn bytes(&self) -> usize {
self.content.len()
}
pub fn placeholder(&self) -> String {
format!(
"[Pasted text #{}: {} lines, {}]",
self.id,
self.lines(),
format_size(self.bytes())
)
}
}
pub fn format_size(bytes: usize) -> String {
const KIB: f64 = 1024.0;
const MIB: f64 = KIB * 1024.0;
if bytes < 1024 {
format!("{bytes} B")
} else if (bytes as f64) < MIB {
format!("{:.1} KiB", bytes as f64 / KIB)
} else {
format!("{:.1} MiB", bytes as f64 / MIB)
}
}
pub fn should_summarize(text: &str) -> bool {
let lines = text.lines().count();
let bytes = text.len();
lines >= PASTE_SUMMARIZE_LINES || bytes >= PASTE_SUMMARIZE_BYTES
}
fn next_paste_id(pastes: &[PendingTextPaste]) -> u32 {
pastes
.iter()
.map(|p| p.id)
.max()
.map(|m| m + 1)
.unwrap_or(1)
}
pub fn attach_paste(state: &mut AppState, content: String) -> String {
let id = next_paste_id(&state.pending_text_pastes);
let paste = PendingTextPaste { id, content };
let placeholder = paste.placeholder();
state.pending_text_pastes.push(paste);
placeholder
}
pub fn expand_paste_placeholders(prompt: &str, pastes: &[PendingTextPaste]) -> String {
let mut out = prompt.to_string();
for paste in pastes {
let ph = paste.placeholder();
if !out.contains(&ph) {
continue;
}
let expansion = format!(
"\n\n--- Begin pasted text #{id} ({lines} lines, {bytes} bytes) ---\n{content}\n--- End pasted text #{id} ---",
id = paste.id,
lines = paste.lines(),
bytes = paste.bytes(),
content = paste.content,
);
out = out.replacen(&ph, &expansion, 1);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn paste(id: u32, content: &str) -> PendingTextPaste {
PendingTextPaste {
id,
content: content.to_string(),
}
}
#[test]
fn format_size_human_readable() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(512), "512 B");
assert_eq!(format_size(1024), "1.0 KiB");
assert_eq!(format_size(1536), "1.5 KiB");
assert_eq!(format_size(2 * 1024 * 1024), "2.0 MiB");
}
#[test]
fn placeholder_reports_lines_and_size() {
let p = paste(7, "a\nb\nc");
assert_eq!(p.placeholder(), "[Pasted text #7: 3 lines, 5 B]");
}
#[test]
fn placeholder_single_line_still_reports_one_line() {
let p = paste(1, "single line");
assert_eq!(p.lines(), 1);
assert!(p.placeholder().contains("1 lines"));
}
#[test]
fn should_summarize_thresholds() {
assert!(!should_summarize("first line\nsecond line"));
assert!(!should_summarize("line one\nline two\nline three"));
assert!(should_summarize("a\nb\nc\nd\ne"));
let big = "x".repeat(400);
assert!(should_summarize(&big));
let almost = "x".repeat(399);
assert!(!should_summarize(&almost));
}
#[test]
fn expand_replaces_placeholder_with_full_content() {
let pastes = vec![paste(1, "hello\nworld")];
let prompt = "please review [Pasted text #1: 2 lines, 11 B] and respond";
let out = expand_paste_placeholders(prompt, &pastes);
assert!(out.contains("--- Begin pasted text #1 (2 lines, 11 bytes) ---"));
assert!(out.contains("hello\nworld"));
assert!(out.contains("--- End pasted text #1 ---"));
assert!(out.starts_with("please review "));
assert!(out.ends_with(" and respond"));
}
#[test]
fn expand_drops_pastes_whose_placeholder_was_deleted() {
let pastes = vec![paste(1, "hello"), paste(2, "world")];
let prompt = "look at [Pasted text #2: 1 lines, 5 B]";
let out = expand_paste_placeholders(prompt, &pastes);
assert!(out.contains("world"));
assert!(!out.contains("hello"));
assert!(!out.contains("#1"));
}
#[test]
fn expand_replaces_only_first_occurrence_per_paste() {
let pastes = vec![paste(1, "DATA")];
let prompt = "[Pasted text #1: 1 lines, 4 B] then again [Pasted text #1: 1 lines, 4 B]";
let out = expand_paste_placeholders(prompt, &pastes);
assert_eq!(out.matches("DATA").count(), 1);
assert_eq!(out.matches("[Pasted text #1: 1 lines, 4 B]").count(), 1);
}
#[test]
fn next_paste_id_starts_at_one_and_increments() {
let mut pastes: Vec<PendingTextPaste> = Vec::new();
assert_eq!(next_paste_id(&pastes), 1);
pastes.push(paste(1, "a"));
assert_eq!(next_paste_id(&pastes), 2);
pastes.push(paste(2, "b"));
assert_eq!(next_paste_id(&pastes), 3);
}
}