Skip to main content

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