1use std::borrow::Cow;
16use std::cmp;
17use std::io;
18
19use bstr::ByteSlice as _;
20use unicode_width::UnicodeWidthChar as _;
21use unicode_width::UnicodeWidthStr as _;
22
23use crate::formatter::FormatRecorder;
24use crate::formatter::Formatter;
25
26pub fn complete_newline(s: impl Into<String>) -> String {
27 let mut s = s.into();
28 if !s.is_empty() && !s.ends_with('\n') {
29 s.push('\n');
30 }
31 s
32}
33
34pub fn split_email(email: &str) -> (&str, Option<&str>) {
35 if let Some((username, rest)) = email.split_once('@') {
36 (username, Some(rest))
37 } else {
38 (email, None)
39 }
40}
41
42pub fn elide_start<'a>(
47 text: &'a str,
48 ellipsis: &'a str,
49 max_width: usize,
50) -> (Cow<'a, str>, usize) {
51 let (text_start, text_width) = truncate_start_pos(text, max_width);
52 if text_start == 0 {
53 return (Cow::Borrowed(text), text_width);
54 }
55
56 let (ellipsis_start, ellipsis_width) = truncate_start_pos(ellipsis, max_width);
57 if ellipsis_start != 0 {
58 let ellipsis = trim_start_zero_width_chars(&ellipsis[ellipsis_start..]);
59 return (Cow::Borrowed(ellipsis), ellipsis_width);
60 }
61
62 let text = &text[text_start..];
63 let max_text_width = max_width - ellipsis_width;
64 let (skip, skipped_width) = skip_start_pos(text, text_width.saturating_sub(max_text_width));
65 let text = trim_start_zero_width_chars(&text[skip..]);
66 let concat_width = ellipsis_width + (text_width - skipped_width);
67 assert!(concat_width <= max_width);
68 (Cow::Owned([ellipsis, text].concat()), concat_width)
69}
70
71pub fn elide_end<'a>(text: &'a str, ellipsis: &'a str, max_width: usize) -> (Cow<'a, str>, usize) {
76 let (text_end, text_width) = truncate_end_pos(text, max_width);
77 if text_end == text.len() {
78 return (Cow::Borrowed(text), text_width);
79 }
80
81 let (ellipsis_end, ellipsis_width) = truncate_end_pos(ellipsis, max_width);
82 if ellipsis_end != ellipsis.len() {
83 let ellipsis = &ellipsis[..ellipsis_end];
84 return (Cow::Borrowed(ellipsis), ellipsis_width);
85 }
86
87 let text = &text[..text_end];
88 let max_text_width = max_width - ellipsis_width;
89 let (skip, skipped_width) = skip_end_pos(text, text_width.saturating_sub(max_text_width));
90 let text = &text[..skip];
91 let concat_width = (text_width - skipped_width) + ellipsis_width;
92 assert!(concat_width <= max_width);
93 (Cow::Owned([text, ellipsis].concat()), concat_width)
94}
95
96fn truncate_start_pos(text: &str, max_width: usize) -> (usize, usize) {
101 truncate_start_pos_with_indices(
102 text.char_indices()
103 .rev()
104 .map(|(start, c)| (start + c.len_utf8(), c)),
105 max_width,
106 )
107}
108
109fn truncate_start_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) {
110 truncate_start_pos_with_indices(
111 text.char_indices().rev().map(|(_, end, c)| (end, c)),
112 max_width,
113 )
114}
115
116fn truncate_start_pos_with_indices(
117 char_indices_rev: impl Iterator<Item = (usize, char)>,
118 max_width: usize,
119) -> (usize, usize) {
120 let mut acc_width = 0;
121 for (end, c) in char_indices_rev {
122 let new_width = acc_width + c.width().unwrap_or(0);
123 if new_width > max_width {
124 return (end, acc_width);
125 }
126 acc_width = new_width;
127 }
128 (0, acc_width)
129}
130
131fn truncate_end_pos(text: &str, max_width: usize) -> (usize, usize) {
134 truncate_end_pos_with_indices(text.char_indices(), text.len(), max_width)
135}
136
137fn truncate_end_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) {
138 truncate_end_pos_with_indices(
139 text.char_indices().map(|(start, _, c)| (start, c)),
140 text.len(),
141 max_width,
142 )
143}
144
145fn truncate_end_pos_with_indices(
146 char_indices_fwd: impl Iterator<Item = (usize, char)>,
147 text_len: usize,
148 max_width: usize,
149) -> (usize, usize) {
150 let mut acc_width = 0;
151 for (start, c) in char_indices_fwd {
152 let new_width = acc_width + c.width().unwrap_or(0);
153 if new_width > max_width {
154 return (start, acc_width);
155 }
156 acc_width = new_width;
157 }
158 (text_len, acc_width)
159}
160
161fn skip_start_pos(text: &str, width: usize) -> (usize, usize) {
168 skip_start_pos_with_indices(text.char_indices(), text.len(), width)
169}
170
171fn skip_start_pos_with_indices(
172 char_indices_fwd: impl Iterator<Item = (usize, char)>,
173 text_len: usize,
174 width: usize,
175) -> (usize, usize) {
176 let mut acc_width = 0;
177 for (start, c) in char_indices_fwd {
178 if acc_width >= width {
179 return (start, acc_width);
180 }
181 acc_width += c.width().unwrap_or(0);
182 }
183 (text_len, acc_width)
184}
185
186fn skip_end_pos(text: &str, width: usize) -> (usize, usize) {
191 skip_end_pos_with_indices(
192 text.char_indices()
193 .rev()
194 .map(|(start, c)| (start + c.len_utf8(), c)),
195 width,
196 )
197}
198
199fn skip_end_pos_with_indices(
200 char_indices_rev: impl Iterator<Item = (usize, char)>,
201 width: usize,
202) -> (usize, usize) {
203 let mut acc_width = 0;
204 for (end, c) in char_indices_rev {
205 if acc_width >= width {
206 return (end, acc_width);
207 }
208 acc_width += c.width().unwrap_or(0);
209 }
210 (0, acc_width)
211}
212
213fn trim_start_zero_width_chars(text: &str) -> &str {
215 text.trim_start_matches(|c: char| c.width().unwrap_or(0) == 0)
216}
217
218fn count_start_zero_width_chars_bytes(text: &[u8]) -> usize {
220 text.char_indices()
221 .find(|(_, _, c)| c.width().unwrap_or(0) != 0)
222 .map(|(start, _, _)| start)
223 .unwrap_or(text.len())
224}
225
226pub fn write_truncated_start(
231 formatter: &mut dyn Formatter,
232 recorded_content: &FormatRecorder,
233 recorded_ellipsis: &FormatRecorder,
234 max_width: usize,
235) -> io::Result<usize> {
236 let data = recorded_content.data();
237 let data_width = String::from_utf8_lossy(data).width();
238 let ellipsis_data = recorded_ellipsis.data();
239 let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width();
240
241 let (start, mut truncated_width) = if data_width > max_width {
242 truncate_start_pos_bytes(data, max_width.saturating_sub(ellipsis_width))
243 } else {
244 (0, data_width)
245 };
246
247 let mut replay_truncated = |recorded: &FormatRecorder, truncated_start: usize| {
248 recorded.replay_with(formatter, |formatter, range| {
249 let start = cmp::max(range.start, truncated_start);
250 if start < range.end {
251 formatter.write_all(&recorded.data()[start..range.end])?;
252 }
253 Ok(())
254 })
255 };
256
257 if data_width > max_width {
258 let (start, ellipsis_width) = truncate_start_pos_bytes(ellipsis_data, max_width);
260 let truncated_start = start + count_start_zero_width_chars_bytes(&ellipsis_data[start..]);
261 truncated_width += ellipsis_width;
262 replay_truncated(recorded_ellipsis, truncated_start)?;
263 }
264 let truncated_start = start + count_start_zero_width_chars_bytes(&data[start..]);
265 replay_truncated(recorded_content, truncated_start)?;
266 Ok(truncated_width)
267}
268
269pub fn write_truncated_end(
274 formatter: &mut dyn Formatter,
275 recorded_content: &FormatRecorder,
276 recorded_ellipsis: &FormatRecorder,
277 max_width: usize,
278) -> io::Result<usize> {
279 let data = recorded_content.data();
280 let data_width = String::from_utf8_lossy(data).width();
281 let ellipsis_data = recorded_ellipsis.data();
282 let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width();
283
284 let (truncated_end, mut truncated_width) = if data_width > max_width {
285 truncate_end_pos_bytes(data, max_width.saturating_sub(ellipsis_width))
286 } else {
287 (data.len(), data_width)
288 };
289
290 let mut replay_truncated = |recorded: &FormatRecorder, truncated_end: usize| {
291 recorded.replay_with(formatter, |formatter, range| {
292 let end = cmp::min(range.end, truncated_end);
293 if range.start < end {
294 formatter.write_all(&recorded.data()[range.start..end])?;
295 }
296 Ok(())
297 })
298 };
299
300 replay_truncated(recorded_content, truncated_end)?;
301 if data_width > max_width {
302 let (truncated_end, ellipsis_width) = truncate_end_pos_bytes(ellipsis_data, max_width);
304 truncated_width += ellipsis_width;
305 replay_truncated(recorded_ellipsis, truncated_end)?;
306 }
307 Ok(truncated_width)
308}
309
310pub fn write_padded_start(
315 formatter: &mut dyn Formatter,
316 recorded_content: &FormatRecorder,
317 recorded_fill_char: &FormatRecorder,
318 min_width: usize,
319) -> io::Result<()> {
320 let width = String::from_utf8_lossy(recorded_content.data()).width();
322 let fill_width = min_width.saturating_sub(width);
323 write_padding(formatter, recorded_fill_char, fill_width)?;
324 recorded_content.replay(formatter)?;
325 Ok(())
326}
327
328pub fn write_padded_end(
333 formatter: &mut dyn Formatter,
334 recorded_content: &FormatRecorder,
335 recorded_fill_char: &FormatRecorder,
336 min_width: usize,
337) -> io::Result<()> {
338 let width = String::from_utf8_lossy(recorded_content.data()).width();
340 let fill_width = min_width.saturating_sub(width);
341 recorded_content.replay(formatter)?;
342 write_padding(formatter, recorded_fill_char, fill_width)?;
343 Ok(())
344}
345
346pub fn write_padded_centered(
352 formatter: &mut dyn Formatter,
353 recorded_content: &FormatRecorder,
354 recorded_fill_char: &FormatRecorder,
355 min_width: usize,
356) -> io::Result<()> {
357 let width = String::from_utf8_lossy(recorded_content.data()).width();
359 let fill_width = min_width.saturating_sub(width);
360 let fill_left = fill_width / 2;
361 let fill_right = fill_width - fill_left;
362 write_padding(formatter, recorded_fill_char, fill_left)?;
363 recorded_content.replay(formatter)?;
364 write_padding(formatter, recorded_fill_char, fill_right)?;
365 Ok(())
366}
367
368fn write_padding(
369 formatter: &mut dyn Formatter,
370 recorded_fill_char: &FormatRecorder,
371 fill_width: usize,
372) -> io::Result<()> {
373 if fill_width == 0 {
374 return Ok(());
375 }
376 let data = recorded_fill_char.data();
377 recorded_fill_char.replay_with(formatter, |formatter, range| {
378 for _ in 0..fill_width {
382 formatter.write_all(&data[range.clone()])?;
383 }
384 Ok(())
385 })
386}
387
388pub fn write_indented(
390 formatter: &mut dyn Formatter,
391 recorded_content: &FormatRecorder,
392 mut write_prefix: impl FnMut(&mut dyn Formatter) -> io::Result<()>,
393) -> io::Result<()> {
394 let data = recorded_content.data();
395 let mut new_line = true;
396 recorded_content.replay_with(formatter, |formatter, range| {
397 for line in data[range].split_inclusive(|&c| c == b'\n') {
398 if new_line && line != b"\n" {
399 write_prefix(formatter)?;
402 }
403 formatter.write_all(line)?;
404 new_line = line.ends_with(b"\n");
405 }
406 Ok(())
407 })
408}
409
410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
412struct ByteFragment<'a> {
413 word: &'a [u8],
414 whitespace_len: usize,
415 word_width: usize,
416}
417
418impl<'a> ByteFragment<'a> {
419 fn new(word: &'a [u8], whitespace_len: usize) -> Self {
420 let word_width = textwrap::core::display_width(&String::from_utf8_lossy(word));
422 Self {
423 word,
424 whitespace_len,
425 word_width,
426 }
427 }
428
429 fn offset_in(&self, text: &[u8]) -> usize {
430 byte_offset_from(text, self.word)
431 }
432}
433
434impl textwrap::core::Fragment for ByteFragment<'_> {
435 fn width(&self) -> f64 {
436 self.word_width as f64
437 }
438
439 fn whitespace_width(&self) -> f64 {
440 self.whitespace_len as f64
441 }
442
443 fn penalty_width(&self) -> f64 {
444 0.0
445 }
446}
447
448fn byte_offset_from(outer: &[u8], inner: &[u8]) -> usize {
449 let outer_start = outer.as_ptr() as usize;
450 let inner_start = inner.as_ptr() as usize;
451 assert!(outer_start <= inner_start);
452 assert!(inner_start + inner.len() <= outer_start + outer.len());
453 inner_start - outer_start
454}
455
456fn split_byte_line_to_words(line: &[u8]) -> Vec<ByteFragment<'_>> {
457 let mut words = Vec::new();
458 let mut tail = line;
459 while let Some(word_end) = tail.iter().position(|&c| c == b' ') {
460 let word = &tail[..word_end];
461 let ws_end = tail[word_end + 1..]
462 .iter()
463 .position(|&c| c != b' ')
464 .map(|p| p + word_end + 1)
465 .unwrap_or(tail.len());
466 words.push(ByteFragment::new(word, ws_end - word_end));
467 tail = &tail[ws_end..];
468 }
469 if !tail.is_empty() {
470 words.push(ByteFragment::new(tail, 0));
471 }
472 words
473}
474
475pub fn wrap_bytes(text: &[u8], width: usize) -> Vec<&[u8]> {
487 let mut split_lines = Vec::new();
488 for line in text.split(|&c| c == b'\n') {
489 let words = split_byte_line_to_words(line);
490 let split = textwrap::wrap_algorithms::wrap_first_fit(&words, &[width as f64]);
491 split_lines.extend(split.iter().map(|words| match words {
492 [] => &line[..0], [a] => a.word,
494 [a, .., b] => {
495 let start = a.offset_in(line);
496 let end = b.offset_in(line) + b.word.len();
497 &line[start..end]
498 }
499 }));
500 }
501 split_lines
502}
503
504pub fn write_wrapped(
511 formatter: &mut dyn Formatter,
512 recorded_content: &FormatRecorder,
513 width: usize,
514) -> io::Result<()> {
515 let data = recorded_content.data();
516 let mut line_ranges = wrap_bytes(data, width)
517 .into_iter()
518 .map(|line| {
519 let start = byte_offset_from(data, line);
520 start..start + line.len()
521 })
522 .peekable();
523 recorded_content.replay_with(formatter, |formatter, data_range| {
526 while let Some(line_range) = line_ranges.peek() {
527 let start = cmp::max(data_range.start, line_range.start);
528 let end = cmp::min(data_range.end, line_range.end);
529 if start < end {
530 formatter.write_all(&data[start..end])?;
531 }
532 if data_range.end <= line_range.end {
533 break; }
535 line_ranges.next().unwrap();
536 if line_ranges.peek().is_some() {
537 writeln!(formatter)?; }
539 }
540 Ok(())
541 })
542}
543
544pub fn parse_author(author: &str) -> Result<(String, String), &'static str> {
545 let re = regex::Regex::new(r"(?<name>.*?)\s*<(?<email>.+)>$").unwrap();
546 let captures = re.captures(author).ok_or("Invalid author string")?;
547 Ok((captures["name"].to_string(), captures["email"].to_string()))
548}
549
550#[cfg(test)]
551mod tests {
552 use std::io::Write as _;
553
554 use indoc::indoc;
555 use jj_lib::config::ConfigLayer;
556 use jj_lib::config::ConfigSource;
557 use jj_lib::config::StackedConfig;
558 use testutils::TestResult;
559
560 use super::*;
561 use crate::formatter::ColorFormatter;
562 use crate::formatter::PlainTextFormatter;
563
564 fn format_colored(write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>) -> String {
565 let mut config = StackedConfig::empty();
566 config.add_layer(
567 ConfigLayer::parse(
568 ConfigSource::Default,
569 indoc! {"
570 colors.cyan = 'cyan'
571 colors.red = 'red'
572 "},
573 )
574 .unwrap(),
575 );
576 let mut output = Vec::new();
577 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
578 write(&mut formatter).unwrap();
579 drop(formatter);
580 String::from_utf8(output).unwrap()
581 }
582
583 fn format_plain_text(write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>) -> String {
584 let mut output = Vec::new();
585 let mut formatter = PlainTextFormatter::new(&mut output);
586 write(&mut formatter).unwrap();
587 String::from_utf8(output).unwrap()
588 }
589
590 #[test]
591 fn test_complete_newline() {
592 assert_eq!(complete_newline(""), "");
593 assert_eq!(complete_newline(" "), " \n");
594 assert_eq!(complete_newline("\n "), "\n \n");
595 assert_eq!(complete_newline("\t"), "\t\n");
596 assert_eq!(complete_newline("\n"), "\n");
597 assert_eq!(complete_newline("\n\n"), "\n\n");
598 assert_eq!(complete_newline("a\nb\nc"), "a\nb\nc\n");
599 assert_eq!(complete_newline("a\nb\nc\n"), "a\nb\nc\n");
600 }
601
602 #[test]
603 fn test_split_email() {
604 assert_eq!(split_email(""), ("", None));
605 assert_eq!(split_email("abc"), ("abc", None));
606 assert_eq!(split_email("example.com"), ("example.com", None));
607 assert_eq!(split_email("@example.com"), ("", Some("example.com")));
608 assert_eq!(
609 split_email("user@example.com"),
610 ("user", Some("example.com"))
611 );
612 assert_eq!(
613 split_email("user+tag@example.com"),
614 ("user+tag", Some("example.com"))
615 );
616 assert_eq!(
617 split_email(" user @ example.com "),
618 (" user ", Some(" example.com "))
619 );
620 assert_eq!(
621 split_email("user@extra@example.com"),
622 ("user", Some("extra@example.com"))
623 );
624 }
625
626 #[test]
627 fn test_elide_start() {
628 assert_eq!(elide_start("", "", 1), ("".into(), 0));
630
631 assert_eq!(elide_start("abcdef", "", 6), ("abcdef".into(), 6));
633 assert_eq!(elide_start("abcdef", "", 5), ("bcdef".into(), 5));
634 assert_eq!(elide_start("abcdef", "", 1), ("f".into(), 1));
635 assert_eq!(elide_start("abcdef", "", 0), ("".into(), 0));
636 assert_eq!(elide_start("abcdef", "-=~", 6), ("abcdef".into(), 6));
637 assert_eq!(elide_start("abcdef", "-=~", 5), ("-=~ef".into(), 5));
638 assert_eq!(elide_start("abcdef", "-=~", 4), ("-=~f".into(), 4));
639 assert_eq!(elide_start("abcdef", "-=~", 3), ("-=~".into(), 3));
640 assert_eq!(elide_start("abcdef", "-=~", 2), ("=~".into(), 2));
641 assert_eq!(elide_start("abcdef", "-=~", 1), ("~".into(), 1));
642 assert_eq!(elide_start("abcdef", "-=~", 0), ("".into(), 0));
643
644 assert_eq!(elide_start("一二三", "", 6), ("一二三".into(), 6));
646 assert_eq!(elide_start("一二三", "", 5), ("二三".into(), 4));
647 assert_eq!(elide_start("一二三", "", 4), ("二三".into(), 4));
648 assert_eq!(elide_start("一二三", "", 1), ("".into(), 0));
649 assert_eq!(elide_start("一二三", "-=~", 6), ("一二三".into(), 6));
650 assert_eq!(elide_start("一二三", "-=~", 5), ("-=~三".into(), 5));
651 assert_eq!(elide_start("一二三", "-=~", 4), ("-=~".into(), 3));
652 assert_eq!(elide_start("一二三", "略", 6), ("一二三".into(), 6));
653 assert_eq!(elide_start("一二三", "略", 5), ("略三".into(), 4));
654 assert_eq!(elide_start("一二三", "略", 4), ("略三".into(), 4));
655 assert_eq!(elide_start("一二三", "略", 2), ("略".into(), 2));
656 assert_eq!(elide_start("一二三", "略", 1), ("".into(), 0));
657 assert_eq!(elide_start("一二三", ".", 5), (".二三".into(), 5));
658 assert_eq!(elide_start("一二三", ".", 4), (".三".into(), 3));
659 assert_eq!(elide_start("一二三", "略.", 5), ("略.三".into(), 5));
660 assert_eq!(elide_start("一二三", "略.", 4), ("略.".into(), 3));
661
662 assert_eq!(elide_start("àbcdè", "", 5), ("àbcdè".into(), 5));
664 assert_eq!(elide_start("àbcdè", "", 4), ("bcdè".into(), 4));
665 assert_eq!(elide_start("àbcdè", "", 1), ("è".into(), 1));
666 assert_eq!(elide_start("àbcdè", "", 0), ("".into(), 0));
667 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 4), ("ÀÇÈè".into(), 4));
668 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
669 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 2), ("ÇÈ".into(), 2));
670
671 assert_eq!(
673 elide_start("a\u{300}bcde\u{300}", "", 5),
674 ("a\u{300}bcde\u{300}".into(), 5)
675 );
676 assert_eq!(
677 elide_start("a\u{300}bcde\u{300}", "", 4),
678 ("bcde\u{300}".into(), 4)
679 );
680 assert_eq!(
681 elide_start("a\u{300}bcde\u{300}", "", 1),
682 ("e\u{300}".into(), 1)
683 );
684 assert_eq!(elide_start("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
685 assert_eq!(
686 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
687 ("A\u{300}CE\u{300}e\u{300}".into(), 4)
688 );
689 assert_eq!(
690 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
691 ("A\u{300}CE\u{300}".into(), 3)
692 );
693 assert_eq!(
694 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
695 ("CE\u{300}".into(), 2)
696 );
697 }
698
699 #[test]
700 fn test_elide_end() {
701 assert_eq!(elide_end("", "", 1), ("".into(), 0));
703
704 assert_eq!(elide_end("abcdef", "", 6), ("abcdef".into(), 6));
706 assert_eq!(elide_end("abcdef", "", 5), ("abcde".into(), 5));
707 assert_eq!(elide_end("abcdef", "", 1), ("a".into(), 1));
708 assert_eq!(elide_end("abcdef", "", 0), ("".into(), 0));
709 assert_eq!(elide_end("abcdef", "-=~", 6), ("abcdef".into(), 6));
710 assert_eq!(elide_end("abcdef", "-=~", 5), ("ab-=~".into(), 5));
711 assert_eq!(elide_end("abcdef", "-=~", 4), ("a-=~".into(), 4));
712 assert_eq!(elide_end("abcdef", "-=~", 3), ("-=~".into(), 3));
713 assert_eq!(elide_end("abcdef", "-=~", 2), ("-=".into(), 2));
714 assert_eq!(elide_end("abcdef", "-=~", 1), ("-".into(), 1));
715 assert_eq!(elide_end("abcdef", "-=~", 0), ("".into(), 0));
716
717 assert_eq!(elide_end("一二三", "", 6), ("一二三".into(), 6));
719 assert_eq!(elide_end("一二三", "", 5), ("一二".into(), 4));
720 assert_eq!(elide_end("一二三", "", 4), ("一二".into(), 4));
721 assert_eq!(elide_end("一二三", "", 1), ("".into(), 0));
722 assert_eq!(elide_end("一二三", "-=~", 6), ("一二三".into(), 6));
723 assert_eq!(elide_end("一二三", "-=~", 5), ("一-=~".into(), 5));
724 assert_eq!(elide_end("一二三", "-=~", 4), ("-=~".into(), 3));
725 assert_eq!(elide_end("一二三", "略", 6), ("一二三".into(), 6));
726 assert_eq!(elide_end("一二三", "略", 5), ("一略".into(), 4));
727 assert_eq!(elide_end("一二三", "略", 4), ("一略".into(), 4));
728 assert_eq!(elide_end("一二三", "略", 2), ("略".into(), 2));
729 assert_eq!(elide_end("一二三", "略", 1), ("".into(), 0));
730 assert_eq!(elide_end("一二三", ".", 5), ("一二.".into(), 5));
731 assert_eq!(elide_end("一二三", ".", 4), ("一.".into(), 3));
732 assert_eq!(elide_end("一二三", "略.", 5), ("一略.".into(), 5));
733 assert_eq!(elide_end("一二三", "略.", 4), ("略.".into(), 3));
734
735 assert_eq!(elide_end("àbcdè", "", 5), ("àbcdè".into(), 5));
737 assert_eq!(elide_end("àbcdè", "", 4), ("àbcd".into(), 4));
738 assert_eq!(elide_end("àbcdè", "", 1), ("à".into(), 1));
739 assert_eq!(elide_end("àbcdè", "", 0), ("".into(), 0));
740 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 4), ("àÀÇÈ".into(), 4));
741 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
742 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 2), ("ÀÇ".into(), 2));
743
744 assert_eq!(
746 elide_end("a\u{300}bcde\u{300}", "", 5),
747 ("a\u{300}bcde\u{300}".into(), 5)
748 );
749 assert_eq!(
750 elide_end("a\u{300}bcde\u{300}", "", 4),
751 ("a\u{300}bcd".into(), 4)
752 );
753 assert_eq!(
754 elide_end("a\u{300}bcde\u{300}", "", 1),
755 ("a\u{300}".into(), 1)
756 );
757 assert_eq!(elide_end("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
758 assert_eq!(
759 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
760 ("a\u{300}A\u{300}CE\u{300}".into(), 4)
761 );
762 assert_eq!(
763 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
764 ("A\u{300}CE\u{300}".into(), 3)
765 );
766 assert_eq!(
767 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
768 ("A\u{300}C".into(), 2)
769 );
770 }
771
772 #[test]
773 fn test_write_truncated_labeled() -> TestResult {
774 let ellipsis_recorder = FormatRecorder::new(false);
775 let mut recorder = FormatRecorder::new(false);
776 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
777 recorder.push_label(label);
778 write!(recorder, "{word}")?;
779 recorder.pop_label();
780 }
781
782 insta::assert_snapshot!(
784 format_colored(|formatter| {
785 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
786 }),
787 @"[38;5;1mfoo[38;5;6mbar[39m"
788 );
789 insta::assert_snapshot!(
790 format_colored(|formatter| {
791 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
792 }),
793 @"[38;5;1moo[38;5;6mbar[39m"
794 );
795 insta::assert_snapshot!(
796 format_colored(|formatter| {
797 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
798 }),
799 @"[38;5;6mbar[39m"
800 );
801 insta::assert_snapshot!(
802 format_colored(|formatter| {
803 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
804 }),
805 @"[38;5;6mar[39m"
806 );
807 insta::assert_snapshot!(
808 format_colored(|formatter| {
809 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
810 }),
811 @""
812 );
813
814 insta::assert_snapshot!(
816 format_colored(|formatter| {
817 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
818 }),
819 @"[38;5;1mfoo[38;5;6mbar[39m"
820 );
821 insta::assert_snapshot!(
822 format_colored(|formatter| {
823 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
824 }),
825 @"[38;5;1mfoo[38;5;6mba[39m"
826 );
827 insta::assert_snapshot!(
828 format_colored(|formatter| {
829 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
830 }),
831 @"[38;5;1mfoo[39m"
832 );
833 insta::assert_snapshot!(
834 format_colored(|formatter| {
835 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
836 }),
837 @"[38;5;1mfo[39m"
838 );
839 insta::assert_snapshot!(
840 format_colored(|formatter| {
841 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
842 }),
843 @""
844 );
845 Ok(())
846 }
847
848 #[test]
849 fn test_write_truncated_non_ascii_chars() -> TestResult {
850 let ellipsis_recorder = FormatRecorder::new(false);
851 let mut recorder = FormatRecorder::new(false);
852 write!(recorder, "a\u{300}bc\u{300}一二三")?;
853
854 insta::assert_snapshot!(
856 format_colored(|formatter| {
857 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
858 }),
859 @""
860 );
861 insta::assert_snapshot!(
862 format_colored(|formatter| {
863 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
864 }),
865 @"三"
866 );
867 insta::assert_snapshot!(
868 format_colored(|formatter| {
869 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
870 }),
871 @"三"
872 );
873 insta::assert_snapshot!(
874 format_colored(|formatter| {
875 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
876 }),
877 @"一二三"
878 );
879 insta::assert_snapshot!(
880 format_colored(|formatter| {
881 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
882 }),
883 @"c̀一二三"
884 );
885 insta::assert_snapshot!(
886 format_colored(|formatter| {
887 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
888 }),
889 @"àbc̀一二三"
890 );
891 insta::assert_snapshot!(
892 format_colored(|formatter| {
893 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
894 }),
895 @"àbc̀一二三"
896 );
897
898 insta::assert_snapshot!(
900 format_colored(|formatter| {
901 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
902 }),
903 @"à"
904 );
905 insta::assert_snapshot!(
906 format_colored(|formatter| {
907 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
908 }),
909 @"àbc̀"
910 );
911 insta::assert_snapshot!(
912 format_colored(|formatter| {
913 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
914 }),
915 @"àbc̀一"
916 );
917 insta::assert_snapshot!(
918 format_colored(|formatter| {
919 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
920 }),
921 @"àbc̀一二三"
922 );
923 insta::assert_snapshot!(
924 format_colored(|formatter| {
925 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
926 }),
927 @"àbc̀一二三"
928 );
929 Ok(())
930 }
931
932 #[test]
933 fn test_write_truncated_empty_content() {
934 let ellipsis_recorder = FormatRecorder::new(false);
935 let recorder = FormatRecorder::new(false);
936
937 insta::assert_snapshot!(
939 format_colored(|formatter| {
940 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
941 }),
942 @""
943 );
944 insta::assert_snapshot!(
945 format_colored(|formatter| {
946 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
947 }),
948 @""
949 );
950
951 insta::assert_snapshot!(
953 format_colored(|formatter| {
954 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
955 }),
956 @""
957 );
958 insta::assert_snapshot!(
959 format_colored(|formatter| {
960 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
961 }),
962 @""
963 );
964 }
965
966 #[test]
967 fn test_write_truncated_ellipsis_labeled() -> TestResult {
968 let ellipsis_recorder = FormatRecorder::with_data("..");
969 let mut recorder = FormatRecorder::new(false);
970 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
971 recorder.push_label(label);
972 write!(recorder, "{word}")?;
973 recorder.pop_label();
974 }
975
976 insta::assert_snapshot!(
978 format_colored(|formatter| {
979 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
980 }),
981 @"[38;5;1mfoo[38;5;6mbar[39m"
982 );
983 insta::assert_snapshot!(
984 format_colored(|formatter| {
985 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
986 }),
987 @"..[38;5;6mbar[39m"
988 );
989 insta::assert_snapshot!(
990 format_colored(|formatter| {
991 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
992 }),
993 @"..[38;5;6mr[39m"
994 );
995 insta::assert_snapshot!(
996 format_colored(|formatter| {
997 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
998 }),
999 @".."
1000 );
1001 insta::assert_snapshot!(
1002 format_colored(|formatter| {
1003 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1004 }),
1005 @"."
1006 );
1007 insta::assert_snapshot!(
1008 format_colored(|formatter| {
1009 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1010 }),
1011 @""
1012 );
1013
1014 insta::assert_snapshot!(
1016 format_colored(|formatter| {
1017 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
1018 }),
1019 @"[38;5;1mfoo[38;5;6mbar[39m"
1020 );
1021 insta::assert_snapshot!(
1022 format_colored(|formatter| {
1023 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
1024 }),
1025 @"[38;5;1mfoo[39m.."
1026 );
1027 insta::assert_snapshot!(
1028 format_colored(|formatter| {
1029 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
1030 }),
1031 @"[38;5;1mf[39m.."
1032 );
1033 insta::assert_snapshot!(
1034 format_colored(|formatter| {
1035 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
1036 }),
1037 @".."
1038 );
1039 insta::assert_snapshot!(
1040 format_colored(|formatter| {
1041 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1042 }),
1043 @"."
1044 );
1045 insta::assert_snapshot!(
1046 format_colored(|formatter| {
1047 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1048 }),
1049 @""
1050 );
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn test_write_truncated_ellipsis_non_ascii_chars() -> TestResult {
1056 let ellipsis_recorder = FormatRecorder::with_data("..");
1057 let mut recorder = FormatRecorder::new(false);
1058 write!(recorder, "a\u{300}bc\u{300}一二三")?;
1059
1060 insta::assert_snapshot!(
1062 format_colored(|formatter| {
1063 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1064 }),
1065 @"."
1066 );
1067 insta::assert_snapshot!(
1068 format_colored(|formatter| {
1069 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
1070 }),
1071 @".."
1072 );
1073 insta::assert_snapshot!(
1074 format_colored(|formatter| {
1075 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1076 }),
1077 @"..三"
1078 );
1079 insta::assert_snapshot!(
1080 format_colored(|formatter| {
1081 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
1082 }),
1083 @"..二三"
1084 );
1085
1086 insta::assert_snapshot!(
1088 format_colored(|formatter| {
1089 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1090 }),
1091 @"."
1092 );
1093 insta::assert_snapshot!(
1094 format_colored(|formatter| {
1095 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1096 }),
1097 @"àb.."
1098 );
1099 insta::assert_snapshot!(
1100 format_colored(|formatter| {
1101 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
1102 }),
1103 @"àbc̀.."
1104 );
1105 insta::assert_snapshot!(
1106 format_colored(|formatter| {
1107 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
1108 }),
1109 @"àbc̀一二三"
1110 );
1111 insta::assert_snapshot!(
1112 format_colored(|formatter| {
1113 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
1114 }),
1115 @"àbc̀一二三"
1116 );
1117 Ok(())
1118 }
1119
1120 #[test]
1121 fn test_write_truncated_ellipsis_empty_content() {
1122 let ellipsis_recorder = FormatRecorder::with_data("..");
1123 let recorder = FormatRecorder::new(false);
1124
1125 insta::assert_snapshot!(
1127 format_colored(|formatter| {
1128 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1129 }),
1130 @""
1131 );
1132 insta::assert_snapshot!(
1133 format_colored(|formatter| {
1134 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1135 }),
1136 @""
1137 );
1138
1139 insta::assert_snapshot!(
1141 format_colored(|formatter| {
1142 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1143 }),
1144 @""
1145 );
1146 insta::assert_snapshot!(
1147 format_colored(|formatter| {
1148 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1149 }),
1150 @""
1151 );
1152 }
1153
1154 #[test]
1155 fn test_write_padded_labeled_content() -> TestResult {
1156 let mut recorder = FormatRecorder::new(false);
1157 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
1158 recorder.push_label(label);
1159 write!(recorder, "{word}")?;
1160 recorder.pop_label();
1161 }
1162 let fill = FormatRecorder::with_data("=");
1163
1164 insta::assert_snapshot!(
1166 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 6)),
1167 @"[38;5;1mfoo[38;5;6mbar[39m"
1168 );
1169 insta::assert_snapshot!(
1170 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 7)),
1171 @"=[38;5;1mfoo[38;5;6mbar[39m"
1172 );
1173 insta::assert_snapshot!(
1174 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 8)),
1175 @"==[38;5;1mfoo[38;5;6mbar[39m"
1176 );
1177
1178 insta::assert_snapshot!(
1180 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1181 @"[38;5;1mfoo[38;5;6mbar[39m"
1182 );
1183 insta::assert_snapshot!(
1184 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 7)),
1185 @"[38;5;1mfoo[38;5;6mbar[39m="
1186 );
1187 insta::assert_snapshot!(
1188 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 8)),
1189 @"[38;5;1mfoo[38;5;6mbar[39m=="
1190 );
1191
1192 insta::assert_snapshot!(
1194 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1195 @"[38;5;1mfoo[38;5;6mbar[39m"
1196 );
1197 insta::assert_snapshot!(
1198 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 7)),
1199 @"[38;5;1mfoo[38;5;6mbar[39m="
1200 );
1201 insta::assert_snapshot!(
1202 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 8)),
1203 @"=[38;5;1mfoo[38;5;6mbar[39m="
1204 );
1205 insta::assert_snapshot!(
1206 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1207 @"===[38;5;1mfoo[38;5;6mbar[39m===="
1208 );
1209 Ok(())
1210 }
1211
1212 #[test]
1213 fn test_write_padded_labeled_fill_char() -> TestResult {
1214 let recorder = FormatRecorder::with_data("foo");
1215 let mut fill = FormatRecorder::new(false);
1216 fill.push_label("red");
1217 write!(fill, "=")?;
1218 fill.pop_label();
1219
1220 insta::assert_snapshot!(
1222 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 5)),
1223 @"[38;5;1m==[39mfoo"
1224 );
1225
1226 insta::assert_snapshot!(
1228 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1229 @"foo[38;5;1m===[39m"
1230 );
1231
1232 insta::assert_snapshot!(
1234 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1235 @"[38;5;1m=[39mfoo[38;5;1m==[39m"
1236 );
1237 Ok(())
1238 }
1239
1240 #[test]
1241 fn test_write_padded_non_ascii_chars() {
1242 let recorder = FormatRecorder::with_data("a\u{300}bc\u{300}一二三");
1243 let fill = FormatRecorder::with_data("=");
1244
1245 insta::assert_snapshot!(
1247 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 9)),
1248 @"àbc̀一二三"
1249 );
1250 insta::assert_snapshot!(
1251 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 10)),
1252 @"=àbc̀一二三"
1253 );
1254
1255 insta::assert_snapshot!(
1257 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 9)),
1258 @"àbc̀一二三"
1259 );
1260 insta::assert_snapshot!(
1261 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 10)),
1262 @"àbc̀一二三="
1263 );
1264
1265 insta::assert_snapshot!(
1267 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 9)),
1268 @"àbc̀一二三"
1269 );
1270 insta::assert_snapshot!(
1271 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 10)),
1272 @"àbc̀一二三="
1273 );
1274 insta::assert_snapshot!(
1275 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1276 @"==àbc̀一二三=="
1277 );
1278 }
1279
1280 #[test]
1281 fn test_write_padded_empty_content() {
1282 let recorder = FormatRecorder::new(false);
1283 let fill = FormatRecorder::with_data("=");
1284
1285 insta::assert_snapshot!(
1287 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 0)),
1288 @""
1289 );
1290 insta::assert_snapshot!(
1291 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 1)),
1292 @"="
1293 );
1294
1295 insta::assert_snapshot!(
1297 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 0)),
1298 @""
1299 );
1300 insta::assert_snapshot!(
1301 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 1)),
1302 @"="
1303 );
1304
1305 insta::assert_snapshot!(
1307 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 0)),
1308 @""
1309 );
1310 insta::assert_snapshot!(
1311 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 1)),
1312 @"="
1313 );
1314 }
1315
1316 #[test]
1317 fn test_split_byte_line_to_words() {
1318 assert_eq!(split_byte_line_to_words(b""), vec![]);
1319 assert_eq!(
1320 split_byte_line_to_words(b"foo"),
1321 vec![ByteFragment {
1322 word: b"foo",
1323 whitespace_len: 0,
1324 word_width: 3
1325 }],
1326 );
1327 assert_eq!(
1328 split_byte_line_to_words(b" foo"),
1329 vec![
1330 ByteFragment {
1331 word: b"",
1332 whitespace_len: 2,
1333 word_width: 0
1334 },
1335 ByteFragment {
1336 word: b"foo",
1337 whitespace_len: 0,
1338 word_width: 3
1339 },
1340 ],
1341 );
1342 assert_eq!(
1343 split_byte_line_to_words(b"foo "),
1344 vec![ByteFragment {
1345 word: b"foo",
1346 whitespace_len: 2,
1347 word_width: 3
1348 }],
1349 );
1350 assert_eq!(
1351 split_byte_line_to_words(b"a b foo bar "),
1352 vec![
1353 ByteFragment {
1354 word: b"a",
1355 whitespace_len: 1,
1356 word_width: 1
1357 },
1358 ByteFragment {
1359 word: b"b",
1360 whitespace_len: 2,
1361 word_width: 1
1362 },
1363 ByteFragment {
1364 word: b"foo",
1365 whitespace_len: 1,
1366 word_width: 3,
1367 },
1368 ByteFragment {
1369 word: b"bar",
1370 whitespace_len: 1,
1371 word_width: 3,
1372 },
1373 ],
1374 );
1375 }
1376
1377 #[test]
1378 fn test_write_indented() -> TestResult {
1379 let write_prefix = |formatter: &mut dyn Formatter| {
1380 formatter.write_all(b">>")?;
1381 Ok(())
1382 };
1383
1384 let recorder = FormatRecorder::new(true);
1386 insta::assert_snapshot!(
1387 format_colored(
1388 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1389 ),
1390 @""
1391 );
1392 let recorder = FormatRecorder::with_data("abc");
1393 insta::assert_snapshot!(
1394 format_colored(
1395 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1396 ),
1397 @">>abc"
1398 );
1399
1400 let recorder = FormatRecorder::with_data("a\nb\nc");
1402 insta::assert_snapshot!(
1403 format_colored(
1404 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1405 ),
1406 @"
1407 >>a
1408 >>b
1409 >>c
1410 "
1411 );
1412
1413 let recorder = FormatRecorder::with_data("\na\n\nb\n\nc\n");
1416 assert_eq!(
1417 format_colored(
1418 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1419 ),
1420 "\n>>a\n\n>>b\n\n>>c\n"
1421 );
1422
1423 let mut recorder = FormatRecorder::new(true);
1425 for (label, word) in [("red", "foo"), ("cyan", "bar\nbaz\n\nquux")] {
1426 recorder.push_label(label);
1427 write!(recorder, "{word}")?;
1428 recorder.pop_label();
1429 writeln!(recorder)?;
1430 }
1431 insta::assert_snapshot!(
1432 format_colored(
1433 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1434 ),
1435 @"
1436 [38;5;1m>>foo[39m
1437 [38;5;6m>>bar[39m
1438 [38;5;6m>>baz[39m
1439 [38;5;6m[39m
1440 [38;5;6m>>quux[39m
1441 "
1442 );
1443 Ok(())
1444 }
1445
1446 #[test]
1447 fn test_wrap_bytes() {
1448 assert_eq!(wrap_bytes(b"foo", 10), [b"foo".as_ref()]);
1449 assert_eq!(wrap_bytes(b"foo bar", 10), [b"foo bar".as_ref()]);
1450 assert_eq!(
1451 wrap_bytes(b"foo bar baz", 10),
1452 [b"foo bar".as_ref(), b"baz".as_ref()],
1453 );
1454
1455 assert_eq!(wrap_bytes(b"", 10), [b"".as_ref()]);
1457 assert_eq!(wrap_bytes(b" ", 10), [b"".as_ref()]);
1458
1459 assert_eq!(
1461 wrap_bytes(b"foo bar baz", 8),
1462 [b"foo bar".as_ref(), b"baz".as_ref()],
1463 );
1464 assert_eq!(
1465 wrap_bytes(b"foo bar x", 7),
1466 [b"foo".as_ref(), b"bar x".as_ref()],
1467 );
1468 assert_eq!(
1469 wrap_bytes(b"foo bar \nx", 7),
1470 [b"foo bar".as_ref(), b"x".as_ref()],
1471 );
1472 assert_eq!(
1473 wrap_bytes(b"foo bar\n x", 7),
1474 [b"foo bar".as_ref(), b" x".as_ref()],
1475 );
1476 assert_eq!(
1477 wrap_bytes(b"foo bar x", 4),
1478 [b"foo".as_ref(), b"bar".as_ref(), b"x".as_ref()],
1479 );
1480
1481 assert_eq!(wrap_bytes(b"foo\n", 10), [b"foo".as_ref(), b"".as_ref()]);
1483 assert_eq!(wrap_bytes(b"foo\n", 3), [b"foo".as_ref(), b"".as_ref()]);
1484 assert_eq!(wrap_bytes(b"\n", 10), [b"".as_ref(), b"".as_ref()]);
1485
1486 assert_eq!(wrap_bytes(b"foo x", 2), [b"foo".as_ref(), b"x".as_ref()]);
1488 assert_eq!(wrap_bytes(b"x y", 0), [b"x".as_ref(), b"y".as_ref()]);
1489
1490 assert_eq!(wrap_bytes(b"foo\x80", 10), [b"foo\x80".as_ref()]);
1492 }
1493
1494 #[test]
1495 fn test_wrap_bytes_slice_ptr() {
1496 let text = b"\nfoo\n\nbar baz\n";
1497 let lines = wrap_bytes(text, 10);
1498 assert_eq!(
1499 lines,
1500 [
1501 b"".as_ref(),
1502 b"foo".as_ref(),
1503 b"".as_ref(),
1504 b"bar baz".as_ref(),
1505 b"".as_ref()
1506 ],
1507 );
1508 assert_eq!(lines[0].as_ptr(), text[0..].as_ptr());
1510 assert_eq!(lines[1].as_ptr(), text[1..].as_ptr());
1511 assert_eq!(lines[2].as_ptr(), text[5..].as_ptr());
1512 assert_eq!(lines[3].as_ptr(), text[6..].as_ptr());
1513 assert_eq!(lines[4].as_ptr(), text[14..].as_ptr());
1514 }
1515
1516 #[test]
1517 fn test_write_wrapped() -> TestResult {
1518 let mut recorder = FormatRecorder::new(false);
1520 recorder.push_label("red");
1521 write!(recorder, "foo bar baz\nqux quux\n")?;
1522 recorder.pop_label();
1523 insta::assert_snapshot!(
1524 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1525 @"
1526 [38;5;1mfoo bar[39m
1527 [38;5;1mbaz[39m
1528 [38;5;1mqux[39m
1529 [38;5;1mquux[39m
1530 "
1531 );
1532
1533 let mut recorder = FormatRecorder::new(false);
1535 for (i, word) in ["foo ", "bar ", "baz\n", "qux ", "quux"].iter().enumerate() {
1536 recorder.push_label(["red", "cyan"][i & 1]);
1537 write!(recorder, "{word}")?;
1538 recorder.pop_label();
1539 }
1540 insta::assert_snapshot!(
1541 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1542 @"
1543 [38;5;1mfoo [38;5;6mbar[39m
1544 [38;5;1mbaz[39m
1545 [38;5;6mqux[39m
1546 [38;5;1mquux[39m
1547 "
1548 );
1549
1550 let mut recorder = FormatRecorder::new(false);
1552 for (i, word) in ["", "foo", "", "bar baz", ""].iter().enumerate() {
1553 recorder.push_label(["red", "cyan"][i & 1]);
1554 writeln!(recorder, "{word}")?;
1555 recorder.pop_label();
1556 }
1557 insta::assert_snapshot!(
1558 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1559 @"
1560 [38;5;1m[39m
1561 [38;5;6mfoo[39m
1562 [38;5;1m[39m
1563 [38;5;6mbar baz[39m
1564 [38;5;1m[39m
1565 "
1566 );
1567
1568 let mut recorder = FormatRecorder::new(false);
1570 recorder.push_label("red");
1571 write!(recorder, "foo bar")?;
1572 recorder.pop_label();
1573 write!(recorder, " ")?;
1574 recorder.push_label("cyan");
1575 writeln!(recorder, "baz")?;
1576 recorder.pop_label();
1577 insta::assert_snapshot!(
1578 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1579 @"
1580 [38;5;1mfoo bar[39m
1581 [38;5;6mbaz[39m
1582 "
1583 );
1584
1585 let mut recorder = FormatRecorder::new(false);
1587 recorder.push_label("red");
1588 write!(recorder, "foo bar ba")?;
1589 recorder.pop_label();
1590 recorder.push_label("cyan");
1591 writeln!(recorder, "z")?;
1592 recorder.pop_label();
1593 insta::assert_snapshot!(
1594 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1595 @"
1596 [38;5;1mfoo bar[39m
1597 [38;5;1mba[38;5;6mz[39m
1598 "
1599 );
1600 Ok(())
1601 }
1602
1603 #[test]
1604 fn test_write_wrapped_leading_labeled_whitespace() -> TestResult {
1605 let mut recorder = FormatRecorder::new(false);
1606 recorder.push_label("red");
1607 write!(recorder, " ")?;
1608 recorder.pop_label();
1609 write!(recorder, "foo")?;
1610 insta::assert_snapshot!(
1611 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1612 @"[38;5;1m [39mfoo"
1613 );
1614 Ok(())
1615 }
1616
1617 #[test]
1618 fn test_write_wrapped_trailing_labeled_whitespace() -> TestResult {
1619 let mut recorder = FormatRecorder::new(false);
1622 write!(recorder, "foo")?;
1623 recorder.push_label("red");
1624 write!(recorder, " ")?;
1625 recorder.pop_label();
1626 assert_eq!(
1627 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1628 "foo",
1629 );
1630
1631 let mut recorder = FormatRecorder::new(false);
1634 write!(recorder, "foo")?;
1635 recorder.push_label("red");
1636 writeln!(recorder)?;
1637 recorder.pop_label();
1638 assert_eq!(
1639 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1640 "foo\n",
1641 );
1642
1643 let mut recorder = FormatRecorder::new(false);
1646 writeln!(recorder, "foo")?;
1647 recorder.push_label("red");
1648 write!(recorder, " ")?;
1649 recorder.pop_label();
1650 assert_eq!(
1651 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1652 "foo\n",
1653 );
1654 Ok(())
1655 }
1656
1657 #[test]
1658 fn test_parse_author() {
1659 let expected_name = "Example";
1660 let expected_email = "example@example.com";
1661 let parsed = parse_author(&format!("{expected_name} <{expected_email}>")).unwrap();
1662 assert_eq!(
1663 (expected_name.to_string(), expected_email.to_string()),
1664 parsed
1665 );
1666 }
1667
1668 #[test]
1669 fn test_parse_author_with_utf8() {
1670 let expected_name = "Ąćęłńóśżź";
1671 let expected_email = "example@example.com";
1672 let parsed = parse_author(&format!("{expected_name} <{expected_email}>")).unwrap();
1673 assert_eq!(
1674 (expected_name.to_string(), expected_email.to_string()),
1675 parsed
1676 );
1677 }
1678
1679 #[test]
1680 fn test_parse_author_without_name() {
1681 let expected_email = "example@example.com";
1682 let parsed = parse_author(&format!("<{expected_email}>")).unwrap();
1683 assert_eq!(("".to_string(), expected_email.to_string()), parsed);
1684 }
1685}