use crate::style::TextWrap;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
#[cfg(debug_assertions)]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {
{
use std::fs::OpenOptions;
use std::io::Write;
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open("/tmp/radical_debug.log")
{
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let _ = writeln!(file, "[{}] {}", timestamp, format!($($arg)*));
}
}
};
}
#[cfg(not(debug_assertions))]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {};
}
pub fn display_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
pub fn char_width(c: char) -> usize {
UnicodeWidthChar::width(c).unwrap_or(0)
}
pub fn truncate_to_width(s: &str, max_width: usize) -> &str {
if max_width == 0 {
return "";
}
let mut width = 0;
let mut end_byte = 0;
for (byte_idx, ch) in s.char_indices() {
let ch_width = char_width(ch);
if width + ch_width > max_width {
break;
}
width += ch_width;
end_byte = byte_idx + ch.len_utf8();
}
&s[..end_byte]
}
pub fn byte_index_from_column(s: &str, target_column: usize) -> Option<usize> {
let mut current_column = 0;
for (byte_idx, ch) in s.char_indices() {
if current_column == target_column {
return Some(byte_idx);
}
let ch_width = char_width(ch);
if current_column + ch_width > target_column {
return None;
}
current_column += ch_width;
}
if current_column == target_column {
Some(s.len())
} else {
None
}
}
pub fn pad_to_width(s: &str, target_width: usize) -> String {
let current_width = display_width(s);
if current_width >= target_width {
s.to_string()
} else {
let padding = target_width - current_width;
format!("{}{}", s, " ".repeat(padding))
}
}
pub fn substring_by_columns(s: &str, start_col: usize, end_col: usize) -> &str {
if start_col >= end_col {
return "";
}
let mut current_col = 0;
let mut start_byte = None;
let mut end_byte = s.len();
for (byte_idx, ch) in s.char_indices() {
let ch_width = char_width(ch);
if start_byte.is_none() {
if current_col >= start_col {
start_byte = Some(byte_idx);
} else if current_col + ch_width > start_col {
start_byte = Some(byte_idx + ch.len_utf8());
}
}
if current_col >= end_col {
end_byte = byte_idx;
break;
} else if current_col + ch_width > end_col {
end_byte = byte_idx;
break;
}
current_col += ch_width;
}
let start = start_byte.unwrap_or(s.len());
if start >= end_byte {
""
} else {
&s[start..end_byte]
}
}
pub fn wrap_text(text: &str, width: u16, mode: TextWrap) -> Vec<String> {
if width == 0 {
return vec![];
}
match mode {
TextWrap::None => {
vec![text.to_string()]
}
TextWrap::Character => {
wrap_character(text, width)
}
TextWrap::Word => {
wrap_word(text, width)
}
TextWrap::WordBreak => {
wrap_word_break(text, width)
}
}
}
fn wrap_character(text: &str, width: u16) -> Vec<String> {
let width = width as usize;
let mut lines = Vec::new();
if text.is_empty() {
return vec![String::new()];
}
let mut current_line = String::new();
let mut current_width = 0;
for ch in text.chars() {
let ch_width = char_width(ch);
if current_width + ch_width > width && !current_line.is_empty() {
lines.push(current_line);
current_line = String::new();
current_width = 0;
}
if current_width + ch_width <= width || current_line.is_empty() {
current_line.push(ch);
current_width += ch_width;
} else {
lines.push(current_line);
current_line = ch.to_string();
current_width = ch_width;
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
fn wrap_word(text: &str, width: u16) -> Vec<String> {
let width = width as usize;
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
let mut in_word = false;
let mut word = String::new();
let mut word_width = 0;
let mut pending_spaces = String::new();
let mut pending_spaces_width = 0;
for ch in text.chars() {
if ch.is_whitespace() {
if in_word {
if current_width == 0 {
current_line.push_str(&word);
current_width = word_width;
} else if current_width + word_width <= width {
current_line.push_str(&word);
current_width += word_width;
} else {
lines.push(current_line.clone());
current_line = word.clone();
current_width = word_width;
}
word.clear();
word_width = 0;
in_word = false;
}
pending_spaces.push(ch);
pending_spaces_width += char_width(ch);
} else {
if !pending_spaces.is_empty() {
if current_width + pending_spaces_width <= width {
current_line.push_str(&pending_spaces);
current_width += pending_spaces_width;
} else {
if current_width > 0 {
lines.push(current_line.clone());
current_line = pending_spaces.clone();
current_width = pending_spaces_width;
} else {
current_line.push_str(&pending_spaces);
current_width = pending_spaces_width;
}
}
pending_spaces.clear();
pending_spaces_width = 0;
}
in_word = true;
word.push(ch);
word_width += char_width(ch);
}
}
if in_word {
if current_width == 0 || current_width + word_width <= width {
current_line.push_str(&word);
} else {
lines.push(current_line.clone());
current_line = word;
}
}
if !pending_spaces.is_empty() {
if current_width + pending_spaces_width <= width {
current_line.push_str(&pending_spaces);
} else if current_width > 0 {
lines.push(current_line.clone());
current_line = pending_spaces;
} else {
current_line.push_str(&pending_spaces);
}
}
if !current_line.is_empty() || lines.is_empty() {
lines.push(current_line);
}
lines
}
fn wrap_word_break(text: &str, width: u16) -> Vec<String> {
let width = width as usize;
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0;
let chars = text.chars();
let mut in_word = false;
let mut word = String::new();
let mut word_width = 0;
for ch in chars {
if ch.is_whitespace() {
if in_word {
if current_width == 0 {
if word_width <= width {
current_line.push_str(&word);
current_width = word_width;
} else {
for word_ch in word.chars() {
let ch_width = char_width(word_ch);
if current_width + ch_width > width && current_width > 0 {
lines.push(current_line.clone());
current_line.clear();
current_width = 0;
}
current_line.push(word_ch);
current_width += ch_width;
}
}
} else if current_width + word_width <= width {
current_line.push_str(&word);
current_width += word_width;
} else {
lines.push(current_line.clone());
current_line.clear();
current_width = 0;
if word_width <= width {
current_line.push_str(&word);
current_width = word_width;
} else {
for word_ch in word.chars() {
let ch_width = char_width(word_ch);
if current_width + ch_width > width && current_width > 0 {
lines.push(current_line.clone());
current_line.clear();
current_width = 0;
}
current_line.push(word_ch);
current_width += ch_width;
}
}
}
word.clear();
word_width = 0;
in_word = false;
}
let ch_width = char_width(ch);
if current_width + ch_width > width && current_width > 0 {
lines.push(current_line.clone());
current_line.clear();
current_line.push(ch);
current_width = ch_width;
} else {
current_line.push(ch);
current_width += ch_width;
}
} else {
in_word = true;
word.push(ch);
word_width += char_width(ch);
}
}
if in_word {
if current_width == 0 {
if word_width <= width {
current_line.push_str(&word);
} else {
for word_ch in word.chars() {
let ch_width = char_width(word_ch);
if current_width + ch_width > width && current_width > 0 {
lines.push(current_line.clone());
current_line.clear();
current_width = 0;
}
current_line.push(word_ch);
current_width += ch_width;
}
}
} else if current_width + word_width <= width {
current_line.push_str(&word);
} else {
lines.push(current_line.clone());
current_line.clear();
if word_width <= width {
current_line = word;
} else {
current_width = 0;
for word_ch in word.chars() {
let ch_width = char_width(word_ch);
if current_width + ch_width > width && current_width > 0 {
lines.push(current_line.clone());
current_line.clear();
current_width = 0;
}
current_line.push(word_ch);
current_width += ch_width;
}
}
}
}
if !current_line.is_empty() {
lines.push(current_line);
} else if lines.is_empty() {
lines.push(String::new());
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_width_ascii() {
assert_eq!(display_width("Hello"), 5);
assert_eq!(display_width(""), 0);
assert_eq!(display_width("Test 123"), 8);
}
#[test]
fn test_display_width_unicode() {
assert_eq!(display_width("世界"), 4);
assert_eq!(display_width("Hello 世界"), 10);
assert_eq!(display_width("😀"), 2);
assert_eq!(display_width("Test 😀"), 7);
}
#[test]
fn test_char_width() {
assert_eq!(char_width('A'), 1);
assert_eq!(char_width('世'), 2);
assert_eq!(char_width('😀'), 2);
assert_eq!(char_width('\0'), 0); }
#[test]
fn test_truncate_to_width() {
assert_eq!(truncate_to_width("Hello World", 5), "Hello");
assert_eq!(truncate_to_width("Hello", 10), "Hello");
assert_eq!(truncate_to_width("Hello 世界", 7), "Hello ");
assert_eq!(truncate_to_width("世界", 2), "世");
assert_eq!(truncate_to_width("世界", 3), "世"); assert_eq!(truncate_to_width("世界", 4), "世界");
}
#[test]
fn test_byte_index_from_column() {
let text = "Hello";
assert_eq!(byte_index_from_column(text, 0), Some(0));
assert_eq!(byte_index_from_column(text, 3), Some(3));
assert_eq!(byte_index_from_column(text, 5), Some(5));
assert_eq!(byte_index_from_column(text, 6), None);
let text = "世界";
assert_eq!(byte_index_from_column(text, 0), Some(0));
assert_eq!(byte_index_from_column(text, 1), None); assert_eq!(byte_index_from_column(text, 2), Some(3)); assert_eq!(byte_index_from_column(text, 4), Some(6)); }
#[test]
fn test_pad_to_width() {
assert_eq!(pad_to_width("Hi", 5), "Hi ");
assert_eq!(pad_to_width("Hello", 5), "Hello");
assert_eq!(pad_to_width("Hello", 3), "Hello");
assert_eq!(pad_to_width("世", 5), "世 ");
assert_eq!(pad_to_width("Hi世", 6), "Hi世 ");
}
#[test]
fn test_substring_by_columns() {
assert_eq!(substring_by_columns("Hello World", 0, 5), "Hello");
assert_eq!(substring_by_columns("Hello World", 6, 11), "World");
assert_eq!(substring_by_columns("Hello World", 3, 8), "lo Wo");
assert_eq!(substring_by_columns("Hello 世界", 0, 6), "Hello ");
assert_eq!(substring_by_columns("Hello 世界", 6, 10), "世界");
assert_eq!(substring_by_columns("Hello 世界", 0, 8), "Hello 世");
assert_eq!(substring_by_columns("Hello 世界", 7, 10), "界"); assert_eq!(substring_by_columns("Hello 世界", 0, 7), "Hello ");
assert_eq!(substring_by_columns("Test😀End", 0, 4), "Test");
assert_eq!(substring_by_columns("Test😀End", 4, 6), "😀");
assert_eq!(substring_by_columns("Test😀End", 6, 9), "End");
assert_eq!(substring_by_columns("Test😀End", 0, 5), "Test"); assert_eq!(substring_by_columns("Test😀End", 5, 9), "End");
assert_eq!(substring_by_columns("", 0, 5), "");
assert_eq!(substring_by_columns("Hello", 5, 5), "");
assert_eq!(substring_by_columns("Hello", 10, 20), "");
}
#[test]
fn test_wrap_none() {
let text = "This is a very long line that should not be wrapped";
let wrapped = wrap_text(text, 10, TextWrap::None);
assert_eq!(wrapped, vec![text]);
}
#[test]
fn test_wrap_character() {
let text = "Hello World";
let wrapped = wrap_text(text, 5, TextWrap::Character);
assert_eq!(wrapped, vec!["Hello", " Worl", "d"]);
}
#[test]
fn test_wrap_character_exact() {
let text = "12345678901234567890";
let wrapped = wrap_text(text, 10, TextWrap::Character);
assert_eq!(wrapped, vec!["1234567890", "1234567890"]);
}
#[test]
fn test_wrap_word() {
let text = "The quick brown fox jumps";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(wrapped, vec!["The quick ", "brown fox ", "jumps"]);
}
#[test]
fn test_wrap_word_long_word() {
let text = "A verylongword that exceeds width";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(
wrapped,
vec!["A ", "verylongword", " that ", "exceeds ", "width"]
);
}
#[test]
fn test_wrap_word_break() {
let text = "A verylongword that exceeds";
let wrapped = wrap_text(text, 10, TextWrap::WordBreak);
assert_eq!(wrapped, vec!["A ", "verylongwo", "rd that ", "exceeds"]);
}
#[test]
fn test_wrap_word_break_preserves_leading_spaces() {
let text = " _ => calculate";
let wrapped = wrap_text(text, 15, TextWrap::WordBreak);
assert_eq!(wrapped, vec![" _ => ", "calculate"]);
}
#[test]
fn test_wrap_empty_text() {
assert_eq!(wrap_text("", 10, TextWrap::Character), vec![""]);
assert_eq!(wrap_text("", 10, TextWrap::Word), vec![""]);
assert_eq!(wrap_text("", 10, TextWrap::WordBreak), vec![""]);
}
#[test]
fn test_wrap_zero_width() {
let text = "Hello";
assert_eq!(
wrap_text(text, 0, TextWrap::Character),
Vec::<String>::new()
);
assert_eq!(wrap_text(text, 0, TextWrap::Word), Vec::<String>::new());
}
#[test]
fn test_wrap_unicode() {
let text = "Hello 世界";
let wrapped = wrap_text(text, 8, TextWrap::Character);
assert_eq!(wrapped, vec!["Hello 世", "界"]);
let wrapped = wrap_text(text, 7, TextWrap::Character);
assert_eq!(wrapped, vec!["Hello ", "世界"]);
let text = "Hello 世界 World";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(wrapped, vec!["Hello 世界", " World"]); }
#[test]
fn test_wrap_emoji() {
let text = "Test 😀 emoji";
let wrapped = wrap_text(text, 8, TextWrap::Character);
assert_eq!(wrapped, vec!["Test 😀 ", "emoji"]);
let wrapped = wrap_text(text, 7, TextWrap::Word);
assert_eq!(wrapped, vec!["Test 😀", " emoji"]); }
#[test]
fn test_wrap_word_multiple_spaces() {
let text = "Hello World Test";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(wrapped, vec!["Hello ", "World ", "Test"]);
}
#[test]
fn test_wrap_preserves_leading_spaces() {
let text = " Hello World";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(wrapped, vec![" Hello ", "World"]);
}
#[test]
fn test_wrap_preserves_trailing_spaces() {
let text = "Hello World ";
let wrapped = wrap_text(text, 10, TextWrap::Word);
assert_eq!(wrapped, vec!["Hello ", "World "]);
}
}