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_elide_start() {
591 assert_eq!(elide_start("", "", 1), ("".into(), 0));
593
594 assert_eq!(elide_start("abcdef", "", 6), ("abcdef".into(), 6));
596 assert_eq!(elide_start("abcdef", "", 5), ("bcdef".into(), 5));
597 assert_eq!(elide_start("abcdef", "", 1), ("f".into(), 1));
598 assert_eq!(elide_start("abcdef", "", 0), ("".into(), 0));
599 assert_eq!(elide_start("abcdef", "-=~", 6), ("abcdef".into(), 6));
600 assert_eq!(elide_start("abcdef", "-=~", 5), ("-=~ef".into(), 5));
601 assert_eq!(elide_start("abcdef", "-=~", 4), ("-=~f".into(), 4));
602 assert_eq!(elide_start("abcdef", "-=~", 3), ("-=~".into(), 3));
603 assert_eq!(elide_start("abcdef", "-=~", 2), ("=~".into(), 2));
604 assert_eq!(elide_start("abcdef", "-=~", 1), ("~".into(), 1));
605 assert_eq!(elide_start("abcdef", "-=~", 0), ("".into(), 0));
606
607 assert_eq!(elide_start("一二三", "", 6), ("一二三".into(), 6));
609 assert_eq!(elide_start("一二三", "", 5), ("二三".into(), 4));
610 assert_eq!(elide_start("一二三", "", 4), ("二三".into(), 4));
611 assert_eq!(elide_start("一二三", "", 1), ("".into(), 0));
612 assert_eq!(elide_start("一二三", "-=~", 6), ("一二三".into(), 6));
613 assert_eq!(elide_start("一二三", "-=~", 5), ("-=~三".into(), 5));
614 assert_eq!(elide_start("一二三", "-=~", 4), ("-=~".into(), 3));
615 assert_eq!(elide_start("一二三", "略", 6), ("一二三".into(), 6));
616 assert_eq!(elide_start("一二三", "略", 5), ("略三".into(), 4));
617 assert_eq!(elide_start("一二三", "略", 4), ("略三".into(), 4));
618 assert_eq!(elide_start("一二三", "略", 2), ("略".into(), 2));
619 assert_eq!(elide_start("一二三", "略", 1), ("".into(), 0));
620 assert_eq!(elide_start("一二三", ".", 5), (".二三".into(), 5));
621 assert_eq!(elide_start("一二三", ".", 4), (".三".into(), 3));
622 assert_eq!(elide_start("一二三", "略.", 5), ("略.三".into(), 5));
623 assert_eq!(elide_start("一二三", "略.", 4), ("略.".into(), 3));
624
625 assert_eq!(elide_start("àbcdè", "", 5), ("àbcdè".into(), 5));
627 assert_eq!(elide_start("àbcdè", "", 4), ("bcdè".into(), 4));
628 assert_eq!(elide_start("àbcdè", "", 1), ("è".into(), 1));
629 assert_eq!(elide_start("àbcdè", "", 0), ("".into(), 0));
630 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 4), ("ÀÇÈè".into(), 4));
631 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
632 assert_eq!(elide_start("àbcdè", "ÀÇÈ", 2), ("ÇÈ".into(), 2));
633
634 assert_eq!(
636 elide_start("a\u{300}bcde\u{300}", "", 5),
637 ("a\u{300}bcde\u{300}".into(), 5)
638 );
639 assert_eq!(
640 elide_start("a\u{300}bcde\u{300}", "", 4),
641 ("bcde\u{300}".into(), 4)
642 );
643 assert_eq!(
644 elide_start("a\u{300}bcde\u{300}", "", 1),
645 ("e\u{300}".into(), 1)
646 );
647 assert_eq!(elide_start("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
648 assert_eq!(
649 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
650 ("A\u{300}CE\u{300}e\u{300}".into(), 4)
651 );
652 assert_eq!(
653 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
654 ("A\u{300}CE\u{300}".into(), 3)
655 );
656 assert_eq!(
657 elide_start("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
658 ("CE\u{300}".into(), 2)
659 );
660 }
661
662 #[test]
663 fn test_elide_end() {
664 assert_eq!(elide_end("", "", 1), ("".into(), 0));
666
667 assert_eq!(elide_end("abcdef", "", 6), ("abcdef".into(), 6));
669 assert_eq!(elide_end("abcdef", "", 5), ("abcde".into(), 5));
670 assert_eq!(elide_end("abcdef", "", 1), ("a".into(), 1));
671 assert_eq!(elide_end("abcdef", "", 0), ("".into(), 0));
672 assert_eq!(elide_end("abcdef", "-=~", 6), ("abcdef".into(), 6));
673 assert_eq!(elide_end("abcdef", "-=~", 5), ("ab-=~".into(), 5));
674 assert_eq!(elide_end("abcdef", "-=~", 4), ("a-=~".into(), 4));
675 assert_eq!(elide_end("abcdef", "-=~", 3), ("-=~".into(), 3));
676 assert_eq!(elide_end("abcdef", "-=~", 2), ("-=".into(), 2));
677 assert_eq!(elide_end("abcdef", "-=~", 1), ("-".into(), 1));
678 assert_eq!(elide_end("abcdef", "-=~", 0), ("".into(), 0));
679
680 assert_eq!(elide_end("一二三", "", 6), ("一二三".into(), 6));
682 assert_eq!(elide_end("一二三", "", 5), ("一二".into(), 4));
683 assert_eq!(elide_end("一二三", "", 4), ("一二".into(), 4));
684 assert_eq!(elide_end("一二三", "", 1), ("".into(), 0));
685 assert_eq!(elide_end("一二三", "-=~", 6), ("一二三".into(), 6));
686 assert_eq!(elide_end("一二三", "-=~", 5), ("一-=~".into(), 5));
687 assert_eq!(elide_end("一二三", "-=~", 4), ("-=~".into(), 3));
688 assert_eq!(elide_end("一二三", "略", 6), ("一二三".into(), 6));
689 assert_eq!(elide_end("一二三", "略", 5), ("一略".into(), 4));
690 assert_eq!(elide_end("一二三", "略", 4), ("一略".into(), 4));
691 assert_eq!(elide_end("一二三", "略", 2), ("略".into(), 2));
692 assert_eq!(elide_end("一二三", "略", 1), ("".into(), 0));
693 assert_eq!(elide_end("一二三", ".", 5), ("一二.".into(), 5));
694 assert_eq!(elide_end("一二三", ".", 4), ("一.".into(), 3));
695 assert_eq!(elide_end("一二三", "略.", 5), ("一略.".into(), 5));
696 assert_eq!(elide_end("一二三", "略.", 4), ("略.".into(), 3));
697
698 assert_eq!(elide_end("àbcdè", "", 5), ("àbcdè".into(), 5));
700 assert_eq!(elide_end("àbcdè", "", 4), ("àbcd".into(), 4));
701 assert_eq!(elide_end("àbcdè", "", 1), ("à".into(), 1));
702 assert_eq!(elide_end("àbcdè", "", 0), ("".into(), 0));
703 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 4), ("àÀÇÈ".into(), 4));
704 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 3), ("ÀÇÈ".into(), 3));
705 assert_eq!(elide_end("àbcdè", "ÀÇÈ", 2), ("ÀÇ".into(), 2));
706
707 assert_eq!(
709 elide_end("a\u{300}bcde\u{300}", "", 5),
710 ("a\u{300}bcde\u{300}".into(), 5)
711 );
712 assert_eq!(
713 elide_end("a\u{300}bcde\u{300}", "", 4),
714 ("a\u{300}bcd".into(), 4)
715 );
716 assert_eq!(
717 elide_end("a\u{300}bcde\u{300}", "", 1),
718 ("a\u{300}".into(), 1)
719 );
720 assert_eq!(elide_end("a\u{300}bcde\u{300}", "", 0), ("".into(), 0));
721 assert_eq!(
722 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 4),
723 ("a\u{300}A\u{300}CE\u{300}".into(), 4)
724 );
725 assert_eq!(
726 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 3),
727 ("A\u{300}CE\u{300}".into(), 3)
728 );
729 assert_eq!(
730 elide_end("a\u{300}bcde\u{300}", "A\u{300}CE\u{300}", 2),
731 ("A\u{300}C".into(), 2)
732 );
733 }
734
735 #[test]
736 fn test_write_truncated_labeled() {
737 let ellipsis_recorder = FormatRecorder::new();
738 let mut recorder = FormatRecorder::new();
739 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
740 recorder.push_label(label);
741 write!(recorder, "{word}").unwrap();
742 recorder.pop_label();
743 }
744
745 insta::assert_snapshot!(
747 format_colored(|formatter| {
748 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
749 }),
750 @"[38;5;1mfoo[38;5;6mbar[39m"
751 );
752 insta::assert_snapshot!(
753 format_colored(|formatter| {
754 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
755 }),
756 @"[38;5;1moo[38;5;6mbar[39m"
757 );
758 insta::assert_snapshot!(
759 format_colored(|formatter| {
760 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
761 }),
762 @"[38;5;6mbar[39m"
763 );
764 insta::assert_snapshot!(
765 format_colored(|formatter| {
766 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
767 }),
768 @"[38;5;6mar[39m"
769 );
770 insta::assert_snapshot!(
771 format_colored(|formatter| {
772 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
773 }),
774 @""
775 );
776
777 insta::assert_snapshot!(
779 format_colored(|formatter| {
780 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
781 }),
782 @"[38;5;1mfoo[38;5;6mbar[39m"
783 );
784 insta::assert_snapshot!(
785 format_colored(|formatter| {
786 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
787 }),
788 @"[38;5;1mfoo[38;5;6mba[39m"
789 );
790 insta::assert_snapshot!(
791 format_colored(|formatter| {
792 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
793 }),
794 @"[38;5;1mfoo[39m"
795 );
796 insta::assert_snapshot!(
797 format_colored(|formatter| {
798 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
799 }),
800 @"[38;5;1mfo[39m"
801 );
802 insta::assert_snapshot!(
803 format_colored(|formatter| {
804 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
805 }),
806 @""
807 );
808 }
809
810 #[test]
811 fn test_write_truncated_non_ascii_chars() {
812 let ellipsis_recorder = FormatRecorder::new();
813 let mut recorder = FormatRecorder::new();
814 write!(recorder, "a\u{300}bc\u{300}一二三").unwrap();
815
816 insta::assert_snapshot!(
818 format_colored(|formatter| {
819 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
820 }),
821 @""
822 );
823 insta::assert_snapshot!(
824 format_colored(|formatter| {
825 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
826 }),
827 @"三"
828 );
829 insta::assert_snapshot!(
830 format_colored(|formatter| {
831 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
832 }),
833 @"三"
834 );
835 insta::assert_snapshot!(
836 format_colored(|formatter| {
837 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
838 }),
839 @"一二三"
840 );
841 insta::assert_snapshot!(
842 format_colored(|formatter| {
843 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
844 }),
845 @"c̀一二三"
846 );
847 insta::assert_snapshot!(
848 format_colored(|formatter| {
849 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
850 }),
851 @"àbc̀一二三"
852 );
853 insta::assert_snapshot!(
854 format_colored(|formatter| {
855 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
856 }),
857 @"àbc̀一二三"
858 );
859
860 insta::assert_snapshot!(
862 format_colored(|formatter| {
863 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
864 }),
865 @"à"
866 );
867 insta::assert_snapshot!(
868 format_colored(|formatter| {
869 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
870 }),
871 @"àbc̀"
872 );
873 insta::assert_snapshot!(
874 format_colored(|formatter| {
875 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
876 }),
877 @"àbc̀一"
878 );
879 insta::assert_snapshot!(
880 format_colored(|formatter| {
881 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
882 }),
883 @"àbc̀一二三"
884 );
885 insta::assert_snapshot!(
886 format_colored(|formatter| {
887 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
888 }),
889 @"àbc̀一二三"
890 );
891 }
892
893 #[test]
894 fn test_write_truncated_empty_content() {
895 let ellipsis_recorder = FormatRecorder::new();
896 let recorder = FormatRecorder::new();
897
898 insta::assert_snapshot!(
900 format_colored(|formatter| {
901 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
902 }),
903 @""
904 );
905 insta::assert_snapshot!(
906 format_colored(|formatter| {
907 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
908 }),
909 @""
910 );
911
912 insta::assert_snapshot!(
914 format_colored(|formatter| {
915 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
916 }),
917 @""
918 );
919 insta::assert_snapshot!(
920 format_colored(|formatter| {
921 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
922 }),
923 @""
924 );
925 }
926
927 #[test]
928 fn test_write_truncated_ellipsis_labeled() {
929 let ellipsis_recorder = FormatRecorder::with_data("..");
930 let mut recorder = FormatRecorder::new();
931 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
932 recorder.push_label(label);
933 write!(recorder, "{word}").unwrap();
934 recorder.pop_label();
935 }
936
937 insta::assert_snapshot!(
939 format_colored(|formatter| {
940 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
941 }),
942 @"[38;5;1mfoo[38;5;6mbar[39m"
943 );
944 insta::assert_snapshot!(
945 format_colored(|formatter| {
946 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
947 }),
948 @"..[38;5;6mbar[39m"
949 );
950 insta::assert_snapshot!(
951 format_colored(|formatter| {
952 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
953 }),
954 @"..[38;5;6mr[39m"
955 );
956 insta::assert_snapshot!(
957 format_colored(|formatter| {
958 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
959 }),
960 @".."
961 );
962 insta::assert_snapshot!(
963 format_colored(|formatter| {
964 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
965 }),
966 @"."
967 );
968 insta::assert_snapshot!(
969 format_colored(|formatter| {
970 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
971 }),
972 @""
973 );
974
975 insta::assert_snapshot!(
977 format_colored(|formatter| {
978 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
979 }),
980 @"[38;5;1mfoo[38;5;6mbar[39m"
981 );
982 insta::assert_snapshot!(
983 format_colored(|formatter| {
984 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
985 }),
986 @"[38;5;1mfoo[39m.."
987 );
988 insta::assert_snapshot!(
989 format_colored(|formatter| {
990 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
991 }),
992 @"[38;5;1mf[39m.."
993 );
994 insta::assert_snapshot!(
995 format_colored(|formatter| {
996 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
997 }),
998 @".."
999 );
1000 insta::assert_snapshot!(
1001 format_colored(|formatter| {
1002 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1003 }),
1004 @"."
1005 );
1006 insta::assert_snapshot!(
1007 format_colored(|formatter| {
1008 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1009 }),
1010 @""
1011 );
1012 }
1013
1014 #[test]
1015 fn test_write_truncated_ellipsis_non_ascii_chars() {
1016 let ellipsis_recorder = FormatRecorder::with_data("..");
1017 let mut recorder = FormatRecorder::new();
1018 write!(recorder, "a\u{300}bc\u{300}一二三").unwrap();
1019
1020 insta::assert_snapshot!(
1022 format_colored(|formatter| {
1023 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1024 }),
1025 @"."
1026 );
1027 insta::assert_snapshot!(
1028 format_colored(|formatter| {
1029 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
1030 }),
1031 @".."
1032 );
1033 insta::assert_snapshot!(
1034 format_colored(|formatter| {
1035 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1036 }),
1037 @"..三"
1038 );
1039 insta::assert_snapshot!(
1040 format_colored(|formatter| {
1041 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 7).map(|_| ())
1042 }),
1043 @"..二三"
1044 );
1045
1046 insta::assert_snapshot!(
1048 format_colored(|formatter| {
1049 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1050 }),
1051 @"."
1052 );
1053 insta::assert_snapshot!(
1054 format_colored(|formatter| {
1055 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 4).map(|_| ())
1056 }),
1057 @"àb.."
1058 );
1059 insta::assert_snapshot!(
1060 format_colored(|formatter| {
1061 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
1062 }),
1063 @"àbc̀.."
1064 );
1065 insta::assert_snapshot!(
1066 format_colored(|formatter| {
1067 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 9).map(|_| ())
1068 }),
1069 @"àbc̀一二三"
1070 );
1071 insta::assert_snapshot!(
1072 format_colored(|formatter| {
1073 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 10).map(|_| ())
1074 }),
1075 @"àbc̀一二三"
1076 );
1077 }
1078
1079 #[test]
1080 fn test_write_truncated_ellipsis_empty_content() {
1081 let ellipsis_recorder = FormatRecorder::with_data("..");
1082 let recorder = FormatRecorder::new();
1083
1084 insta::assert_snapshot!(
1086 format_colored(|formatter| {
1087 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1088 }),
1089 @""
1090 );
1091 insta::assert_snapshot!(
1092 format_colored(|formatter| {
1093 write_truncated_start(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1094 }),
1095 @""
1096 );
1097
1098 insta::assert_snapshot!(
1100 format_colored(|formatter| {
1101 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
1102 }),
1103 @""
1104 );
1105 insta::assert_snapshot!(
1106 format_colored(|formatter| {
1107 write_truncated_end(formatter, &recorder, &ellipsis_recorder, 1).map(|_| ())
1108 }),
1109 @""
1110 );
1111 }
1112
1113 #[test]
1114 fn test_write_padded_labeled_content() {
1115 let mut recorder = FormatRecorder::new();
1116 for (label, word) in [("red", "foo"), ("cyan", "bar")] {
1117 recorder.push_label(label);
1118 write!(recorder, "{word}").unwrap();
1119 recorder.pop_label();
1120 }
1121 let fill = FormatRecorder::with_data("=");
1122
1123 insta::assert_snapshot!(
1125 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 6)),
1126 @"[38;5;1mfoo[38;5;6mbar[39m"
1127 );
1128 insta::assert_snapshot!(
1129 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 7)),
1130 @"=[38;5;1mfoo[38;5;6mbar[39m"
1131 );
1132 insta::assert_snapshot!(
1133 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 8)),
1134 @"==[38;5;1mfoo[38;5;6mbar[39m"
1135 );
1136
1137 insta::assert_snapshot!(
1139 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1140 @"[38;5;1mfoo[38;5;6mbar[39m"
1141 );
1142 insta::assert_snapshot!(
1143 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 7)),
1144 @"[38;5;1mfoo[38;5;6mbar[39m="
1145 );
1146 insta::assert_snapshot!(
1147 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 8)),
1148 @"[38;5;1mfoo[38;5;6mbar[39m=="
1149 );
1150
1151 insta::assert_snapshot!(
1153 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1154 @"[38;5;1mfoo[38;5;6mbar[39m"
1155 );
1156 insta::assert_snapshot!(
1157 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 7)),
1158 @"[38;5;1mfoo[38;5;6mbar[39m="
1159 );
1160 insta::assert_snapshot!(
1161 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 8)),
1162 @"=[38;5;1mfoo[38;5;6mbar[39m="
1163 );
1164 insta::assert_snapshot!(
1165 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1166 @"===[38;5;1mfoo[38;5;6mbar[39m===="
1167 );
1168 }
1169
1170 #[test]
1171 fn test_write_padded_labeled_fill_char() {
1172 let recorder = FormatRecorder::with_data("foo");
1173 let mut fill = FormatRecorder::new();
1174 fill.push_label("red");
1175 write!(fill, "=").unwrap();
1176 fill.pop_label();
1177
1178 insta::assert_snapshot!(
1180 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 5)),
1181 @"[38;5;1m==[39mfoo"
1182 );
1183
1184 insta::assert_snapshot!(
1186 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1187 @"foo[38;5;1m===[39m"
1188 );
1189
1190 insta::assert_snapshot!(
1192 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1193 @"[38;5;1m=[39mfoo[38;5;1m==[39m"
1194 );
1195 }
1196
1197 #[test]
1198 fn test_write_padded_non_ascii_chars() {
1199 let recorder = FormatRecorder::with_data("a\u{300}bc\u{300}一二三");
1200 let fill = FormatRecorder::with_data("=");
1201
1202 insta::assert_snapshot!(
1204 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 9)),
1205 @"àbc̀一二三"
1206 );
1207 insta::assert_snapshot!(
1208 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 10)),
1209 @"=àbc̀一二三"
1210 );
1211
1212 insta::assert_snapshot!(
1214 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 9)),
1215 @"àbc̀一二三"
1216 );
1217 insta::assert_snapshot!(
1218 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 10)),
1219 @"àbc̀一二三="
1220 );
1221
1222 insta::assert_snapshot!(
1224 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 9)),
1225 @"àbc̀一二三"
1226 );
1227 insta::assert_snapshot!(
1228 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 10)),
1229 @"àbc̀一二三="
1230 );
1231 insta::assert_snapshot!(
1232 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1233 @"==àbc̀一二三=="
1234 );
1235 }
1236
1237 #[test]
1238 fn test_write_padded_empty_content() {
1239 let recorder = FormatRecorder::new();
1240 let fill = FormatRecorder::with_data("=");
1241
1242 insta::assert_snapshot!(
1244 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 0)),
1245 @""
1246 );
1247 insta::assert_snapshot!(
1248 format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 1)),
1249 @"="
1250 );
1251
1252 insta::assert_snapshot!(
1254 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 0)),
1255 @""
1256 );
1257 insta::assert_snapshot!(
1258 format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 1)),
1259 @"="
1260 );
1261
1262 insta::assert_snapshot!(
1264 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 0)),
1265 @""
1266 );
1267 insta::assert_snapshot!(
1268 format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 1)),
1269 @"="
1270 );
1271 }
1272
1273 #[test]
1274 fn test_split_byte_line_to_words() {
1275 assert_eq!(split_byte_line_to_words(b""), vec![]);
1276 assert_eq!(
1277 split_byte_line_to_words(b"foo"),
1278 vec![ByteFragment {
1279 word: b"foo",
1280 whitespace_len: 0,
1281 word_width: 3
1282 }],
1283 );
1284 assert_eq!(
1285 split_byte_line_to_words(b" foo"),
1286 vec![
1287 ByteFragment {
1288 word: b"",
1289 whitespace_len: 2,
1290 word_width: 0
1291 },
1292 ByteFragment {
1293 word: b"foo",
1294 whitespace_len: 0,
1295 word_width: 3
1296 },
1297 ],
1298 );
1299 assert_eq!(
1300 split_byte_line_to_words(b"foo "),
1301 vec![ByteFragment {
1302 word: b"foo",
1303 whitespace_len: 2,
1304 word_width: 3
1305 }],
1306 );
1307 assert_eq!(
1308 split_byte_line_to_words(b"a b foo bar "),
1309 vec![
1310 ByteFragment {
1311 word: b"a",
1312 whitespace_len: 1,
1313 word_width: 1
1314 },
1315 ByteFragment {
1316 word: b"b",
1317 whitespace_len: 2,
1318 word_width: 1
1319 },
1320 ByteFragment {
1321 word: b"foo",
1322 whitespace_len: 1,
1323 word_width: 3,
1324 },
1325 ByteFragment {
1326 word: b"bar",
1327 whitespace_len: 1,
1328 word_width: 3,
1329 },
1330 ],
1331 );
1332 }
1333
1334 #[test]
1335 fn test_wrap_bytes() {
1336 assert_eq!(wrap_bytes(b"foo", 10), [b"foo".as_ref()]);
1337 assert_eq!(wrap_bytes(b"foo bar", 10), [b"foo bar".as_ref()]);
1338 assert_eq!(
1339 wrap_bytes(b"foo bar baz", 10),
1340 [b"foo bar".as_ref(), b"baz".as_ref()],
1341 );
1342
1343 assert_eq!(wrap_bytes(b"", 10), [b"".as_ref()]);
1345 assert_eq!(wrap_bytes(b" ", 10), [b"".as_ref()]);
1346
1347 assert_eq!(
1349 wrap_bytes(b"foo bar baz", 8),
1350 [b"foo bar".as_ref(), b"baz".as_ref()],
1351 );
1352 assert_eq!(
1353 wrap_bytes(b"foo bar x", 7),
1354 [b"foo".as_ref(), b"bar x".as_ref()],
1355 );
1356 assert_eq!(
1357 wrap_bytes(b"foo bar \nx", 7),
1358 [b"foo bar".as_ref(), b"x".as_ref()],
1359 );
1360 assert_eq!(
1361 wrap_bytes(b"foo bar\n x", 7),
1362 [b"foo bar".as_ref(), b" x".as_ref()],
1363 );
1364 assert_eq!(
1365 wrap_bytes(b"foo bar x", 4),
1366 [b"foo".as_ref(), b"bar".as_ref(), b"x".as_ref()],
1367 );
1368
1369 assert_eq!(wrap_bytes(b"foo\n", 10), [b"foo".as_ref(), b"".as_ref()]);
1371 assert_eq!(wrap_bytes(b"foo\n", 3), [b"foo".as_ref(), b"".as_ref()]);
1372 assert_eq!(wrap_bytes(b"\n", 10), [b"".as_ref(), b"".as_ref()]);
1373
1374 assert_eq!(wrap_bytes(b"foo x", 2), [b"foo".as_ref(), b"x".as_ref()]);
1376 assert_eq!(wrap_bytes(b"x y", 0), [b"x".as_ref(), b"y".as_ref()]);
1377
1378 assert_eq!(wrap_bytes(b"foo\x80", 10), [b"foo\x80".as_ref()]);
1380 }
1381
1382 #[test]
1383 fn test_wrap_bytes_slice_ptr() {
1384 let text = b"\nfoo\n\nbar baz\n";
1385 let lines = wrap_bytes(text, 10);
1386 assert_eq!(
1387 lines,
1388 [
1389 b"".as_ref(),
1390 b"foo".as_ref(),
1391 b"".as_ref(),
1392 b"bar baz".as_ref(),
1393 b"".as_ref()
1394 ],
1395 );
1396 assert_eq!(lines[0].as_ptr(), text[0..].as_ptr());
1398 assert_eq!(lines[1].as_ptr(), text[1..].as_ptr());
1399 assert_eq!(lines[2].as_ptr(), text[5..].as_ptr());
1400 assert_eq!(lines[3].as_ptr(), text[6..].as_ptr());
1401 assert_eq!(lines[4].as_ptr(), text[14..].as_ptr());
1402 }
1403
1404 #[test]
1405 fn test_write_wrapped() {
1406 let mut recorder = FormatRecorder::new();
1408 recorder.push_label("red");
1409 write!(recorder, "foo bar baz\nqux quux\n").unwrap();
1410 recorder.pop_label();
1411 insta::assert_snapshot!(
1412 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1413 @r"
1414 [38;5;1mfoo bar[39m
1415 [38;5;1mbaz[39m
1416 [38;5;1mqux[39m
1417 [38;5;1mquux[39m
1418 "
1419 );
1420
1421 let mut recorder = FormatRecorder::new();
1423 for (i, word) in ["foo ", "bar ", "baz\n", "qux ", "quux"].iter().enumerate() {
1424 recorder.push_label(["red", "cyan"][i & 1]);
1425 write!(recorder, "{word}").unwrap();
1426 recorder.pop_label();
1427 }
1428 insta::assert_snapshot!(
1429 format_colored(|formatter| write_wrapped(formatter, &recorder, 7)),
1430 @r"
1431 [38;5;1mfoo [38;5;6mbar[39m
1432 [38;5;1mbaz[39m
1433 [38;5;6mqux[39m
1434 [38;5;1mquux[39m
1435 "
1436 );
1437
1438 let mut recorder = FormatRecorder::new();
1440 for (i, word) in ["", "foo", "", "bar baz", ""].iter().enumerate() {
1441 recorder.push_label(["red", "cyan"][i & 1]);
1442 writeln!(recorder, "{word}").unwrap();
1443 recorder.pop_label();
1444 }
1445 insta::assert_snapshot!(
1446 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1447 @r"
1448 [38;5;1m[39m
1449 [38;5;6mfoo[39m
1450 [38;5;1m[39m
1451 [38;5;6mbar baz[39m
1452 [38;5;1m[39m
1453 "
1454 );
1455
1456 let mut recorder = FormatRecorder::new();
1458 recorder.push_label("red");
1459 write!(recorder, "foo bar").unwrap();
1460 recorder.pop_label();
1461 write!(recorder, " ").unwrap();
1462 recorder.push_label("cyan");
1463 writeln!(recorder, "baz").unwrap();
1464 recorder.pop_label();
1465 insta::assert_snapshot!(
1466 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1467 @r"
1468 [38;5;1mfoo bar[39m
1469 [38;5;6mbaz[39m
1470 "
1471 );
1472
1473 let mut recorder = FormatRecorder::new();
1475 recorder.push_label("red");
1476 write!(recorder, "foo bar ba").unwrap();
1477 recorder.pop_label();
1478 recorder.push_label("cyan");
1479 writeln!(recorder, "z").unwrap();
1480 recorder.pop_label();
1481 insta::assert_snapshot!(
1482 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1483 @r"
1484 [38;5;1mfoo bar[39m
1485 [38;5;1mba[38;5;6mz[39m
1486 "
1487 );
1488 }
1489
1490 #[test]
1491 fn test_write_wrapped_leading_labeled_whitespace() {
1492 let mut recorder = FormatRecorder::new();
1493 recorder.push_label("red");
1494 write!(recorder, " ").unwrap();
1495 recorder.pop_label();
1496 write!(recorder, "foo").unwrap();
1497 insta::assert_snapshot!(
1498 format_colored(|formatter| write_wrapped(formatter, &recorder, 10)),
1499 @"[38;5;1m [39mfoo"
1500 );
1501 }
1502
1503 #[test]
1504 fn test_write_wrapped_trailing_labeled_whitespace() {
1505 let mut recorder = FormatRecorder::new();
1508 write!(recorder, "foo").unwrap();
1509 recorder.push_label("red");
1510 write!(recorder, " ").unwrap();
1511 recorder.pop_label();
1512 assert_eq!(
1513 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1514 "foo",
1515 );
1516
1517 let mut recorder = FormatRecorder::new();
1520 write!(recorder, "foo").unwrap();
1521 recorder.push_label("red");
1522 writeln!(recorder).unwrap();
1523 recorder.pop_label();
1524 assert_eq!(
1525 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1526 "foo\n",
1527 );
1528
1529 let mut recorder = FormatRecorder::new();
1532 writeln!(recorder, "foo").unwrap();
1533 recorder.push_label("red");
1534 write!(recorder, " ").unwrap();
1535 recorder.pop_label();
1536 assert_eq!(
1537 format_plain_text(|formatter| write_wrapped(formatter, &recorder, 10)),
1538 "foo\n",
1539 );
1540 }
1541
1542 #[test]
1543 fn test_parse_author() {
1544 let expected_name = "Example";
1545 let expected_email = "example@example.com";
1546 let parsed = parse_author(&format!("{expected_name} <{expected_email}>")).unwrap();
1547 assert_eq!(
1548 (expected_name.to_string(), expected_email.to_string()),
1549 parsed
1550 );
1551 }
1552
1553 #[test]
1554 fn test_parse_author_with_utf8() {
1555 let expected_name = "Ąćęłńóśżź";
1556 let expected_email = "example@example.com";
1557 let parsed = parse_author(&format!("{expected_name} <{expected_email}>")).unwrap();
1558 assert_eq!(
1559 (expected_name.to_string(), expected_email.to_string()),
1560 parsed
1561 );
1562 }
1563
1564 #[test]
1565 fn test_parse_author_without_name() {
1566 let expected_email = "example@example.com";
1567 let parsed = parse_author(&format!("<{expected_email}>")).unwrap();
1568 assert_eq!(("".to_string(), expected_email.to_string()), parsed);
1569 }
1570}