devboy_format_pipeline/
truncation.rs1pub fn truncate_string(s: &str, max_chars: usize) -> String {
12 if s.chars().nth(max_chars).is_none() {
14 return s.to_string();
15 }
16
17 let content_limit = max_chars.saturating_sub(3);
19 if content_limit == 0 {
20 return "...".to_string();
21 }
22
23 let byte_limit = s
25 .char_indices()
26 .nth(content_limit)
27 .map(|(i, _)| i)
28 .unwrap_or(s.len());
29 let truncated = &s[..byte_limit];
30
31 if let Some(pos) = truncated.rfind('\n')
33 && pos > byte_limit / 2
34 {
35 return format!("{}...", &s[..pos]);
36 }
37
38 if let Some(pos) = truncated.rfind(' ')
40 && pos > byte_limit / 2
41 {
42 return format!("{}...", &s[..pos]);
43 }
44
45 format!("{truncated}...")
47}
48
49pub fn truncate_diff(diff: &str, max_chars: usize) -> String {
54 if diff.chars().nth(max_chars).is_none() {
56 return diff.to_string();
57 }
58
59 let lines: Vec<&str> = diff.lines().collect();
60 if lines.len() <= 10 {
61 return truncate_string(diff, max_chars);
62 }
63
64 let head: String = lines[..5].join("\n");
66 let tail: String = lines[lines.len() - 5..].join("\n");
67 let hidden_count = lines.len() - 10;
68
69 format!(
70 "{}\n\n... [{} lines hidden] ...\n\n{}",
71 head, hidden_count, tail
72 )
73}
74
75#[derive(Debug, Clone)]
77pub struct TruncationConfig {
78 pub max_items: usize,
80 pub max_total_chars: usize,
82 pub max_item_chars: usize,
84 pub show_indicators: bool,
86}
87
88impl Default for TruncationConfig {
89 fn default() -> Self {
90 Self {
91 max_items: 20,
92 max_total_chars: 4000,
93 max_item_chars: 500,
94 show_indicators: true,
95 }
96 }
97}
98
99pub struct TruncationPlugin {
101 config: TruncationConfig,
102}
103
104impl TruncationPlugin {
105 pub fn new() -> Self {
107 Self {
108 config: TruncationConfig::default(),
109 }
110 }
111
112 pub fn with_limits(max_items: usize, max_chars: usize) -> Self {
114 Self {
115 config: TruncationConfig {
116 max_items,
117 max_total_chars: max_chars,
118 ..Default::default()
119 },
120 }
121 }
122
123 pub fn with_config(config: TruncationConfig) -> Self {
125 Self { config }
126 }
127
128 pub fn max_items(&self) -> usize {
130 self.config.max_items
131 }
132
133 pub fn max_total_chars(&self) -> usize {
135 self.config.max_total_chars
136 }
137
138 pub fn max_item_chars(&self) -> usize {
140 self.config.max_item_chars
141 }
142
143 pub fn truncate(&self, s: &str) -> String {
145 truncate_string(s, self.config.max_total_chars)
146 }
147
148 pub fn truncate_item(&self, s: &str) -> String {
150 truncate_string(s, self.config.max_item_chars)
151 }
152
153 pub fn create_summary(&self, total: usize, shown: usize, item_type: &str) -> String {
155 if shown >= total {
156 return String::new();
157 }
158
159 let remaining = total - shown;
160 format!(
161 "📊 Showing {}/{} {}. {} more available. Use `offset={}` and `limit={}` for next page.",
162 shown, total, item_type, remaining, shown, self.config.max_items
163 )
164 }
165}
166
167impl Default for TruncationPlugin {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_truncate_string_short() {
179 let s = "Hello, world!";
180 assert_eq!(truncate_string(s, 100), s);
181 }
182
183 #[test]
184 fn test_truncate_string_at_word() {
185 let s = "Hello world this is a test";
186 let result = truncate_string(s, 15);
187 assert!(result.ends_with("..."));
188 assert!(result.len() <= 15);
189 }
190
191 #[test]
192 fn test_truncate_string_at_newline() {
193 let s = "Line 1\nLine 2\nLine 3\nLine 4";
194 let result = truncate_string(s, 15);
195 assert!(result.contains("Line 1"));
196 assert!(result.contains("[truncated]") || result.ends_with("..."));
197 }
198
199 #[test]
200 fn test_truncate_diff() {
201 let diff = (1..=20)
202 .map(|i| format!("Line {}", i))
203 .collect::<Vec<_>>()
204 .join("\n");
205
206 let result = truncate_diff(&diff, 50);
208 assert!(result.contains("Line 1"));
209 assert!(result.contains("Line 20"));
210 assert!(result.contains("lines hidden"));
211 }
212
213 #[test]
214 fn test_truncate_diff_short() {
215 let diff = "Line 1\nLine 2\nLine 3";
216 assert_eq!(truncate_diff(diff, 1000), diff);
217 }
218
219 #[test]
220 fn test_plugin_create_summary() {
221 let plugin = TruncationPlugin::with_limits(10, 1000);
222 let summary = plugin.create_summary(25, 10, "issues");
223
224 assert!(summary.contains("10/25"));
225 assert!(summary.contains("15 more"));
226 assert!(summary.contains("offset=10"));
227 }
228
229 #[test]
230 fn test_plugin_no_summary_when_all_shown() {
231 let plugin = TruncationPlugin::new();
232 let summary = plugin.create_summary(5, 5, "issues");
233 assert!(summary.is_empty());
234 }
235
236 #[test]
237 fn test_truncate_string_very_small_limit() {
238 let s = "Hello, world!";
239 let result = truncate_string(s, 3);
240 assert_eq!(result, "...");
241 }
242
243 #[test]
244 fn test_truncate_string_zero_limit() {
245 let s = "Hello, world!";
246 let result = truncate_string(s, 0);
247 assert_eq!(result, "...");
248 }
249
250 #[test]
251 fn test_truncate_string_hard_truncate() {
252 let s = "abcdefghijklmnopqrstuvwxyz";
254 let result = truncate_string(s, 10);
255 assert_eq!(result.len(), 10);
256 assert_eq!(result, "abcdefg...");
257 }
258
259 #[test]
260 fn test_truncate_diff_few_lines() {
261 let diff = "L1\nL2\nL3\nL4\nL5\nL6\nL7\nL8";
263 let result = truncate_diff(diff, 10);
264 assert!(result.ends_with("...") || result == diff);
265 }
266
267 #[test]
268 fn test_plugin_with_config() {
269 let config = TruncationConfig {
270 max_items: 5,
271 max_total_chars: 200,
272 max_item_chars: 50,
273 show_indicators: false,
274 };
275 let plugin = TruncationPlugin::with_config(config);
276
277 assert_eq!(plugin.max_items(), 5);
278 assert_eq!(plugin.max_total_chars(), 200);
279 assert_eq!(plugin.max_item_chars(), 50);
280 }
281
282 #[test]
283 fn test_plugin_with_limits() {
284 let plugin = TruncationPlugin::with_limits(15, 2000);
285
286 assert_eq!(plugin.max_items(), 15);
287 assert_eq!(plugin.max_total_chars(), 2000);
288 assert_eq!(plugin.max_item_chars(), 500); }
290
291 #[test]
292 fn test_plugin_truncate() {
293 let plugin = TruncationPlugin::with_limits(10, 20);
294
295 let short = "Hello";
296 assert_eq!(plugin.truncate(short), "Hello");
297
298 let long = "This is a much longer string that will be truncated";
299 let result = plugin.truncate(long);
300 assert!(result.len() <= 20);
301 assert!(result.ends_with("..."));
302 }
303
304 #[test]
305 fn test_plugin_truncate_item() {
306 let config = TruncationConfig {
307 max_item_chars: 10,
308 ..Default::default()
309 };
310 let plugin = TruncationPlugin::with_config(config);
311
312 let long = "This is a long item description";
313 let result = plugin.truncate_item(long);
314 assert!(result.len() <= 10);
315 assert!(result.ends_with("..."));
316 }
317
318 #[test]
319 fn test_plugin_default() {
320 let plugin = TruncationPlugin::default();
321 assert_eq!(plugin.max_items(), 20);
322 assert_eq!(plugin.max_total_chars(), 4000);
323 }
324
325 #[test]
326 fn test_truncation_config_default() {
327 let config = TruncationConfig::default();
328 assert_eq!(config.max_items, 20);
329 assert_eq!(config.max_total_chars, 4000);
330 assert_eq!(config.max_item_chars, 500);
331 assert!(config.show_indicators);
332 }
333}