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