jj_cli/
text_util.rs

1// Copyright 2022-2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
42/// Shortens `text` to `max_width` by removing leading characters. `ellipsis` is
43/// added if the `text` gets truncated.
44///
45/// The returned string (including `ellipsis`) never exceeds the `max_width`.
46pub 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
71/// Shortens `text` to `max_width` by removing trailing characters. `ellipsis`
72/// is added if the `text` gets truncated.
73///
74/// The returned string (including `ellipsis`) never exceeds the `max_width`.
75pub 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
96/// Shortens `text` to `max_width` by removing leading characters, returning
97/// `(start_index, width)`.
98///
99/// The truncated string may have 0-width decomposed characters at start.
100fn 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
131/// Shortens `text` to `max_width` by removing trailing characters, returning
132/// `(end_index, width)`.
133fn 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
161/// Skips `width` leading characters, returning `(start_index, skipped_width)`.
162///
163/// The `skipped_width` may exceed the given `width` if `width` is not at
164/// character boundary.
165///
166/// The truncated string may have 0-width decomposed characters at start.
167fn 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
186/// Skips `width` trailing characters, returning `(end_index, skipped_width)`.
187///
188/// The `skipped_width` may exceed the given `width` if `width` is not at
189/// character boundary.
190fn 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
213/// Removes leading 0-width characters.
214fn trim_start_zero_width_chars(text: &str) -> &str {
215    text.trim_start_matches(|c: char| c.width().unwrap_or(0) == 0)
216}
217
218/// Returns bytes length of leading 0-width characters.
219fn 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
226/// Writes text truncated to `max_width` by removing leading characters. Returns
227/// width of the truncated text, which may be shorter than `max_width`.
228///
229/// The input `recorded_content` should be a single-line text.
230pub 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        // The ellipsis itself may be larger than max_width, so maybe truncate it too.
259        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
269/// Writes text truncated to `max_width` by removing trailing characters.
270/// Returns width of the truncated text, which may be shorter than `max_width`.
271///
272/// The input `recorded_content` should be a single-line text.
273pub 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        // The ellipsis itself may be larger than max_width, so maybe truncate it too.
303        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
310/// Writes text padded to `min_width` by adding leading fill characters.
311///
312/// The input `recorded_content` should be a single-line text. The
313/// `recorded_fill_char` should be bytes of 1-width character.
314pub 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    // We don't care about the width of non-UTF-8 bytes, but should not panic.
321    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
328/// Writes text padded to `min_width` by adding leading fill characters.
329///
330/// The input `recorded_content` should be a single-line text. The
331/// `recorded_fill_char` should be bytes of 1-width character.
332pub 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    // We don't care about the width of non-UTF-8 bytes, but should not panic.
339    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
346/// Writes text padded to `min_width` by adding leading and trailing fill
347/// characters.
348///
349/// The input `recorded_content` should be a single-line text. The
350/// `recorded_fill_char` should be bytes of a 1-width character.
351pub 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    // We don't care about the width of non-UTF-8 bytes, but should not panic.
358    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        // Don't emit labels repeatedly, just repeat content. Suppose fill char
379        // is a single character, the byte sequence shouldn't be broken up to
380        // multiple labeled regions.
381        for _ in 0..fill_width {
382            formatter.write_all(&data[range.clone()])?;
383        }
384        Ok(())
385    })
386}
387
388/// Indents each line by the given prefix preserving labels.
389pub 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                // Prefix inherits the current labels. This is implementation detail
400                // and may be fixed later.
401                write_prefix(formatter)?;
402            }
403            formatter.write_all(line)?;
404            new_line = line.ends_with(b"\n");
405        }
406        Ok(())
407    })
408}
409
410/// Word with trailing whitespace.
411#[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        // We don't care about the width of non-UTF-8 bytes, but should not panic.
421        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
475/// Wraps lines at the given width, returns a vector of lines (excluding "\n".)
476///
477/// Existing newline characters will never be removed. For `str` content, you
478/// can use `textwrap::refill()` to refill a pre-formatted text.
479///
480/// Each line is a sub-slice of the given text, even if the line is empty.
481///
482/// The wrapping logic is more restricted than the default of the `textwrap`.
483/// Notably, this doesn't support hyphenation nor unicode line break. The
484/// display width is calculated based on unicode property in the same manner
485/// as `textwrap::wrap()`.
486pub 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], // Empty line
493            [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
504/// Wraps lines at the given width preserving labels.
505///
506/// `textwrap::wrap()` can also process text containing ANSI escape sequences.
507/// The main difference is that this function will reset the style for each line
508/// and recreate it on the following line if the output `formatter` is
509/// a `ColorFormatter`.
510pub 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    // The recorded data ranges are contiguous, and the line ranges are increasing
524    // sequence (with some holes.) Both ranges should start from data[0].
525    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; // No more lines in this data range
534            }
535            line_ranges.next().unwrap();
536            if line_ranges.peek().is_some() {
537                writeln!(formatter)?; // Not the last line
538            }
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        // Empty string
592        assert_eq!(elide_start("", "", 1), ("".into(), 0));
593
594        // Basic truncation
595        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        // East Asian characters (char.width() == 2)
608        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        // Multi-byte character at boundary
626        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        // Decomposed character at boundary
635        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        // Empty string
665        assert_eq!(elide_end("", "", 1), ("".into(), 0));
666
667        // Basic truncation
668        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        // East Asian characters (char.width() == 2)
681        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        // Multi-byte character at boundary
699        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        // Decomposed character at boundary
708        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        // Truncate start
746        insta::assert_snapshot!(
747            format_colored(|formatter| {
748                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
749            }),
750            @"foobar"
751        );
752        insta::assert_snapshot!(
753            format_colored(|formatter| {
754                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
755            }),
756            @"oobar"
757        );
758        insta::assert_snapshot!(
759            format_colored(|formatter| {
760                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
761            }),
762            @"bar"
763        );
764        insta::assert_snapshot!(
765            format_colored(|formatter| {
766                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
767            }),
768            @"ar"
769        );
770        insta::assert_snapshot!(
771            format_colored(|formatter| {
772                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 0).map(|_| ())
773            }),
774            @""
775        );
776
777        // Truncate end
778        insta::assert_snapshot!(
779            format_colored(|formatter| {
780                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
781            }),
782            @"foobar"
783        );
784        insta::assert_snapshot!(
785            format_colored(|formatter| {
786                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
787            }),
788            @"fooba"
789        );
790        insta::assert_snapshot!(
791            format_colored(|formatter| {
792                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
793            }),
794            @"foo"
795        );
796        insta::assert_snapshot!(
797            format_colored(|formatter| {
798                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 2).map(|_| ())
799            }),
800            @"fo"
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        // Truncate start
817        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        // Truncate end
861        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        // Truncate start
899        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        // Truncate end
913        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        // Truncate start
938        insta::assert_snapshot!(
939            format_colored(|formatter| {
940                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
941            }),
942            @"foobar"
943        );
944        insta::assert_snapshot!(
945            format_colored(|formatter| {
946                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
947            }),
948            @"..bar"
949        );
950        insta::assert_snapshot!(
951            format_colored(|formatter| {
952                write_truncated_start(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
953            }),
954            @"..r"
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        // Truncate end
976        insta::assert_snapshot!(
977            format_colored(|formatter| {
978                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 6).map(|_| ())
979            }),
980            @"foobar"
981        );
982        insta::assert_snapshot!(
983            format_colored(|formatter| {
984                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 5).map(|_| ())
985            }),
986            @"foo.."
987        );
988        insta::assert_snapshot!(
989            format_colored(|formatter| {
990                write_truncated_end(formatter, &recorder, &ellipsis_recorder, 3).map(|_| ())
991            }),
992            @"f.."
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        // Truncate start
1021        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        // Truncate end
1047        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        // Truncate start, empty content
1085        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        // Truncate end
1099        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        // Pad start
1124        insta::assert_snapshot!(
1125            format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 6)),
1126            @"foobar"
1127        );
1128        insta::assert_snapshot!(
1129            format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 7)),
1130            @"=foobar"
1131        );
1132        insta::assert_snapshot!(
1133            format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 8)),
1134            @"==foobar"
1135        );
1136
1137        // Pad end
1138        insta::assert_snapshot!(
1139            format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1140            @"foobar"
1141        );
1142        insta::assert_snapshot!(
1143            format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 7)),
1144            @"foobar="
1145        );
1146        insta::assert_snapshot!(
1147            format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 8)),
1148            @"foobar=="
1149        );
1150
1151        // Pad centered
1152        insta::assert_snapshot!(
1153            format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1154            @"foobar"
1155        );
1156        insta::assert_snapshot!(
1157            format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 7)),
1158            @"foobar="
1159        );
1160        insta::assert_snapshot!(
1161            format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 8)),
1162            @"=foobar="
1163        );
1164        insta::assert_snapshot!(
1165            format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 13)),
1166            @"===foobar===="
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        // Pad start
1179        insta::assert_snapshot!(
1180            format_colored(|formatter| write_padded_start(formatter, &recorder, &fill, 5)),
1181            @"==foo"
1182        );
1183
1184        // Pad end
1185        insta::assert_snapshot!(
1186            format_colored(|formatter| write_padded_end(formatter, &recorder, &fill, 6)),
1187            @"foo==="
1188        );
1189
1190        // Pad centered
1191        insta::assert_snapshot!(
1192            format_colored(|formatter| write_padded_centered(formatter, &recorder, &fill, 6)),
1193            @"=foo=="
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        // Pad start
1203        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        // Pad end
1213        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        // Pad centered
1223        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        // Pad start
1243        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        // Pad end
1253        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        // Pad centered
1263        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        // Empty text is represented as [""]
1344        assert_eq!(wrap_bytes(b"", 10), [b"".as_ref()]);
1345        assert_eq!(wrap_bytes(b" ", 10), [b"".as_ref()]);
1346
1347        // Whitespace in the middle should be preserved
1348        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        // Ends with "\n"
1370        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        // Overflow
1375        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        // Invalid UTF-8 bytes should not cause panic
1379        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        // Each line should be a sub-slice of the source text
1397        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        // Split single label chunk
1407        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        foo bar
1415        baz
1416        qux
1417        quux
1418        "
1419        );
1420
1421        // Multiple label chunks in a line
1422        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        foo bar
1432        baz
1433        qux
1434        quux
1435        "
1436        );
1437
1438        // Empty lines should not cause panic
1439        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        
1449        foo
1450        
1451        bar baz
1452        
1453        "
1454        );
1455
1456        // Split at label boundary
1457        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        foo bar
1469        baz
1470        "
1471        );
1472
1473        // Do not split at label boundary "ba|z" (since it's a single word)
1474        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        foo bar
1485        baz
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            @" foo"
1500        );
1501    }
1502
1503    #[test]
1504    fn test_write_wrapped_trailing_labeled_whitespace() {
1505        // data: "foo" " "
1506        // line:  ---
1507        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        // data: "foo" "\n"
1518        // line:  ---     -
1519        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        // data: "foo\n" " "
1530        // line:  ---    -
1531        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}