chore(vendor): add anyhow crate

This commit was merged in pull request #68.
This commit is contained in:
2026-09-17 14:13:23 -04:00
parent 7a3a3d6b90
commit 37539682d4
454 changed files with 8906 additions and 1468 deletions

View File

@@ -0,0 +1,158 @@
/// Compute the display width of `text`
///
/// # Examples
///
/// **Note:** When the `unicode` Cargo feature is disabled, all characters are presumed to take up
/// 1 width. With the feature enabled, function will correctly deal with [combining characters] in
/// their decomposed form (see [Unicode equivalence]).
///
/// An example of a decomposed character is “é”, which can be decomposed into: “e” followed by a
/// combining acute accent: “◌́”. Without the `unicode` Cargo feature, every `char` has a width of
/// 1. This includes the combining accent:
///
/// ## Emojis and CJK Characters
///
/// Characters such as emojis and [CJK characters] used in the
/// Chinese, Japanese, and Korean languages are seen as double-width,
/// even if the `unicode-width` feature is disabled:
///
/// # Limitations
///
/// The displayed width of a string cannot always be computed from the
/// string alone. This is because the width depends on the rendering
/// engine used. This is particularly visible with [emoji modifier
/// sequences] where a base emoji is modified with, e.g., skin tone or
/// hair color modifiers. It is up to the rendering engine to detect
/// this and to produce a suitable emoji.
///
/// A simple example is “❤️”, which consists of “❤” (U+2764: Black
/// Heart Symbol) followed by U+FE0F (Variation Selector-16). By
/// itself, “❤” is a black heart, but if you follow it with the
/// variant selector, you may get a wider red heart.
///
/// A more complex example would be “👨‍🦰” which should depict a man
/// with red hair. Here the computed width is too large — and the
/// width differs depending on the use of the `unicode-width` feature:
///
/// This happens because the grapheme consists of three code points:
/// “👨” (U+1F468: Man), Zero Width Joiner (U+200D), and “🦰”
/// (U+1F9B0: Red Hair). You can see them above in the test. With
/// `unicode-width` enabled, the ZWJ is correctly seen as having zero
/// width, without it is counted as a double-width character.
///
/// ## Terminal Support
///
/// Modern browsers typically do a great job at combining characters
/// as shown above, but terminals often struggle more. As an example,
/// Gnome Terminal version 3.38.1, shows “❤️” as a big red heart, but
/// shows "👨‍🦰" as “👨🦰”.
///
/// [combining characters]: https://en.wikipedia.org/wiki/Combining_character
/// [Unicode equivalence]: https://en.wikipedia.org/wiki/Unicode_equivalence
/// [CJK characters]: https://en.wikipedia.org/wiki/CJK_characters
/// [emoji modifier sequences]: https://unicode.org/emoji/charts/full-emoji-modifiers.html
#[inline(never)]
pub(crate) fn display_width(text: &str) -> usize {
let mut width = 0;
let mut control_sequence = false;
let control_terminate: char = 'm';
for ch in text.chars() {
if ch.is_ascii_control() {
control_sequence = true;
} else if control_sequence && ch == control_terminate {
control_sequence = false;
continue;
}
if !control_sequence {
width += ch_width(ch);
}
}
width
}
#[cfg(feature = "unicode")]
fn ch_width(ch: char) -> usize {
unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)
}
#[cfg(not(feature = "unicode"))]
fn ch_width(_: char) -> usize {
1
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "unicode")]
use unicode_width::UnicodeWidthChar;
#[test]
fn emojis_have_correct_width() {
use unic_emoji_char::is_emoji;
// Emojis in the Basic Latin (ASCII) and Latin-1 Supplement
// blocks all have a width of 1 column. This includes
// characters such as '#' and '©'.
for ch in '\u{1}'..'\u{FF}' {
if is_emoji(ch) {
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
#[cfg(feature = "unicode")]
assert_eq!(ch.width().unwrap(), 1, "char: {desc}");
#[cfg(not(feature = "unicode"))]
assert_eq!(ch_width(ch), 1, "char: {desc}");
}
}
// Emojis in the remaining blocks of the Basic Multilingual
// Plane (BMP), in the Supplementary Multilingual Plane (SMP),
// and in the Supplementary Ideographic Plane (SIP), are all 1
// or 2 columns wide when unicode-width is used, and always 2
// columns wide otherwise. This includes all of our favorite
// emojis such as 😊.
for ch in '\u{FF}'..'\u{2FFFF}' {
if is_emoji(ch) {
let desc = format!("{:?} U+{:04X}", ch, ch as u32);
#[cfg(feature = "unicode")]
assert!(ch.width().unwrap() <= 2, "char: {desc}");
#[cfg(not(feature = "unicode"))]
assert_eq!(ch_width(ch), 1, "char: {desc}");
}
}
// The remaining planes contain almost no assigned code points
// and thus also no emojis.
}
#[test]
#[cfg(feature = "unicode")]
fn display_width_works() {
assert_eq!("Café Plain".len(), 11); // “é” is two bytes
assert_eq!(display_width("Café Plain"), 10);
}
#[test]
#[cfg(feature = "unicode")]
fn display_width_narrow_emojis() {
assert_eq!(display_width("⁉"), 1);
}
#[test]
#[cfg(feature = "unicode")]
fn display_width_narrow_emojis_variant_selector() {
assert_eq!(display_width("⁉\u{fe0f}"), 1);
}
#[test]
#[cfg(feature = "unicode")]
fn display_width_emojis() {
assert_eq!(display_width("😂😭🥺🤣✨😍🙏🥰😊🔥"), 20);
}
}

