use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub(crate) fn coerce_crlf(input: &str) -> Cow<'_, str> {
let mut result = Cow::Borrowed(input);
let mut cursor: usize = 0;
for (idx, _) in input.match_indices('\n') {
if !(idx > 0 && input.as_bytes()[idx - 1] == b'\r') {
match &mut result {
Cow::Borrowed(_) => {
let mut owned = String::with_capacity(input.len() + 1);
owned.push_str(&input[cursor..idx]);
owned.push_str("\r\n");
result = Cow::Owned(owned);
}
Cow::Owned(result) => {
result.push_str(&input[cursor..idx]);
result.push_str("\r\n");
}
}
cursor = idx + 1;
}
}
if let Cow::Owned(result) = &mut result {
result.push_str(&input[cursor..input.len()]);
}
result
}
pub(crate) fn strip_ansi(string: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(string))
.map_err(|_| ())
.unwrap_or_else(|_| string.to_owned())
}
pub(crate) fn estimate_required_lines(input: &str, screen_width: u16) -> usize {
input.lines().fold(0, |acc, line| {
let wrap = estimate_single_line_wraps(line, screen_width);
acc + 1 + wrap
})
}
pub(crate) fn estimate_single_line_wraps(line: &str, terminal_columns: u16) -> usize {
let terminal_columns: usize = terminal_columns.into();
if terminal_columns == 0 {
return 0;
}
let estimated_width = line_width(line);
let estimated_line_count = estimated_width.div_ceil(terminal_columns);
estimated_line_count.saturating_sub(1)
}
pub(crate) fn line_width(line: &str) -> usize {
strip_ansi(line).width()
}
pub(crate) fn deferred_wrap_row<'a>(
pieces: impl IntoIterator<Item = &'a str>,
terminal_columns: u16,
) -> Option<u16> {
let columns: usize = terminal_columns.into();
if columns == 0 {
return None;
}
let (mut row, mut col) = (0u16, 0usize);
for piece in pieces {
for grapheme in strip_ansi(piece).graphemes(true) {
match grapheme {
"\n" => (row, col) = (row.saturating_add(1), 0),
"\r" => col = 0,
_ => {
let width = grapheme.width();
if col >= columns {
(row, col) = (row.saturating_add(1), 0);
}
if col + width > columns {
(row, col) = (row.saturating_add(1), 0);
}
col += width;
}
}
}
}
(col >= columns).then(|| row.saturating_add(1))
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case("sentence\nsentence", "sentence\r\nsentence")]
#[case("sentence\r\nsentence", "sentence\r\nsentence")]
#[case("sentence\nsentence\n", "sentence\r\nsentence\r\n")]
#[case("😇\nsentence", "😇\r\nsentence")]
#[case("sentence\n😇", "sentence\r\n😇")]
#[case("\n", "\r\n")]
#[case("", "")]
fn test_coerce_crlf(#[case] input: &str, #[case] expected: &str) {
let result = coerce_crlf(input);
assert_eq!(result, expected);
assert!(
input != expected || matches!(result, Cow::Borrowed(_)),
"Unnecessary allocation"
)
}
#[rstest]
#[case("", 20, None)]
#[case("a", 20, None)]
#[case(&"a".repeat(19), 20, None)]
#[case(&"a".repeat(20), 20, Some(1))]
#[case(&"a".repeat(21), 20, None)]
#[case(&"a".repeat(40), 20, Some(2))]
#[case(&"a".repeat(60), 20, Some(3))]
#[case("ab\naaaaaaaaaaaaaaaaaaaa", 20, Some(2))]
#[case("ab\n", 20, None)]
#[case(&"a".repeat(20), 0, None)]
fn deferred_wrap_row_on_narrow_graphemes(
#[case] printed: &str,
#[case] columns: u16,
#[case] expected: Option<u16>,
) {
assert_eq!(deferred_wrap_row([printed], columns), expected);
}
#[rstest]
#[case(&"あ".repeat(21), 21, None)]
#[case(&"あ".repeat(10), 21, None)]
#[case(&"あ".repeat(10), 20, Some(1))]
#[case(&"あ".repeat(20), 20, Some(2))]
#[case(&"あ".repeat(9), 20, None)]
#[case(&format!("> {}", "あ".repeat(9)), 20, Some(1))]
#[case(&format!("> {}", "あ".repeat(10)), 20, None)]
#[case(&"あ".repeat(5), 5, None)]
#[case(&"あ".repeat(3), 3, None)]
#[case("あああaaa", 5, Some(2))]
fn deferred_wrap_row_on_wide_graphemes(
#[case] printed: &str,
#[case] columns: u16,
#[case] expected: Option<u16>,
) {
assert_eq!(deferred_wrap_row([printed], columns), expected);
}
#[rstest]
#[case(&format!("\x1b[31m{}\x1b[0m", "a".repeat(20)), 20, Some(1))]
#[case(&"e\u{301}".repeat(20), 20, Some(1))]
fn deferred_wrap_row_ignores_zero_width_input(
#[case] printed: &str,
#[case] columns: u16,
#[case] expected: Option<u16>,
) {
assert_eq!(deferred_wrap_row([printed], columns), expected);
}
#[test]
fn coerce_crlf_preserves_leading_replacement_before_later_newline() {
assert_eq!(coerce_crlf("\n::: 3\n::: 4"), "\r\n::: 3\r\n::: 4");
}
#[test]
fn estimate_single_line_wraps_zero_columns_does_not_panic() {
assert_eq!(estimate_single_line_wraps("hello world", 0), 0);
assert_eq!(estimate_single_line_wraps("", 0), 0);
}
#[rstest]
#[case("", 80, 0)]
#[case("hello", 80, 0)]
#[case("abcdefghij", 5, 1)]
#[case("abcdefghijk", 5, 2)]
fn estimate_single_line_wraps_basic(
#[case] line: &str,
#[case] columns: u16,
#[case] expected: usize,
) {
assert_eq!(estimate_single_line_wraps(line, columns), expected);
}
}