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