// Copyright 2022-2023 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::borrow::Cow;use std::cmp;use std::io;use std::ops::Range; use bstr::ByteSlice as _;use unicode_width::UnicodeWidthChar as _;use unicode_width::UnicodeWidthStr as _; use crate::formatter::FormatRecorder;use crate::formatter::Formatter; pub fn complete_newline(s: impl Into<String>) -> String { let mut s = s.into(); if !s.is_empty() && !s.ends_with('\n') { s.push('\n'); } s} pub fn split_email(email: &str) -> (&str, Option<&str>) { if let Some((username, rest)) = email.split_once('@') { (username, Some(rest)) } else { (email, None) }} /// Shortens `text` to `max_width` by removing leading characters. `ellipsis` is/// added if the `text` gets truncated.////// The returned string (including `ellipsis`) never exceeds the `max_width`.pub fn elide_start<'a>( text: &'a str, ellipsis: &'a str, max_width: usize,) -> (Cow<'a, str>, usize) { let (text_start, text_width) = truncate_start_pos(text, max_width); if text_start == 0 { return (Cow::Borrowed(text), text_width); } let (ellipsis_start, ellipsis_width) = truncate_start_pos(ellipsis, max_width); if ellipsis_start != 0 { let ellipsis = trim_start_zero_width_chars(&ellipsis[ellipsis_start..]); return (Cow::Borrowed(ellipsis), ellipsis_width); } let text = &text[text_start..]; let max_text_width = max_width - ellipsis_width; let (skip, skipped_width) = skip_start_pos(text, text_width.saturating_sub(max_text_width)); let text = trim_start_zero_width_chars(&text[skip..]); let concat_width = ellipsis_width + (text_width - skipped_width); assert!(concat_width <= max_width); (Cow::Owned([ellipsis, text].concat()), concat_width)} /// Shortens `text` to `max_width` by removing trailing characters. `ellipsis`/// is added if the `text` gets truncated.////// The returned string (including `ellipsis`) never exceeds the `max_width`.pub fn elide_end<'a>(text: &'a str, ellipsis: &'a str, max_width: usize) -> (Cow<'a, str>, usize) { let (text_end, text_width) = truncate_end_pos(text, max_width); if text_end == text.len() { return (Cow::Borrowed(text), text_width); } let (ellipsis_end, ellipsis_width) = truncate_end_pos(ellipsis, max_width); if ellipsis_end != ellipsis.len() { let ellipsis = &ellipsis[..ellipsis_end]; return (Cow::Borrowed(ellipsis), ellipsis_width); } let text = &text[..text_end]; let max_text_width = max_width - ellipsis_width; let (skip, skipped_width) = skip_end_pos(text, text_width.saturating_sub(max_text_width)); let text = &text[..skip]; let concat_width = (text_width - skipped_width) + ellipsis_width; assert!(concat_width <= max_width); (Cow::Owned([text, ellipsis].concat()), concat_width)} /// Shortens `text` to `max_width` by removing leading characters, returning/// `(start_index, width)`.////// The truncated string may have 0-width decomposed characters at start.fn truncate_start_pos(text: &str, max_width: usize) -> (usize, usize) { truncate_start_pos_with_indices( text.char_indices() .rev() .map(|(start, c)| (start + c.len_utf8(), c)), max_width, )} fn truncate_start_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) { truncate_start_pos_with_indices( text.char_indices().rev().map(|(_, end, c)| (end, c)), max_width, )} fn truncate_start_pos_with_indices( char_indices_rev: impl Iterator<Item = (usize, char)>, max_width: usize,) -> (usize, usize) { let mut acc_width = 0; for (end, c) in char_indices_rev { let new_width = acc_width + c.width().unwrap_or(0); if new_width > max_width { return (end, acc_width); } acc_width = new_width; } (0, acc_width)} /// Shortens `text` to `max_width` by removing trailing characters, returning/// `(end_index, width)`.fn truncate_end_pos(text: &str, max_width: usize) -> (usize, usize) { truncate_end_pos_with_indices(text.char_indices(), text.len(), max_width)} fn truncate_end_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) { truncate_end_pos_with_indices( text.char_indices().map(|(start, _, c)| (start, c)), text.len(), max_width, )} fn truncate_end_pos_with_indices( char_indices_fwd: impl Iterator<Item = (usize, char)>, text_len: usize, max_width: usize,) -> (usize, usize) { let mut acc_width = 0; for (start, c) in char_indices_fwd { let new_width = acc_width + c.width().unwrap_or(0); if new_width > max_width { return (start, acc_width); } acc_width = new_width; } (text_len, acc_width)} /// Skips `width` leading characters, returning `(start_index, skipped_width)`.////// The `skipped_width` may exceed the given `width` if `width` is not at/// character boundary.////// The truncated string may have 0-width decomposed characters at start.fn skip_start_pos(text: &str, width: usize) -> (usize, usize) { skip_start_pos_with_indices(text.char_indices(), text.len(), width)} fn skip_start_pos_with_indices( char_indices_fwd: impl Iterator<Item = (usize, char)>, text_len: usize, width: usize,) -> (usize, usize) { let mut acc_width = 0; for (start, c) in char_indices_fwd { if acc_width >= width { return (start, acc_width); } acc_width += c.width().unwrap_or(0); } (text_len, acc_width)} /// Skips `width` trailing characters, returning `(end_index, skipped_width)`.////// The `skipped_width` may exceed the given `width` if `width` is not at/// character boundary.fn skip_end_pos(text: &str, width: usize) -> (usize, usize) { skip_end_pos_with_indices( text.char_indices() .rev() .map(|(start, c)| (start + c.len_utf8(), c)), width, )} fn skip_end_pos_with_indices( char_indices_rev: impl Iterator<Item = (usize, char)>, width: usize,) -> (usize, usize) { let mut acc_width = 0; for (end, c) in char_indices_rev { if acc_width >= width { return (end, acc_width); } acc_width += c.width().unwrap_or(0); } (0, acc_width)} /// Removes leading 0-width characters.fn trim_start_zero_width_chars(text: &str) -> &str { text.trim_start_matches(|c: char| c.width().unwrap_or(0) == 0)} /// Returns bytes length of leading 0-width characters.fn count_start_zero_width_chars_bytes(text: &[u8]) -> usize { text.char_indices() .find(|(_, _, c)| c.width().unwrap_or(0) != 0) .map(|(start, _, _)| start) .unwrap_or(text.len())} /// Writes text truncated to `max_width` by removing leading characters. Returns/// width of the truncated text, which may be shorter than `max_width`.////// The input `recorded_content` should be a single-line text.pub fn write_truncated_start( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_ellipsis: &FormatRecorder, max_width: usize,) -> io::Result<usize> { let data = recorded_content.data(); let data_width = String::from_utf8_lossy(data).width(); let ellipsis_data = recorded_ellipsis.data(); let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width(); let (start, mut truncated_width) = if data_width > max_width { truncate_start_pos_bytes(data, max_width.saturating_sub(ellipsis_width)) } else { (0, data_width) }; let mut replay_truncated = |recorded: &FormatRecorder, truncated_start: usize| { recorded.replay_with(formatter, |formatter, range| { let start = cmp::max(range.start, truncated_start); if start < range.end { formatter.write_all(&recorded.data()[start..range.end])?; } Ok(()) }) }; if data_width > max_width { // The ellipsis itself may be larger than max_width, so maybe truncate it too. let (start, ellipsis_width) = truncate_start_pos_bytes(ellipsis_data, max_width); let truncated_start = start + count_start_zero_width_chars_bytes(&ellipsis_data[start..]); truncated_width += ellipsis_width; replay_truncated(recorded_ellipsis, truncated_start)?; } let truncated_start = start + count_start_zero_width_chars_bytes(&data[start..]); replay_truncated(recorded_content, truncated_start)?; Ok(truncated_width)} /// Writes text truncated to `max_width` by removing trailing characters./// Returns width of the truncated text, which may be shorter than `max_width`.////// The input `recorded_content` should be a single-line text.pub fn write_truncated_end( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_ellipsis: &FormatRecorder, max_width: usize,) -> io::Result<usize> { let data = recorded_content.data(); let data_width = String::from_utf8_lossy(data).width(); let ellipsis_data = recorded_ellipsis.data(); let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width(); let (truncated_end, mut truncated_width) = if data_width > max_width { truncate_end_pos_bytes(data, max_width.saturating_sub(ellipsis_width)) } else { (data.len(), data_width) }; let mut replay_truncated = |recorded: &FormatRecorder, truncated_end: usize| { recorded.replay_with(formatter, |formatter, range| { let end = cmp::min(range.end, truncated_end); if range.start < end { formatter.write_all(&recorded.data()[range.start..end])?; } Ok(()) }) }; replay_truncated(recorded_content, truncated_end)?; if data_width > max_width { // The ellipsis itself may be larger than max_width, so maybe truncate it too. let (truncated_end, ellipsis_width) = truncate_end_pos_bytes(ellipsis_data, max_width); truncated_width += ellipsis_width; replay_truncated(recorded_ellipsis, truncated_end)?; } Ok(truncated_width)} /// Writes text padded to `min_width` by adding leading fill characters.////// The input `recorded_content` should be a single-line text. The/// `recorded_fill_char` should be bytes of 1-width character.pub fn write_padded_start( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize,) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); write_padding(formatter, recorded_fill_char, fill_width)?; recorded_content.replay(formatter)?; Ok(())} /// Writes text padded to `min_width` by adding leading fill characters.////// The input `recorded_content` should be a single-line text. The/// `recorded_fill_char` should be bytes of 1-width character.pub fn write_padded_end( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize,) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); recorded_content.replay(formatter)?; write_padding(formatter, recorded_fill_char, fill_width)?; Ok(())} /// Writes text padded to `min_width` by adding leading and trailing fill/// characters.////// The input `recorded_content` should be a single-line text. The/// `recorded_fill_char` should be bytes of a 1-width character.pub fn write_padded_centered( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize,) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); let fill_left = fill_width / 2; let fill_right = fill_width - fill_left; write_padding(formatter, recorded_fill_char, fill_left)?; recorded_content.replay(formatter)?; write_padding(formatter, recorded_fill_char, fill_right)?; Ok(())} fn write_padding( formatter: &mut dyn Formatter, recorded_fill_char: &FormatRecorder, fill_width: usize,) -> io::Result<()> { if fill_width == 0 { return Ok(()); } let data = recorded_fill_char.data(); recorded_fill_char.replay_with(formatter, |formatter, range| { // Don't emit labels repeatedly, just repeat content. Suppose fill char // is a single character, the byte sequence shouldn't be broken up to // multiple labeled regions. for _ in 0..fill_width { formatter.write_all(&data[range.clone()])?; } Ok(()) })} /// Indents each line by the given prefix preserving labels.pub fn write_indented( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, mut write_prefix: impl FnMut(&mut dyn Formatter) -> io::Result<()>,) -> io::Result<()> { let data = recorded_content.data(); let mut new_line = true; recorded_content.replay_with(formatter, |formatter, range| { for line in data[range].split_inclusive(|&c| c == b'\n') { if new_line && line != b"\n" { // Prefix inherits the current labels. This is implementation detail // and may be fixed later. write_prefix(formatter)?; } formatter.write_all(line)?; new_line = line.ends_with(b"\n"); } Ok(()) })} /// Word with trailing whitespace.#[derive(Clone, Copy, Debug, Eq, PartialEq)]struct ByteFragment<'a> { word: &'a [u8], whitespace_len: usize, word_width: usize,} impl<'a> ByteFragment<'a> { fn new(word: &'a [u8], whitespace_len: usize) -> Self { // We don't care about the width of non-UTF-8 bytes, but should not panic. let word_width = textwrap::core::display_width(&String::from_utf8_lossy(word)); Self { word, whitespace_len, word_width, } } fn offset_in(&self, text: &[u8]) -> usize { byte_offset_from(text, self.word) }} impl textwrap::core::Fragment for ByteFragment<'_> { fn width(&self) -> f64 { self.word_width as f64 } fn whitespace_width(&self) -> f64 { self.whitespace_len as f64 } fn penalty_width(&self) -> f64 { 0.0 }} fn byte_offset_from(outer: &[u8], inner: &[u8]) -> usize { let outer_start = outer.as_ptr() as usize; let inner_start = inner.as_ptr() as usize; assert!(outer_start <= inner_start); assert!(inner_start + inner.len() <= outer_start + outer.len()); inner_start - outer_start} fn split_byte_line_to_words(line: &[u8]) -> Vec<ByteFragment<'_>> { let mut words = Vec::new(); let mut tail = line; while let Some(word_end) = tail.iter().position(|&c| c == b' ') { let word = &tail[..word_end]; let ws_end = tail[word_end + 1..] .iter() .position(|&c| c != b' ') .map(|p| p + word_end + 1) .unwrap_or(tail.len()); words.push(ByteFragment::new(word, ws_end - word_end)); tail = &tail[ws_end..]; } if !tail.is_empty() { words.push(ByteFragment::new(tail, 0)); } words} /// Wraps lines at the given width, returns a vector of lines (excluding "\n".)////// Existing newline characters will never be removed. For `str` content, you/// can use `textwrap::refill()` to refill a pre-formatted text.////// Each line is a sub-slice of the given text, even if the line is empty.////// The wrapping logic is more restricted than the default of the `textwrap`./// Notably, this doesn't support hyphenation nor unicode line break. The/// display width is calculated based on unicode property in the same manner/// as `textwrap::wrap()`.pub fn wrap_bytes(text: &[u8], width: usize) -> Vec<&[u8]> { let mut split_lines = Vec::new(); for line in text.split(|&c| c == b'\n') { let words = split_byte_line_to_words(line); let split = textwrap::wrap_algorithms::wrap_first_fit(&words, &[width as f64]); split_lines.extend(split.iter().map(|words| match words { [] => &line[..0], // Empty line [a] => a.word, [a, .., b] => { let start = a.offset_in(line); let end = b.offset_in(line) + b.word.len(); &line[start..end] } }));Lines 1–500 of 1867. Load more
// Copyright 2022-2023 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::borrow::Cow; use std::cmp; use std::io; use std::ops::Range; use bstr::ByteSlice as _; use unicode_width::UnicodeWidthChar as _; use unicode_width::UnicodeWidthStr as _; use crate::formatter::FormatRecorder; use crate::formatter::Formatter; pub fn complete_newline(s: impl Into<String>) -> String { let mut s = s.into(); if !s.is_empty() && !s.ends_with('\n') { s.push('\n'); } s } pub fn split_email(email: &str) -> (&str, Option<&str>) { if let Some((username, rest)) = email.split_once('@') { (username, Some(rest)) } else { (email, None) } } /// Shortens `text` to `max_width` by removing leading characters. `ellipsis` is /// added if the `text` gets truncated. /// /// The returned string (including `ellipsis`) never exceeds the `max_width`. pub fn elide_start<'a>( text: &'a str, ellipsis: &'a str, max_width: usize, ) -> (Cow<'a, str>, usize) { let (text_start, text_width) = truncate_start_pos(text, max_width); if text_start == 0 { return (Cow::Borrowed(text), text_width); } let (ellipsis_start, ellipsis_width) = truncate_start_pos(ellipsis, max_width); if ellipsis_start != 0 { let ellipsis = trim_start_zero_width_chars(&ellipsis[ellipsis_start..]); return (Cow::Borrowed(ellipsis), ellipsis_width); } let text = &text[text_start..]; let max_text_width = max_width - ellipsis_width; let (skip, skipped_width) = skip_start_pos(text, text_width.saturating_sub(max_text_width)); let text = trim_start_zero_width_chars(&text[skip..]); let concat_width = ellipsis_width + (text_width - skipped_width); assert!(concat_width <= max_width); (Cow::Owned([ellipsis, text].concat()), concat_width) } /// Shortens `text` to `max_width` by removing trailing characters. `ellipsis` /// is added if the `text` gets truncated. /// /// The returned string (including `ellipsis`) never exceeds the `max_width`. pub fn elide_end<'a>(text: &'a str, ellipsis: &'a str, max_width: usize) -> (Cow<'a, str>, usize) { let (text_end, text_width) = truncate_end_pos(text, max_width); if text_end == text.len() { return (Cow::Borrowed(text), text_width); } let (ellipsis_end, ellipsis_width) = truncate_end_pos(ellipsis, max_width); if ellipsis_end != ellipsis.len() { let ellipsis = &ellipsis[..ellipsis_end]; return (Cow::Borrowed(ellipsis), ellipsis_width); } let text = &text[..text_end]; let max_text_width = max_width - ellipsis_width; let (skip, skipped_width) = skip_end_pos(text, text_width.saturating_sub(max_text_width)); let text = &text[..skip]; let concat_width = (text_width - skipped_width) + ellipsis_width; assert!(concat_width <= max_width); (Cow::Owned([text, ellipsis].concat()), concat_width) } /// Shortens `text` to `max_width` by removing leading characters, returning /// `(start_index, width)`. /// /// The truncated string may have 0-width decomposed characters at start. fn truncate_start_pos(text: &str, max_width: usize) -> (usize, usize) { truncate_start_pos_with_indices( text.char_indices() .rev() .map(|(start, c)| (start + c.len_utf8(), c)), max_width, ) } fn truncate_start_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) { truncate_start_pos_with_indices( text.char_indices().rev().map(|(_, end, c)| (end, c)), max_width, ) } fn truncate_start_pos_with_indices( char_indices_rev: impl Iterator<Item = (usize, char)>, max_width: usize, ) -> (usize, usize) { let mut acc_width = 0; for (end, c) in char_indices_rev { let new_width = acc_width + c.width().unwrap_or(0); if new_width > max_width { return (end, acc_width); } acc_width = new_width; } (0, acc_width) } /// Shortens `text` to `max_width` by removing trailing characters, returning /// `(end_index, width)`. fn truncate_end_pos(text: &str, max_width: usize) -> (usize, usize) { truncate_end_pos_with_indices(text.char_indices(), text.len(), max_width) } fn truncate_end_pos_bytes(text: &[u8], max_width: usize) -> (usize, usize) { truncate_end_pos_with_indices( text.char_indices().map(|(start, _, c)| (start, c)), text.len(), max_width, ) } fn truncate_end_pos_with_indices( char_indices_fwd: impl Iterator<Item = (usize, char)>, text_len: usize, max_width: usize, ) -> (usize, usize) { let mut acc_width = 0; for (start, c) in char_indices_fwd { let new_width = acc_width + c.width().unwrap_or(0); if new_width > max_width { return (start, acc_width); } acc_width = new_width; } (text_len, acc_width) } /// Skips `width` leading characters, returning `(start_index, skipped_width)`. /// /// The `skipped_width` may exceed the given `width` if `width` is not at /// character boundary. /// /// The truncated string may have 0-width decomposed characters at start. fn skip_start_pos(text: &str, width: usize) -> (usize, usize) { skip_start_pos_with_indices(text.char_indices(), text.len(), width) } fn skip_start_pos_with_indices( char_indices_fwd: impl Iterator<Item = (usize, char)>, text_len: usize, width: usize, ) -> (usize, usize) { let mut acc_width = 0; for (start, c) in char_indices_fwd { if acc_width >= width { return (start, acc_width); } acc_width += c.width().unwrap_or(0); } (text_len, acc_width) } /// Skips `width` trailing characters, returning `(end_index, skipped_width)`. /// /// The `skipped_width` may exceed the given `width` if `width` is not at /// character boundary. fn skip_end_pos(text: &str, width: usize) -> (usize, usize) { skip_end_pos_with_indices( text.char_indices() .rev() .map(|(start, c)| (start + c.len_utf8(), c)), width, ) } fn skip_end_pos_with_indices( char_indices_rev: impl Iterator<Item = (usize, char)>, width: usize, ) -> (usize, usize) { let mut acc_width = 0; for (end, c) in char_indices_rev { if acc_width >= width { return (end, acc_width); } acc_width += c.width().unwrap_or(0); } (0, acc_width) } /// Removes leading 0-width characters. fn trim_start_zero_width_chars(text: &str) -> &str { text.trim_start_matches(|c: char| c.width().unwrap_or(0) == 0) } /// Returns bytes length of leading 0-width characters. fn count_start_zero_width_chars_bytes(text: &[u8]) -> usize { text.char_indices() .find(|(_, _, c)| c.width().unwrap_or(0) != 0) .map(|(start, _, _)| start) .unwrap_or(text.len()) } /// Writes text truncated to `max_width` by removing leading characters. Returns /// width of the truncated text, which may be shorter than `max_width`. /// /// The input `recorded_content` should be a single-line text. pub fn write_truncated_start( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_ellipsis: &FormatRecorder, max_width: usize, ) -> io::Result<usize> { let data = recorded_content.data(); let data_width = String::from_utf8_lossy(data).width(); let ellipsis_data = recorded_ellipsis.data(); let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width(); let (start, mut truncated_width) = if data_width > max_width { truncate_start_pos_bytes(data, max_width.saturating_sub(ellipsis_width)) } else { (0, data_width) }; let mut replay_truncated = |recorded: &FormatRecorder, truncated_start: usize| { recorded.replay_with(formatter, |formatter, range| { let start = cmp::max(range.start, truncated_start); if start < range.end { formatter.write_all(&recorded.data()[start..range.end])?; } Ok(()) }) }; if data_width > max_width { // The ellipsis itself may be larger than max_width, so maybe truncate it too. let (start, ellipsis_width) = truncate_start_pos_bytes(ellipsis_data, max_width); let truncated_start = start + count_start_zero_width_chars_bytes(&ellipsis_data[start..]); truncated_width += ellipsis_width; replay_truncated(recorded_ellipsis, truncated_start)?; } let truncated_start = start + count_start_zero_width_chars_bytes(&data[start..]); replay_truncated(recorded_content, truncated_start)?; Ok(truncated_width) } /// Writes text truncated to `max_width` by removing trailing characters. /// Returns width of the truncated text, which may be shorter than `max_width`. /// /// The input `recorded_content` should be a single-line text. pub fn write_truncated_end( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_ellipsis: &FormatRecorder, max_width: usize, ) -> io::Result<usize> { let data = recorded_content.data(); let data_width = String::from_utf8_lossy(data).width(); let ellipsis_data = recorded_ellipsis.data(); let ellipsis_width = String::from_utf8_lossy(ellipsis_data).width(); let (truncated_end, mut truncated_width) = if data_width > max_width { truncate_end_pos_bytes(data, max_width.saturating_sub(ellipsis_width)) } else { (data.len(), data_width) }; let mut replay_truncated = |recorded: &FormatRecorder, truncated_end: usize| { recorded.replay_with(formatter, |formatter, range| { let end = cmp::min(range.end, truncated_end); if range.start < end { formatter.write_all(&recorded.data()[range.start..end])?; } Ok(()) }) }; replay_truncated(recorded_content, truncated_end)?; if data_width > max_width { // The ellipsis itself may be larger than max_width, so maybe truncate it too. let (truncated_end, ellipsis_width) = truncate_end_pos_bytes(ellipsis_data, max_width); truncated_width += ellipsis_width; replay_truncated(recorded_ellipsis, truncated_end)?; } Ok(truncated_width) } /// Writes text padded to `min_width` by adding leading fill characters. /// /// The input `recorded_content` should be a single-line text. The /// `recorded_fill_char` should be bytes of 1-width character. pub fn write_padded_start( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize, ) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); write_padding(formatter, recorded_fill_char, fill_width)?; recorded_content.replay(formatter)?; Ok(()) } /// Writes text padded to `min_width` by adding leading fill characters. /// /// The input `recorded_content` should be a single-line text. The /// `recorded_fill_char` should be bytes of 1-width character. pub fn write_padded_end( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize, ) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); recorded_content.replay(formatter)?; write_padding(formatter, recorded_fill_char, fill_width)?; Ok(()) } /// Writes text padded to `min_width` by adding leading and trailing fill /// characters. /// /// The input `recorded_content` should be a single-line text. The /// `recorded_fill_char` should be bytes of a 1-width character. pub fn write_padded_centered( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, recorded_fill_char: &FormatRecorder, min_width: usize, ) -> io::Result<()> { // We don't care about the width of non-UTF-8 bytes, but should not panic. let width = String::from_utf8_lossy(recorded_content.data()).width(); let fill_width = min_width.saturating_sub(width); let fill_left = fill_width / 2; let fill_right = fill_width - fill_left; write_padding(formatter, recorded_fill_char, fill_left)?; recorded_content.replay(formatter)?; write_padding(formatter, recorded_fill_char, fill_right)?; Ok(()) } fn write_padding( formatter: &mut dyn Formatter, recorded_fill_char: &FormatRecorder, fill_width: usize, ) -> io::Result<()> { if fill_width == 0 { return Ok(()); } let data = recorded_fill_char.data(); recorded_fill_char.replay_with(formatter, |formatter, range| { // Don't emit labels repeatedly, just repeat content. Suppose fill char // is a single character, the byte sequence shouldn't be broken up to // multiple labeled regions. for _ in 0..fill_width { formatter.write_all(&data[range.clone()])?; } Ok(()) }) } /// Indents each line by the given prefix preserving labels. pub fn write_indented( formatter: &mut dyn Formatter, recorded_content: &FormatRecorder, mut write_prefix: impl FnMut(&mut dyn Formatter) -> io::Result<()>, ) -> io::Result<()> { let data = recorded_content.data(); let mut new_line = true; recorded_content.replay_with(formatter, |formatter, range| { for line in data[range].split_inclusive(|&c| c == b'\n') { if new_line && line != b"\n" { // Prefix inherits the current labels. This is implementation detail // and may be fixed later. write_prefix(formatter)?; } formatter.write_all(line)?; new_line = line.ends_with(b"\n"); } Ok(()) }) } /// Word with trailing whitespace. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct ByteFragment<'a> { word: &'a [u8], whitespace_len: usize, word_width: usize, } impl<'a> ByteFragment<'a> { fn new(word: &'a [u8], whitespace_len: usize) -> Self { // We don't care about the width of non-UTF-8 bytes, but should not panic. let word_width = textwrap::core::display_width(&String::from_utf8_lossy(word)); Self { word, whitespace_len, word_width, } } fn offset_in(&self, text: &[u8]) -> usize { byte_offset_from(text, self.word) } } impl textwrap::core::Fragment for ByteFragment<'_> { fn width(&self) -> f64 { self.word_width as f64 } fn whitespace_width(&self) -> f64 { self.whitespace_len as f64 } fn penalty_width(&self) -> f64 { 0.0 } } fn byte_offset_from(outer: &[u8], inner: &[u8]) -> usize { let outer_start = outer.as_ptr() as usize; let inner_start = inner.as_ptr() as usize; assert!(outer_start <= inner_start); assert!(inner_start + inner.len() <= outer_start + outer.len()); inner_start - outer_start } fn split_byte_line_to_words(line: &[u8]) -> Vec<ByteFragment<'_>> { let mut words = Vec::new(); let mut tail = line; while let Some(word_end) = tail.iter().position(|&c| c == b' ') { let word = &tail[..word_end]; let ws_end = tail[word_end + 1..] .iter() .position(|&c| c != b' ') .map(|p| p + word_end + 1) .unwrap_or(tail.len()); words.push(ByteFragment::new(word, ws_end - word_end)); tail = &tail[ws_end..]; } if !tail.is_empty() { words.push(ByteFragment::new(tail, 0)); } words } /// Wraps lines at the given width, returns a vector of lines (excluding "\n".) /// /// Existing newline characters will never be removed. For `str` content, you /// can use `textwrap::refill()` to refill a pre-formatted text. /// /// Each line is a sub-slice of the given text, even if the line is empty. /// /// The wrapping logic is more restricted than the default of the `textwrap`. /// Notably, this doesn't support hyphenation nor unicode line break. The /// display width is calculated based on unicode property in the same manner /// as `textwrap::wrap()`. pub fn wrap_bytes(text: &[u8], width: usize) -> Vec<&[u8]> { let mut split_lines = Vec::new(); for line in text.split(|&c| c == b'\n') { let words = split_byte_line_to_words(line); let split = textwrap::wrap_algorithms::wrap_first_fit(&words, &[width as f64]); split_lines.extend(split.iter().map(|words| match words { [] => &line[..0], // Empty line [a] => a.word, [a, .., b] => { let start = a.offset_in(line); let end = b.offset_in(line) + b.word.len(); &line[start..end] } }));