View File

@@ -0,0 +1,122 @@
//! Fork of `textwrap` crate
//!
//! Benefits of forking:
//! - Pull in only what we need rather than relying on the compiler to remove what we don't need
//! - `LineWrapper` is able to incrementally wrap which will help with `StyledStr`
pub(crate) mod core;
#[cfg(feature = "wrap_help")]
pub(crate) mod word_separators;
#[cfg(feature = "wrap_help")]
pub(crate) mod wrap_algorithms;
#[cfg(feature = "wrap_help")]
pub(crate) fn wrap(content: &str, hard_width: usize) -> String {
let mut wrapper = wrap_algorithms::LineWrapper::new(hard_width);
let mut total = Vec::new();
for line in content.split_inclusive('\n') {
wrapper.reset();
let line = word_separators::find_words_ascii_space(line).collect::<Vec<_>>();
total.extend(wrapper.wrap(line));
}
total.join("")
}
#[cfg(not(feature = "wrap_help"))]
pub(crate) fn wrap(content: &str, _hard_width: usize) -> String {
content.to_owned()
}
#[cfg(test)]
#[cfg(feature = "wrap_help")]
mod test {
/// Compatibility shim to keep textwrap's tests
fn wrap(content: &str, hard_width: usize) -> Vec<String> {
super::wrap(content, hard_width)
.trim_end()
.split('\n')
.map(|s| s.to_owned())
.collect::<Vec<_>>()
}
#[test]
fn no_wrap() {
assert_eq!(wrap("foo", 10), vec!["foo"]);
}
#[test]
fn wrap_simple() {
assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]);
}
#[test]
fn to_be_or_not() {
assert_eq!(
wrap("To be, or not to be, that is the question.", 10),
vec!["To be, or", "not to be,", "that is", "the", "question."]
);
}
#[test]
fn multiple_words_on_first_line() {
assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]);
}
#[test]
fn long_word() {
assert_eq!(wrap("foo", 0), vec!["foo"]);
}
#[test]
fn long_words() {
assert_eq!(wrap("foo bar", 0), vec!["foo", "bar"]);
}
#[test]
fn max_width() {
assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);
let text = "Hello there! This is some English text. \
It should not be wrapped given the extents below.";
assert_eq!(wrap(text, usize::MAX), vec![text]);
}
#[test]
fn leading_whitespace() {
assert_eq!(wrap(" foo bar", 6), vec![" foo", " bar"]);
}
#[test]
fn leading_whitespace_empty_first_line() {
// If there is no space for the first word, the first line
// will be empty. This is because the string is split into
// words like [" ", "foobar ", "baz"], which puts "foobar " on
// the second line. We never output trailing whitespace
assert_eq!(wrap(" foobar baz", 6), vec![" foobar", " baz"]);
}
#[test]
fn trailing_whitespace() {
// Whitespace is only significant inside a line. After a line
// gets too long and is broken, the first word starts in
// column zero and is not indented.
assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]);
}
#[test]
fn issue_99() {
// We did not reset the in_whitespace flag correctly and did
// not handle single-character words after a line break.
assert_eq!(
wrap("aaabbbccc x yyyzzzwww", 9),
vec!["aaabbbccc", "x", "yyyzzzwww"]
);
}
#[test]
fn issue_129() {
// The dash is an em-dash which takes up four bytes. We used
// to panic since we tried to index into the character.
assert_eq!(wrap("x – x", 1), vec!["x", "–", "x"]);
}
}

