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
559 use super::*;
560 use crate::formatter::ColorFormatter;
561 use crate::formatter::PlainTextFormatter;
562
563 fn format_colored(write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>) -> String {
564 let mut config = StackedConfig::empty();
565 config.add_layer(
566 ConfigLayer::parse(
567 ConfigSource::Default,
568 indoc! {"
569 colors.cyan = 'cyan'
570 colors.red = 'red'
571 "},
572 )
573 .unwrap(),
574 );
575 let mut output = Vec::new();
576 let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
577 write(&mut formatter).unwrap();
578 drop(formatter);
579 String::from_utf8(output).unwrap()
580 }
581
582 fn format_plain_text(write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>) -> String {
583 let mut output = Vec::new();
584 let mut formatter = PlainTextFormatter::new(&mut output);
585 write(&mut formatter).unwrap();
586 String::from_utf8(output).unwrap()
587 }
588
589 #[test]
590 fn test_complete_newline() {
591 assert_eq!(complete_newline(""), "");
592 assert_eq!(complete_newline(" "), " \n");
593 assert_eq!(complete_newline("\n "), "\n \n");
594 assert_eq!(complete_newline("\t"), "\t\n");
595 assert_eq!(complete_newline("\n"), "\n");
596 assert_eq!(complete_newline("\n\n"), "\n\n");
597 assert_eq!(complete_newline("a\nb\nc"), "a\nb\nc\n");
598 assert_eq!(complete_newline("a\nb\nc\n"), "a\nb\nc\n");
599 }
600
601 #[test]
602 fn test_split_email() {
603 assert_eq!(split_email(""), ("", None));
604 assert_eq!(split_email("abc"), ("abc", None));
605 assert_eq!(split_email("example.com"), ("example.com", None));
606 assert_eq!(split_email("@example.com"), ("", Some("example.com")));
607 assert_eq!(
608 split_email("user@example.com"),
609 ("user", Some("example.com"))
610 );
611 assert_eq!(
612 split_email("user+tag@example.com"),
613 ("user+tag", Some("example.com"))
614 );
615 assert_eq!(
616 split_email(" user @ example.com "),
617 (" user ", Some(" example.com "))
618 );
619 assert_eq!(
620 split_email("user@extra@example.com"),
621 ("user", Some("extra@example.com"))
622 );
623 }
624
625 #[test]
626 fn test_elide_start() {
627 assert_eq!(elide_start("", "", 1), ("".into(), 0));
629
630 assert_eq!(elide_start("abcdef", "", 6), ("abcdef".into(), 6));
632 assert_eq!(elide_start("abcdef", "", 5), ("bcdef".into(), 5));
633 assert_eq!(elide_start("abcdef", "", 1), ("f".into(), 1));
634 assert_eq!(elide_start("abcdef", "", 0), ("".into(), 0));
635 assert_eq!(elide_start("abcdef", "-=~", 6), ("abcdef".into(), 6));
636 assert_eq!(elide_start("abcdef", "-=~", 5), ("-=~ef".into(), 5));
637 assert_eq!(elide_start("abcdef", "-=~", 4), ("-=~f".into(), 4));
638 assert_eq!(elide_start("abcdef", "-=~", 3), ("-=~".into(), 3));
639 assert_eq!(elide_start("abcdef", "-=~", 2), ("=~".into(), 2));
640 assert_eq!(elide_start("abcdef", "-=~", 1), ("~".into(), 1));
641 assert_eq!(elide_start("abcdef", "-=~", 0), ("".into(), 0));
642
643 assert_eq!(elide_start("一二三", "", 6), ("一二三".into(), 6));
645 assert_eq!(elide_start("一二三", "", 5), ("二三".into(), 4));
646 assert_eq!(elide_start("一二三", "", 4), ("二三".into(), 4));
647 assert_eq!(elide_start("一二三", "", 1), ("".into(), 0));
648 assert_eq!(elide_start("一二三", "-=~", 6), ("一二三".into(), 6));
649 assert_eq!(elide_start("一二三", "-=~", 5), ("-=~三".into(), 5));
650 assert_eq!(elide_start("一二三", "-=~", 4), ("-=~".into(), 3));
651 assert_eq!(elide_start("一二三", "略", 6), ("一二三".into(), 6));
652 assert_eq!(elide_start("一二三", "略", 5), ("略三".into(), 4));
653 assert_eq!(elide_start("一二三", "略", 4), ("略三".into(), 4));
654 assert_eq!(elide_start("一二三", "略", 2), ("略".into(), 2));
655 assert_eq!(elide_start("一二三", "略", 1), ("".into(), 0));
656 assert_eq!(elide_start("一二三", ".", 5), (".二三".into(), 5));
657 assert_eq!(elide_start("一二三", ".", 4), (".三".into(), 3));
658 assert_eq!(elide_start("一二三", "略.", 5), ("略.三".into(), 5));
659 assert_eq!(elide_start("一二三", "略.", 4), ("略.".into(), 3));
660
661 assert_eq!(elide_start("àbcdè", "", 5), ("àbcdè".into(), 5));
663 assert_eq!(elide_start("àbcdè", "", 4), ("bcdè".into(), 4));
664 assert_eq!(elide_start("àbcdè", "", 1), ("è".into(), 1));
665 assert_eq!(elide_start("àbcdè", "", 0), ("".into(), 0));
666 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 4), ("ÀÇÈè".into(), 4));
667 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
668 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 2), ("ÇÈ".into(), 2));
669
670 assert_eq!(
672 elide_start("a\u{300}bcde\u{300}", "", 5),
673 ("a\u{300}bcde\u{300}".into(), 5)
674 );
675 assert_eq!(
676 elide_start("a\u{300}bcde\u{300}", "", 4),
677 ("bcde\u{300}".into(), 4)
678 );
679 assert_eq!(
680 elide_start("a\u{300}bcde\u{300}", "", 1),
681 ("e\u{300}".into(), 1)
682 );
683 assert_eq!(elide_start("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
684 assert_eq!(
685 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
686 ("A\u{300}CE\u{300}e\u{300}".into(), 4)
687 );
688 assert_eq!(
689 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
690 ("A\u{300}CE\u{300}".into(), 3)
691 );
692 assert_eq!(
693 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
694 ("CE\u{300}".into(), 2)
695 );
696 }
697
698 #[test]
699 fn test_elide_end() {
700 assert_eq!(elide_end("", "", 1), ("".into(), 0));
702
703 assert_eq!(elide_end("abcdef", "", 6), ("abcdef".into(), 6));
705 assert_eq!(elide_end("abcdef", "", 5), ("abcde".into(), 5));
706 assert_eq!(elide_end("abcdef", "", 1), ("a".into(), 1));
707 assert_eq!(elide_end("abcdef", "", 0), ("".into(), 0));
708 assert_eq!(elide_end("abcdef", "-=~", 6), ("abcdef".into(), 6));
709 assert_eq!(elide_end("abcdef", "-=~", 5), ("ab-=~".into(), 5));
710 assert_eq!(elide_end("abcdef", "-=~", 4), ("a-=~".into(), 4));
711 assert_eq!(elide_end("abcdef", "-=~", 3), ("-=~".into(), 3));
712 assert_eq!(elide_end("abcdef", "-=~", 2), ("-=".into(), 2));
713 assert_eq!(elide_end("abcdef", "-=~", 1), ("-".into(), 1));
714 assert_eq!(elide_end("abcdef", "-=~", 0), ("".into(), 0));
715
716 assert_eq!(elide_end("一二三", "", 6), ("一二三".into(), 6));
718 assert_eq!(elide_end("一二三", "", 5), ("一二".into(), 4));
719 assert_eq!(elide_end("一二三", "", 4), ("一二".into(), 4));
720 assert_eq!(elide_end("一二三", "", 1), ("".into(), 0));
721 assert_eq!(elide_end("一二三", "-=~", 6), ("一二三".into(), 6));
722 assert_eq!(elide_end("一二三", "-=~", 5), ("一-=~".into(), 5));
723 assert_eq!(elide_end("一二三", "-=~", 4), ("-=~".into(), 3));
724 assert_eq!(elide_end("一二三", "略", 6), ("一二三".into(), 6));
725 assert_eq!(elide_end("一二三", "略", 5), ("一略".into(), 4));
726 assert_eq!(elide_end("一二三", "略", 4), ("一略".into(), 4));
727 assert_eq!(elide_end("一二三", "略", 2), ("略".into(), 2));
728 assert_eq!(elide_end("一二三", "略", 1), ("".into(), 0));
729 assert_eq!(elide_end("一二三", ".", 5), ("一二.".into(), 5));
730 assert_eq!(elide_end("一二三", ".", 4), ("一.".into(), 3));
731 assert_eq!(elide_end("一二三", "略.", 5), ("一略.".into(), 5));
732 assert_eq!(elide_end("一二三", "略.", 4), ("略.".into(), 3));
733
734 assert_eq!(elide_end("àbcdè", "", 5), ("àbcdè".into(), 5));
736 assert_eq!(elide_end("àbcdè", "", 4), ("àbcd".into(), 4));
737 assert_eq!(elide_end("àbcdè", "", 1), ("à".into(), 1));
738 assert_eq!(elide_end("àbcdè", "", 0), ("".into(), 0));
739 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 4), ("àÀÇÈ".into(), 4));
740 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
741 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 2), ("ÀÇ".into(), 2));
742
743 assert_eq!(
745 elide_end("a\u{300}bcde\u{300}", "", 5),
746 ("a\u{300}bcde\u{300}".into(), 5)
747 );
748 assert_eq!(
749 elide_end("a\u{300}bcde\u{300}", "", 4),
750 ("a\u{300}bcd".into(), 4)
751 );
752 assert_eq!(
753 elide_end("a\u{300}bcde\u{300}", "", 1),
754 ("a\u{300}".into(), 1)
755 );
756 assert_eq!(elide_end("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
757 assert_eq!(
758 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
759 ("a\u{300}A\u{300}CE\u{300}".into(), 4)
760 );
761 assert_eq!(
762 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
763 ("A\u{300}CE\u{300}".into(), 3)
764 );
765 assert_eq!(
766 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
767 ("A\u{300}C".into(), 2)
768 );
769 }
770
771 #[test]
772 fn test_write_truncated_labeled() {
773 let ellipsis_recorder = FormatRecorder::new(false);
774 let mut recorder = FormatRecorder::new(false);
775 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
776 recorder.push_label(label);
777 write!(recorder, "{word}").unwrap();
778 recorder.pop_label();
779 }
780
781 insta::assert_snapshot!(
783 format_colored(|formatter| {
784 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
785 }),
786 @"[38;5;1mfoo[38;5;6mbar[39m"
787 );
788 insta::assert_snapshot!(
789 format_colored(|formatter| {
790 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
791 }),
792 @"[38;5;1moo[38;5;6mbar[39m"
793 );
794 insta::assert_snapshot!(
795 format_colored(|formatter| {
796 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
797 }),
798 @"[38;5;6mbar[39m"
799 );
800 insta::assert_snapshot!(
801 format_colored(|formatter| {
802 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
803 }),
804 @"[38;5;6mar[39m"
805 );
806 insta::assert_snapshot!(
807 format_colored(|formatter| {
808 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
809 }),
810 @""
811 );
812
813 insta::assert_snapshot!(
815 format_colored(|formatter| {
816 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
817 }),
818 @"[38;5;1mfoo[38;5;6mbar[39m"
819 );
820 insta::assert_snapshot!(
821 format_colored(|formatter| {
822 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
823 }),
824 @"[38;5;1mfoo[38;5;6mba[39m"
825 );
826 insta::assert_snapshot!(
827 format_colored(|formatter| {
828 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
829 }),
830 @"[38;5;1mfoo[39m"
831 );
832 insta::assert_snapshot!(
833 format_colored(|formatter| {
834 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
835 }),
836 @"[38;5;1mfo[39m"
837 );
838 insta::assert_snapshot!(
839 format_colored(|formatter| {
840 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
841 }),
842 @""
843 );
844 }
845
846 #[test]
847 fn test_write_truncated_non_ascii_chars() {
848 let ellipsis_recorder = FormatRecorder::new(false);
849 let mut recorder = FormatRecorder::new(false);
850 write!(recorder, "a\u{300}bc\u{300}一二三").unwrap();
851
852 insta::assert_snapshot!(
854 format_colored(|formatter| {
855 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
856 }),
857 @""
858 );
859 insta::assert_snapshot!(
860 format_colored(|formatter| {
861 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
862 }),
863 @"三"
864 );
865 insta::assert_snapshot!(
866 format_colored(|formatter| {
867 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
868 }),
869 @"三"
870 );
871 insta::assert_snapshot!(
872 format_colored(|formatter| {
873 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
874 }),
875 @"一二三"
876 );
877 insta::assert_snapshot!(
878 format_colored(|formatter| {
879 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
880 }),
881 @"c̀一二三"
882 );
883 insta::assert_snapshot!(
884 format_colored(|formatter| {
885 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
886 }),
887 @"àbc̀一二三"
888 );
889 insta::assert_snapshot!(
890 format_colored(|formatter| {
891 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
892 }),
893 @"àbc̀一二三"
894 );
895
896 insta::assert_snapshot!(
898 format_colored(|formatter| {
899 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
900 }),
901 @"à"
902 );
903 insta::assert_snapshot!(
904 format_colored(|formatter| {
905 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
906 }),
907 @"àbc̀"
908 );
909 insta::assert_snapshot!(
910 format_colored(|formatter| {
911 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
912 }),
913 @"àbc̀一"
914 );
915 insta::assert_snapshot!(
916 format_colored(|formatter| {
917 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
918 }),
919 @"àbc̀一二三"
920 );
921 insta::assert_snapshot!(
922 format_colored(|formatter| {
923 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
924 }),
925 @"àbc̀一二三"
926 );
927 }
928
929 #[test]
930 fn test_write_truncated_empty_content() {
931 let ellipsis_recorder = FormatRecorder::new(false);
932 let recorder = FormatRecorder::new(false);
933
934 insta::assert_snapshot!(
936 format_colored(|formatter| {
937 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
938 }),
939 @""
940 );
941 insta::assert_snapshot!(
942 format_colored(|formatter| {
943 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
944 }),
945 @""
946 );
947
948 insta::assert_snapshot!(
950 format_colored(|formatter| {
951 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
952 }),
953 @""
954 );
955 insta::assert_snapshot!(
956 format_colored(|formatter| {
957 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
958 }),
959 @""
960 );
961 }
962
963 #[test]
964 fn test_write_truncated_ellipsis_labeled() {
965 let ellipsis_recorder = FormatRecorder::with_data("..");
966 let mut recorder = FormatRecorder::new(false);
967 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
968 recorder.push_label(label);
969 write!(recorder, "{word}").unwrap();
970 recorder.pop_label();
971 }
972
973 insta::assert_snapshot!(
975 format_colored(|formatter| {
976 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
977 }),
978 @"[38;5;1mfoo[38;5;6mbar[39m"
979 );
980 insta::assert_snapshot!(
981 format_colored(|formatter| {
982 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
983 }),
984 @"..[38;5;6mbar[39m"
985 );
986 insta::assert_snapshot!(
987 format_colored(|formatter| {
988 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
989 }),
990 @"..[38;5;6mr[39m"
991 );
992 insta::assert_snapshot!(
993 format_colored(|formatter| {
994 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
995 }),
996 @".."
997 );
998 insta::assert_snapshot!(
999 format_colored(|formatter| {
1000 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1001 }),
1002 @"."
1003 );
1004 insta::assert_snapshot!(
1005 format_colored(|formatter| {
1006 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1007 }),
1008 @""
1009 );
1010
1011 insta::assert_snapshot!(
1013 format_colored(|formatter| {
1014 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
1015 }),
1016 @"[38;5;1mfoo[38;5;6mbar[39m"
1017 );
1018 insta::assert_snapshot!(
1019 format_colored(|formatter| {
1020 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
1021 }),
1022 @"[38;5;1mfoo[39m.."
1023 );
1024 insta::assert_snapshot!(
1025 format_colored(|formatter| {
1026 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
1027 }),
1028 @"[38;5;1mf[39m.."
1029 );
1030 insta::assert_snapshot!(
1031 format_colored(|formatter| {
1032 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
1033 }),
1034 @".."
1035 );
1036 insta::assert_snapshot!(
1037 format_colored(|formatter| {
1038 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1039 }),
1040 @"."
1041 );
1042 insta::assert_snapshot!(
1043 format_colored(|formatter| {
1044 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1045 }),
1046 @""
1047 );
1048 }
1049
1050 #[test]
1051 fn test_write_truncated_ellipsis_non_ascii_chars() {
1052 let ellipsis_recorder = FormatRecorder::with_data("..");
1053 let mut recorder = FormatRecorder::new(false);
1054 write!(recorder, "a\u{300}bc\u{300}一二三").unwrap();
1055
1056 insta::assert_snapshot!(
1058 format_colored(|formatter| {
1059 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1060 }),
1061 @"."
1062 );
1063 insta::assert_snapshot!(
1064 format_colored(|formatter| {
1065 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
1066 }),
1067 @".."
1068 );
1069 insta::assert_snapshot!(
1070 format_colored(|formatter| {
1071 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1072 }),
1073 @"..三"
1074 );
1075 insta::assert_snapshot!(
1076 format_colored(|formatter| {
1077 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
1078 }),
1079 @"..二三"
1080 );
1081
1082 insta::assert_snapshot!(
1084 format_colored(|formatter| {
1085 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1086 }),
1087 @"."
1088 );
1089 insta::assert_snapshot!(
1090 format_colored(|formatter| {
1091 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1092 }),
1093 @"àb.."
1094 );
1095 insta::assert_snapshot!(
1096 format_colored(|formatter| {
1097 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
1098 }),
1099 @"àbc̀.."
1100 );
1101 insta::assert_snapshot!(
1102 format_colored(|formatter| {
1103 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
1104 }),
1105 @"àbc̀一二三"
1106 );
1107 insta::assert_snapshot!(
1108 format_colored(|formatter| {
1109 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
1110 }),
1111 @"àbc̀一二三"
1112 );
1113 }
1114
1115 #[test]
1116 fn test_write_truncated_ellipsis_empty_content() {
1117 let ellipsis_recorder = FormatRecorder::with_data("..");
1118 let recorder = FormatRecorder::new(false);
1119
1120 insta::assert_snapshot!(
1122 format_colored(|formatter| {
1123 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1124 }),
1125 @""
1126 );
1127 insta::assert_snapshot!(
1128 format_colored(|formatter| {
1129 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1130 }),
1131 @""
1132 );
1133
1134 insta::assert_snapshot!(
1136 format_colored(|formatter| {
1137 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1138 }),
1139 @""
1140 );
1141 insta::assert_snapshot!(
1142 format_colored(|formatter| {
1143 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1144 }),
1145 @""
1146 );
1147 }
1148
1149 #[test]
1150 fn test_write_padded_labeled_content() {
1151 let mut recorder = FormatRecorder::new(false);
1152 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
1153 recorder.push_label(label);
1154 write!(recorder, "{word}").unwrap();
1155 recorder.pop_label();
1156 }
1157 let fill = FormatRecorder::with_data("=");
1158
1159 insta::assert_snapshot!(
1161 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 6)),
1162 @"[38;5;1mfoo[38;5;6mbar[39m"
1163 );
1164 insta::assert_snapshot!(
1165 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 7)),
1166 @"=[38;5;1mfoo[38;5;6mbar[39m"
1167 );
1168 insta::assert_snapshot!(
1169 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 8)),
1170 @"==[38;5;1mfoo[38;5;6mbar[39m"
1171 );
1172
1173 insta::assert_snapshot!(
1175 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1176 @"[38;5;1mfoo[38;5;6mbar[39m"
1177 );
1178 insta::assert_snapshot!(
1179 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 7)),
1180 @"[38;5;1mfoo[38;5;6mbar[39m="
1181 );
1182 insta::assert_snapshot!(
1183 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 8)),
1184 @"[38;5;1mfoo[38;5;6mbar[39m=="
1185 );
1186
1187 insta::assert_snapshot!(
1189 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1190 @"[38;5;1mfoo[38;5;6mbar[39m"
1191 );
1192 insta::assert_snapshot!(
1193 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 7)),
1194 @"[38;5;1mfoo[38;5;6mbar[39m="
1195 );
1196 insta::assert_snapshot!(
1197 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 8)),
1198 @"=[38;5;1mfoo[38;5;6mbar[39m="
1199 );
1200 insta::assert_snapshot!(
1201 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1202 @"===[38;5;1mfoo[38;5;6mbar[39m===="
1203 );
1204 }
1205
1206 #[test]
1207 fn test_write_padded_labeled_fill_char() {
1208 let recorder = FormatRecorder::with_data("foo");
1209 let mut fill = FormatRecorder::new(false);
1210 fill.push_label("red");
1211 write!(fill, "=").unwrap();
1212 fill.pop_label();
1213
1214 insta::assert_snapshot!(
1216 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 5)),
1217 @"[38;5;1m==[39mfoo"
1218 );
1219
1220 insta::assert_snapshot!(
1222 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1223 @"foo[38;5;1m===[39m"
1224 );
1225
1226 insta::assert_snapshot!(
1228 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1229 @"[38;5;1m=[39mfoo[38;5;1m==[39m"
1230 );
1231 }
1232
1233 #[test]
1234 fn test_write_padded_non_ascii_chars() {
1235 let recorder = FormatRecorder::with_data("a\u{300}bc\u{300}一二三");
1236 let fill = FormatRecorder::with_data("=");
1237
1238 insta::assert_snapshot!(
1240 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 9)),
1241 @"àbc̀一二三"
1242 );
1243 insta::assert_snapshot!(
1244 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 10)),
1245 @"=àbc̀一二三"
1246 );
1247
1248 insta::assert_snapshot!(
1250 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 9)),
1251 @"àbc̀一二三"
1252 );
1253 insta::assert_snapshot!(
1254 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 10)),
1255 @"àbc̀一二三="
1256 );
1257
1258 insta::assert_snapshot!(
1260 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 9)),
1261 @"àbc̀一二三"
1262 );
1263 insta::assert_snapshot!(
1264 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 10)),
1265 @"àbc̀一二三="
1266 );
1267 insta::assert_snapshot!(
1268 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1269 @"==àbc̀一二三=="
1270 );
1271 }
1272
1273 #[test]
1274 fn test_write_padded_empty_content() {
1275 let recorder = FormatRecorder::new(false);
1276 let fill = FormatRecorder::with_data("=");
1277
1278 insta::assert_snapshot!(
1280 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 0)),
1281 @""
1282 );
1283 insta::assert_snapshot!(
1284 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 1)),
1285 @"="
1286 );
1287
1288 insta::assert_snapshot!(
1290 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 0)),
1291 @""
1292 );
1293 insta::assert_snapshot!(
1294 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 1)),
1295 @"="
1296 );
1297
1298 insta::assert_snapshot!(
1300 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 0)),
1301 @""
1302 );
1303 insta::assert_snapshot!(
1304 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 1)),
1305 @"="
1306 );
1307 }
1308
1309 #[test]
1310 fn test_split_byte_line_to_words() {
1311 assert_eq!(split_byte_line_to_words(b""), vec![]);
1312 assert_eq!(
1313 split_byte_line_to_words(b"foo"),
1314 vec![ByteFragment {
1315 word: b"foo",
1316 whitespace_len: 0,
1317 word_width: 3
1318 }],
1319 );
1320 assert_eq!(
1321 split_byte_line_to_words(b" foo"),
1322 vec![
1323 ByteFragment {
1324 word: b"",
1325 whitespace_len: 2,
1326 word_width: 0
1327 },
1328 ByteFragment {
1329 word: b"foo",
1330 whitespace_len: 0,
1331 word_width: 3
1332 },
1333 ],
1334 );
1335 assert_eq!(
1336 split_byte_line_to_words(b"foo "),
1337 vec![ByteFragment {
1338 word: b"foo",
1339 whitespace_len: 2,
1340 word_width: 3
1341 }],
1342 );
1343 assert_eq!(
1344 split_byte_line_to_words(b"a b foo bar "),
1345 vec![
1346 ByteFragment {
1347 word: b"a",
1348 whitespace_len: 1,
1349 word_width: 1
1350 },
1351 ByteFragment {
1352 word: b"b",
1353 whitespace_len: 2,
1354 word_width: 1
1355 },
1356 ByteFragment {
1357 word: b"foo",
1358 whitespace_len: 1,
1359 word_width: 3,
1360 },
1361 ByteFragment {
1362 word: b"bar",
1363 whitespace_len: 1,
1364 word_width: 3,
1365 },
1366 ],
1367 );
1368 }
1369
1370 #[test]
1371 fn test_write_indented() {
1372 let write_prefix = |formatter: &mut dyn Formatter| {
1373 formatter.write_all(b">>").unwrap();
1374 Ok(())
1375 };
1376
1377 let recorder = FormatRecorder::new(true);
1379 insta::assert_snapshot!(
1380 format_colored(
1381 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1382 ),
1383 @""
1384 );
1385 let recorder = FormatRecorder::with_data("abc");
1386 insta::assert_snapshot!(
1387 format_colored(
1388 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1389 ),
1390 @">>abc"
1391 );
1392
1393 let recorder = FormatRecorder::with_data("a\nb\nc");
1395 insta::assert_snapshot!(
1396 format_colored(
1397 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1398 ),
1399 @"
1400 >>a
1401 >>b
1402 >>c
1403 "
1404 );
1405
1406 let recorder = FormatRecorder::with_data("\na\n\nb\n\nc\n");
1409 assert_eq!(
1410 format_colored(
1411 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1412 ),
1413 "\n>>a\n\n>>b\n\n>>c\n"
1414 );
1415
1416 let mut recorder = FormatRecorder::new(true);
1418 for (label, word) in [("red", "foo"), ("cyan", "bar\nbaz\n\nquux")] {
1419 recorder.push_label(label);
1420 write!(recorder, "{word}").unwrap();
1421 recorder.pop_label();
1422 writeln!(recorder).unwrap();
1423 }
1424 insta::assert_snapshot!(
1425 format_colored(
1426 |formatter| write_indented(formatter, &recorder, |fmt| write_prefix(fmt))
1427 ),
1428 @"
1429 [38;5;1m>>foo[39m
1430 [38;5;6m>>bar[39m
1431 [38;5;6m>>baz[39m
1432 [38;5;6m[39m
1433 [38;5;6m>>quux[39m
1434 "
1435 );
1436 }
1437
1438 #[test]
1439 fn test_wrap_bytes() {
1440 assert_eq!(wrap_bytes(b"foo", 10), [b"foo".as_ref()]);
1441 assert_eq!(wrap_bytes(b"foo bar", 10), [b"foo bar".as_ref()]);
1442 assert_eq!(
1443 wrap_bytes(b"foo bar baz", 10),
1444 [b"foo bar".as_ref(), b"baz".as_ref()],
1445 );
1446
1447 assert_eq!(wrap_bytes(b"", 10), [b"".as_ref()]);
1449 assert_eq!(wrap_bytes(b" ", 10), [b"".as_ref()]);
1450
1451 assert_eq!(
1453 wrap_bytes(b"foo bar baz", 8),
1454 [b"foo bar".as_ref(), b"baz".as_ref()],
1455 );
1456 assert_eq!(
1457 wrap_bytes(b"foo bar x", 7),
1458 [b"foo".as_ref(), b"bar x".as_ref()],
1459 );
1460 assert_eq!(
1461 wrap_bytes(b"foo bar \nx", 7),
1462 [b"foo bar".as_ref(), b"x".as_ref()],
1463 );
1464 assert_eq!(
1465 wrap_bytes(b"foo bar\n x", 7),
1466 [b"foo bar".as_ref(), b" x".as_ref()],
1467 );
1468 assert_eq!(
1469 wrap_bytes(b"foo bar x", 4),
1470 [b"foo".as_ref(), b"bar".as_ref(), b"x".as_ref()],
1471 );
1472
1473 assert_eq!(wrap_bytes(b"foo\n", 10), [b"foo".as_ref(), b"".as_ref()]);
1475 assert_eq!(wrap_bytes(b"foo\n", 3), [b"foo".as_ref(), b"".as_ref()]);
1476 assert_eq!(wrap_bytes(b"\n", 10), [b"".as_ref(), b"".as_ref()]);
1477
1478 assert_eq!(wrap_bytes(b"foo x", 2), [b"foo".as_ref(), b"x".as_ref()]);
1480 assert_eq!(wrap_bytes(b"x y", 0), [b"x".as_ref(), b"y".as_ref()]);
1481
1482 assert_eq!(wrap_bytes(b"foo\x80", 10), [b"foo\x80".as_ref()]);
1484 }
1485
1486 #[test]
1487 fn test_wrap_bytes_slice_ptr() {
1488 let text = b"\nfoo\n\nbar baz\n";
1489 let lines = wrap_bytes(text, 10);
1490 assert_eq!(
1491 lines,
1492 [
1493 b"".as_ref(),
1494 b"foo".as_ref(),
1495 b"".as_ref(),
1496 b"bar baz".as_ref(),
1497 b"".as_ref()
1498 ],
1499 );
1500 assert_eq!(lines[0].as_ptr(), text[0..].as_ptr());
1502 assert_eq!(lines[1].as_ptr(), text[1..].as_ptr());
1503 assert_eq!(lines[2].as_ptr(), text[5..].as_ptr());
1504 assert_eq!(lines[3].as_ptr(), text[6..].as_ptr());
1505 assert_eq!(lines[4].as_ptr(), text[14..].as_ptr());
1506 }
1507
1508 #[test]
1509 fn test_write_wrapped() {
1510 let mut recorder = FormatRecorder::new(false);
1512 recorder.push_label("red");
1513 write!(recorder, "foo bar baz\nqux quux\n").unwrap();
1514 recorder.pop_label();
1515 insta::assert_snapshot!(
1516 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1517 @r"
1518 [38;5;1mfoo bar[39m
1519 [38;5;1mbaz[39m
1520 [38;5;1mqux[39m
1521 [38;5;1mquux[39m
1522 "
1523 );
1524
1525 let mut recorder = FormatRecorder::new(false);
1527 for (i, word) in ["foo ", "bar ", "baz\n", "qux ", "quux"].iter().enumerate() {
1528 recorder.push_label(["red", "cyan"][i & 1]);
1529 write!(recorder, "{word}").unwrap();
1530 recorder.pop_label();
1531 }
1532 insta::assert_snapshot!(
1533 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1534 @r"
1535 [38;5;1mfoo [38;5;6mbar[39m
1536 [38;5;1mbaz[39m
1537 [38;5;6mqux[39m
1538 [38;5;1mquux[39m
1539 "
1540 );
1541
1542 let mut recorder = FormatRecorder::new(false);
1544 for (i, word) in ["", "foo", "", "bar baz", ""].iter().enumerate() {
1545 recorder.push_label(["red", "cyan"][i & 1]);
1546 writeln!(recorder, "{word}").unwrap();
1547 recorder.pop_label();
1548 }
1549 insta::assert_snapshot!(
1550 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1551 @r"
1552 [38;5;1m[39m
1553 [38;5;6mfoo[39m
1554 [38;5;1m[39m
1555 [38;5;6mbar baz[39m
1556 [38;5;1m[39m
1557 "
1558 );
1559
1560 let mut recorder = FormatRecorder::new(false);
1562 recorder.push_label("red");
1563 write!(recorder, "foo bar").unwrap();
1564 recorder.pop_label();
1565 write!(recorder, " ").unwrap();
1566 recorder.push_label("cyan");
1567 writeln!(recorder, "baz").unwrap();
1568 recorder.pop_label();
1569 insta::assert_snapshot!(
1570 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1571 @r"
1572 [38;5;1mfoo bar[39m
1573 [38;5;6mbaz[39m
1574 "
1575 );
1576
1577 let mut recorder = FormatRecorder::new(false);
1579 recorder.push_label("red");
1580 write!(recorder, "foo bar ba").unwrap();
1581 recorder.pop_label();
1582 recorder.push_label("cyan");
1583 writeln!(recorder, "z").unwrap();
1584 recorder.pop_label();
1585 insta::assert_snapshot!(
1586 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1587 @r"
1588 [38;5;1mfoo bar[39m
1589 [38;5;1mba[38;5;6mz[39m
1590 "
1591 );
1592 }
1593
1594 #[test]
1595 fn test_write_wrapped_leading_labeled_whitespace() {
1596 let mut recorder = FormatRecorder::new(false);
1597 recorder.push_label("red");
1598 write!(recorder, " ").unwrap();
1599 recorder.pop_label();
1600 write!(recorder, "foo").unwrap();
1601 insta::assert_snapshot!(
1602 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1603 @"[38;5;1m [39mfoo"
1604 );
1605 }
1606
1607 #[test]
1608 fn test_write_wrapped_trailing_labeled_whitespace() {
1609 let mut recorder = FormatRecorder::new(false);
1612 write!(recorder, "foo").unwrap();
1613 recorder.push_label("red");
1614 write!(recorder, " ").unwrap();
1615 recorder.pop_label();
1616 assert_eq!(
1617 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1618 "foo",
1619 );
1620
1621 let mut recorder = FormatRecorder::new(false);
1624 write!(recorder, "foo").unwrap();
1625 recorder.push_label("red");
1626 writeln!(recorder).unwrap();
1627 recorder.pop_label();
1628 assert_eq!(
1629 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1630 "foo\n",
1631 );
1632
1633 let mut recorder = FormatRecorder::new(false);
1636 writeln!(recorder, "foo").unwrap();
1637 recorder.push_label("red");
1638 write!(recorder, " ").unwrap();
1639 recorder.pop_label();
1640 assert_eq!(
1641 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1642 "foo\n",
1643 );
1644 }
1645
1646 #[test]
1647 fn test_parse_author() {
1648 let expected_name = "Example";
1649 let expected_email = "example@example.com";
1650 let parsed = parse_author(&format!("{expected_name} <{expected_email}>")).unwrap();
1651 assert_eq!(
1652 (expected_name.to_string(), expected_email.to_string()),
1653 parsed
1654 );
1655 }
1656
1657 #[test]
1658 fn test_parse_author_with_utf8() {
1659 let expected_name = "Ąćęłńóśżź";
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_without_name() {
1670 let expected_email = "example@example.com";
1671 let parsed = parse_author(&format!("<{expected_email}>")).unwrap();
1672 assert_eq!(("".to_string(), expected_email.to_string()), parsed);
1673 }
1674}