1685 lines · 58.7 KB · Rust
RawDownload
// 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 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 1685.