View File

@@ -0,0 +1,92 @@
pub(crate) fn find_words_ascii_space(line: &str) -> impl Iterator<Item = &'_ str> + '_ {
let mut start = 0;
let mut in_whitespace = false;
let mut char_indices = line.char_indices();
std::iter::from_fn(move || {
for (idx, ch) in char_indices.by_ref() {
let next_whitespace = ch == ' ';
if in_whitespace && !next_whitespace {
let word = &line[start..idx];
start = idx;
in_whitespace = next_whitespace;
return Some(word);
}
in_whitespace = next_whitespace;
}
if start < line.len() {
let word = &line[start..];
start = line.len();
return Some(word);
}
None
})
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_find_words {
($ascii_name:ident,
$([ $line:expr, $ascii_words:expr ]),+) => {
#[test]
fn $ascii_name() {
$(
let expected_words: Vec<&str> = $ascii_words.to_vec();
let actual_words = find_words_ascii_space($line)
.collect::<Vec<_>>();
assert_eq!(actual_words, expected_words, "Line: {:?}", $line);
)+
}
};
}
test_find_words!(ascii_space_empty, ["", []]);
test_find_words!(ascii_single_word, ["foo", ["foo"]]);
test_find_words!(ascii_two_words, ["foo bar", ["foo ", "bar"]]);
test_find_words!(
ascii_multiple_words,
["foo bar", ["foo ", "bar"]],
["x y z", ["x ", "y ", "z"]]
);
test_find_words!(ascii_only_whitespace, [" ", [" "]], [" ", [" "]]);
test_find_words!(
ascii_inter_word_whitespace,
["foo bar", ["foo ", "bar"]]
);
test_find_words!(ascii_trailing_whitespace, ["foo ", ["foo "]]);
test_find_words!(ascii_leading_whitespace, [" foo", [" ", "foo"]]);
test_find_words!(
ascii_multi_column_char,
["\u{1f920}", ["\u{1f920}"]] // cowboy emoji 🤠
);
test_find_words!(
ascii_hyphens,
["foo-bar", ["foo-bar"]],
["foo- bar", ["foo- ", "bar"]],
["foo - bar", ["foo ", "- ", "bar"]],
["foo -bar", ["foo ", "-bar"]]
);
test_find_words!(ascii_newline, ["foo\nbar", ["foo\nbar"]]);
test_find_words!(ascii_tab, ["foo\tbar", ["foo\tbar"]]);
test_find_words!(
ascii_non_breaking_space,
["foo\u{00A0}bar", ["foo\u{00A0}bar"]]
);
}

View File

@@ -0,0 +1,68 @@
use super::core::display_width;
#[derive(Debug)]
pub(crate) struct LineWrapper<'w> {
hard_width: usize,
line_width: usize,
indentation: Option<&'w str>,
}
impl<'w> LineWrapper<'w> {
pub(crate) fn new(hard_width: usize) -> Self {
Self {
hard_width,
line_width: 0,
indentation: None,
}
}
pub(crate) fn reset(&mut self) {
self.line_width = 0;
self.indentation = None;
}
pub(crate) fn wrap(&mut self, mut words: Vec<&'w str>) -> Vec<&'w str> {
let mut first_word = false;
if self.indentation.is_none() {
first_word = true;
if let Some(word) = words.first() {
if word.trim().is_empty() {
self.indentation = Some(*word);
} else {
self.indentation = Some("");
}
}
}
let mut i = 0;
while i < words.len() {
let word = &words[i];
let trimmed = word.trim_end();
let word_width = display_width(trimmed);
let trimmed_delta = word.len() - trimmed.len();
if first_word && 0 < word_width {
// Never try to wrap the first word
first_word = false;
} else if self.hard_width < self.line_width + word_width {
if 0 < i {
let prev = i - 1;
let trimmed = words[prev].trim_end();
words[prev] = trimmed;
}
self.line_width = 0;
words.insert(i, "\n");
i += 1;
if let Some(indentation) = self.indentation {
words.insert(i, indentation);
self.line_width += indentation.len();
i += 1;
}
}
self.line_width += word_width + trimmed_delta;
i += 1;
}
words
}
}