use crate::text_buffer::TextBuffer;
use unicode_width::UnicodeWidthChar;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WrapLine {
pub text: String,
pub logical_line: usize,
pub start_col: usize,
}
#[derive(Clone, Debug)]
pub struct WrapResult {
pub lines: Vec<WrapLine>,
pub line_number_width: u16,
}
pub fn wrap_line(text: &str, width: usize) -> Vec<(String, usize)> {
if width == 0 {
return vec![(text.to_string(), 0)];
}
if text.is_empty() {
return vec![(String::new(), 0)];
}
let mut result = Vec::new();
let mut current_line = String::new();
let mut current_width: usize = 0;
let mut line_start_col: usize = 0;
for (char_col, ch) in text.chars().enumerate() {
let ch_width = ch.width().unwrap_or(0);
if current_width + ch_width > width && !current_line.is_empty() {
if let Some(space_byte_idx) = find_last_space(¤t_line) {
let before: String = current_line[..space_byte_idx].to_string();
let after: String = current_line[space_byte_idx..].trim_start().to_string();
let before_char_count = before.chars().count();
result.push((before, line_start_col));
current_width = display_width_of(&after);
line_start_col +=
before_char_count + count_trimmed_spaces(¤t_line[space_byte_idx..]);
current_line = after;
} else {
result.push((current_line.clone(), line_start_col));
line_start_col = char_col;
current_line = String::new();
current_width = 0;
}
}
current_line.push(ch);
current_width += ch_width;
}
if !current_line.is_empty() || result.is_empty() {
result.push((current_line, line_start_col));
}
result
}
pub fn wrap_lines(buffer: &TextBuffer, width: usize) -> WrapResult {
let total_lines = buffer.line_count();
let mut lines = Vec::new();
for line_idx in 0..total_lines {
if let Some(line_text) = buffer.line(line_idx) {
let wrapped = wrap_line(&line_text, width);
for (text, start_col) in wrapped {
lines.push(WrapLine {
text,
logical_line: line_idx,
start_col,
});
}
}
}
let lnw = line_number_width(total_lines);
WrapResult {
lines,
line_number_width: lnw,
}
}
pub fn line_number_width(line_count: usize) -> u16 {
if line_count == 0 {
return 1;
}
let digits = (line_count as f64).log10().floor() as u16 + 1;
digits.max(1)
}
fn display_width_of(text: &str) -> usize {
text.chars().map(|c| c.width().unwrap_or(0)).sum()
}
fn find_last_space(text: &str) -> Option<usize> {
text.rfind(' ')
}
fn count_trimmed_spaces(text: &str) -> usize {
text.chars().take_while(|c| *c == ' ').count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_line_no_wrap() {
let result = wrap_line("hello", 20);
assert!(result.len() == 1);
assert!(result[0].0 == "hello");
assert!(result[0].1 == 0);
}
#[test]
fn exact_width_no_wrap() {
let result = wrap_line("12345", 5);
assert!(result.len() == 1);
assert!(result[0].0 == "12345");
}
#[test]
fn overflow_by_one_char() {
let result = wrap_line("123456", 5);
assert!(result.len() == 2);
}
#[test]
fn word_wrap() {
let result = wrap_line("hello world foo", 12);
assert!(result.len() == 2);
assert!(result[0].0 == "hello world");
assert!(result[1].0 == "foo");
}
#[test]
fn long_word_break() {
let result = wrap_line("abcdefghij", 5);
assert!(result.len() == 2);
assert!(result[0].0 == "abcde");
assert!(result[1].0 == "fghij");
}
#[test]
fn cjk_characters_width_2() {
let result = wrap_line("日本語テスト", 6);
assert!(result.len() == 2);
assert!(result[0].0 == "日本語");
assert!(result[1].0 == "テスト");
}
#[test]
fn mixed_content() {
let result = wrap_line("abc日本", 5);
assert!(result.len() == 2);
assert!(result[0].0 == "abc日");
assert!(result[1].0 == "本");
}
#[test]
fn empty_line() {
let result = wrap_line("", 10);
assert!(result.len() == 1);
assert!(result[0].0.is_empty());
}
#[test]
fn single_char_line() {
let result = wrap_line("x", 10);
assert!(result.len() == 1);
assert!(result[0].0 == "x");
}
#[test]
fn line_number_width_small() {
assert!(line_number_width(1) == 1);
assert!(line_number_width(9) == 1);
}
#[test]
fn line_number_width_medium() {
assert!(line_number_width(10) == 2);
assert!(line_number_width(99) == 2);
assert!(line_number_width(100) == 3);
}
#[test]
fn line_number_width_zero() {
assert!(line_number_width(0) == 1);
}
#[test]
fn wrap_buffer_multiline() {
let buf = TextBuffer::from_text("short\nthis is a longer line");
let result = wrap_lines(&buf, 10);
assert!(result.lines.len() >= 3);
assert!(result.lines[0].logical_line == 0);
assert!(result.lines[1].logical_line == 1);
}
#[test]
fn wrap_result_line_number_width() {
let buf = TextBuffer::from_text("a\nb\nc\nd\ne\nf\ng\nh\ni\nj");
let result = wrap_lines(&buf, 80);
assert!(result.line_number_width == 2); }
}