pub fn truncate_string(s: &str, max_chars: usize) -> String {
if s.chars().nth(max_chars).is_none() {
return s.to_string();
}
let content_limit = max_chars.saturating_sub(3);
if content_limit == 0 {
return "...".to_string();
}
let byte_limit = s
.char_indices()
.nth(content_limit)
.map(|(i, _)| i)
.unwrap_or(s.len());
let truncated = &s[..byte_limit];
if let Some(pos) = truncated.rfind('\n')
&& pos > byte_limit / 2
{
return format!("{}...", &s[..pos]);
}
if let Some(pos) = truncated.rfind(' ')
&& pos > byte_limit / 2
{
return format!("{}...", &s[..pos]);
}
format!("{truncated}...")
}
pub fn truncate_diff(diff: &str, max_chars: usize) -> String {
if diff.chars().nth(max_chars).is_none() {
return diff.to_string();
}
let lines: Vec<&str> = diff.lines().collect();
if lines.len() <= 10 {
return truncate_string(diff, max_chars);
}
let head: String = lines[..5].join("\n");
let tail: String = lines[lines.len() - 5..].join("\n");
let hidden_count = lines.len() - 10;
format!(
"{}\n\n... [{} lines hidden] ...\n\n{}",
head, hidden_count, tail
)
}
#[derive(Debug, Clone)]
pub struct TruncationConfig {
pub max_items: usize,
pub max_total_chars: usize,
pub max_item_chars: usize,
pub show_indicators: bool,
}
impl Default for TruncationConfig {
fn default() -> Self {
Self {
max_items: 20,
max_total_chars: 4000,
max_item_chars: 500,
show_indicators: true,
}
}
}
pub struct TruncationPlugin {
config: TruncationConfig,
}
impl TruncationPlugin {
pub fn new() -> Self {
Self {
config: TruncationConfig::default(),
}
}
pub fn with_limits(max_items: usize, max_chars: usize) -> Self {
Self {
config: TruncationConfig {
max_items,
max_total_chars: max_chars,
..Default::default()
},
}
}
pub fn with_config(config: TruncationConfig) -> Self {
Self { config }
}
pub fn max_items(&self) -> usize {
self.config.max_items
}
pub fn max_total_chars(&self) -> usize {
self.config.max_total_chars
}
pub fn max_item_chars(&self) -> usize {
self.config.max_item_chars
}
pub fn truncate(&self, s: &str) -> String {
truncate_string(s, self.config.max_total_chars)
}
pub fn truncate_item(&self, s: &str) -> String {
truncate_string(s, self.config.max_item_chars)
}
pub fn create_summary(&self, total: usize, shown: usize, item_type: &str) -> String {
if shown >= total {
return String::new();
}
let remaining = total - shown;
format!(
"📊 Showing {}/{} {}. {} more available. Use `offset={}` and `limit={}` for next page.",
shown, total, item_type, remaining, shown, self.config.max_items
)
}
}
impl Default for TruncationPlugin {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_string_short() {
let s = "Hello, world!";
assert_eq!(truncate_string(s, 100), s);
}
#[test]
fn test_truncate_string_at_word() {
let s = "Hello world this is a test";
let result = truncate_string(s, 15);
assert!(result.ends_with("..."));
assert!(result.len() <= 15);
}
#[test]
fn test_truncate_string_at_newline() {
let s = "Line 1\nLine 2\nLine 3\nLine 4";
let result = truncate_string(s, 15);
assert!(result.contains("Line 1"));
assert!(result.contains("[truncated]") || result.ends_with("..."));
}
#[test]
fn test_truncate_diff() {
let diff = (1..=20)
.map(|i| format!("Line {}", i))
.collect::<Vec<_>>()
.join("\n");
let result = truncate_diff(&diff, 50);
assert!(result.contains("Line 1"));
assert!(result.contains("Line 20"));
assert!(result.contains("lines hidden"));
}
#[test]
fn test_truncate_diff_short() {
let diff = "Line 1\nLine 2\nLine 3";
assert_eq!(truncate_diff(diff, 1000), diff);
}
#[test]
fn test_plugin_create_summary() {
let plugin = TruncationPlugin::with_limits(10, 1000);
let summary = plugin.create_summary(25, 10, "issues");
assert!(summary.contains("10/25"));
assert!(summary.contains("15 more"));
assert!(summary.contains("offset=10"));
}
#[test]
fn test_plugin_no_summary_when_all_shown() {
let plugin = TruncationPlugin::new();
let summary = plugin.create_summary(5, 5, "issues");
assert!(summary.is_empty());
}
#[test]
fn test_truncate_string_very_small_limit() {
let s = "Hello, world!";
let result = truncate_string(s, 3);
assert_eq!(result, "...");
}
#[test]
fn test_truncate_string_zero_limit() {
let s = "Hello, world!";
let result = truncate_string(s, 0);
assert_eq!(result, "...");
}
#[test]
fn test_truncate_string_hard_truncate() {
let s = "abcdefghijklmnopqrstuvwxyz";
let result = truncate_string(s, 10);
assert_eq!(result.len(), 10);
assert_eq!(result, "abcdefg...");
}
#[test]
fn test_truncate_diff_few_lines() {
let diff = "L1\nL2\nL3\nL4\nL5\nL6\nL7\nL8";
let result = truncate_diff(diff, 10);
assert!(result.ends_with("...") || result == diff);
}
#[test]
fn test_plugin_with_config() {
let config = TruncationConfig {
max_items: 5,
max_total_chars: 200,
max_item_chars: 50,
show_indicators: false,
};
let plugin = TruncationPlugin::with_config(config);
assert_eq!(plugin.max_items(), 5);
assert_eq!(plugin.max_total_chars(), 200);
assert_eq!(plugin.max_item_chars(), 50);
}
#[test]
fn test_plugin_with_limits() {
let plugin = TruncationPlugin::with_limits(15, 2000);
assert_eq!(plugin.max_items(), 15);
assert_eq!(plugin.max_total_chars(), 2000);
assert_eq!(plugin.max_item_chars(), 500); }
#[test]
fn test_plugin_truncate() {
let plugin = TruncationPlugin::with_limits(10, 20);
let short = "Hello";
assert_eq!(plugin.truncate(short), "Hello");
let long = "This is a much longer string that will be truncated";
let result = plugin.truncate(long);
assert!(result.len() <= 20);
assert!(result.ends_with("..."));
}
#[test]
fn test_plugin_truncate_item() {
let config = TruncationConfig {
max_item_chars: 10,
..Default::default()
};
let plugin = TruncationPlugin::with_config(config);
let long = "This is a long item description";
let result = plugin.truncate_item(long);
assert!(result.len() <= 10);
assert!(result.ends_with("..."));
}
#[test]
fn test_plugin_default() {
let plugin = TruncationPlugin::default();
assert_eq!(plugin.max_items(), 20);
assert_eq!(plugin.max_total_chars(), 4000);
}
#[test]
fn test_truncation_config_default() {
let config = TruncationConfig::default();
assert_eq!(config.max_items, 20);
assert_eq!(config.max_total_chars, 4000);
assert_eq!(config.max_item_chars, 500);
assert!(config.show_indicators);
}
}