#[derive(Debug, Clone)]
pub struct WrappedSegment {
pub text: String,
pub is_continuation: bool,
pub start_char_offset: usize,
pub end_char_offset: usize,
}
#[derive(Debug, Clone)]
pub struct WrapConfig {
pub first_line_width: usize,
pub continuation_line_width: usize,
pub gutter_width: usize,
}
impl WrapConfig {
pub fn new(content_area_width: usize, gutter_width: usize, has_scrollbar: bool) -> Self {
let scrollbar_width = if has_scrollbar { 1 } else { 0 };
let text_area_width = content_area_width
.saturating_sub(scrollbar_width)
.saturating_sub(gutter_width);
Self {
first_line_width: text_area_width,
continuation_line_width: text_area_width, gutter_width,
}
}
pub fn no_wrap(gutter_width: usize) -> Self {
Self {
first_line_width: usize::MAX,
continuation_line_width: usize::MAX,
gutter_width,
}
}
}
pub fn wrap_line(text: &str, config: &WrapConfig) -> Vec<WrappedSegment> {
let mut segments = Vec::new();
if text.is_empty() {
return vec![WrappedSegment {
text: String::new(),
is_continuation: false,
start_char_offset: 0,
end_char_offset: 0,
}];
}
let chars: Vec<char> = text.chars().collect();
let mut pos = 0; let mut is_first = true;
while pos < chars.len() {
let width = if is_first {
config.first_line_width
} else {
config.continuation_line_width
};
let segment_start_char = pos;
if pos >= chars.len() {
break;
}
let mut segment_len = 0;
let segment_text_start = pos;
while segment_len < width && pos < chars.len() {
segment_len += 1;
pos += 1;
}
let segment_text: String = chars[segment_text_start..pos].iter().collect();
segments.push(WrappedSegment {
text: segment_text,
is_continuation: !is_first,
start_char_offset: segment_start_char,
end_char_offset: pos,
});
is_first = false;
}
if segments.is_empty() {
segments.push(WrappedSegment {
text: String::new(),
is_continuation: false,
start_char_offset: 0,
end_char_offset: 0,
});
}
segments
}
pub fn char_position_to_segment(char_pos: usize, segments: &[WrappedSegment]) -> (usize, usize) {
if segments.is_empty() {
return (0, 0);
}
for (seg_idx, segment) in segments.iter().enumerate() {
if char_pos >= segment.start_char_offset && char_pos < segment.end_char_offset {
let offset_in_range = char_pos - segment.start_char_offset;
let segment_text_len = segment.text.chars().count();
let range_len = segment.end_char_offset - segment.start_char_offset;
let whitespace_skipped = range_len - segment_text_len;
let col = offset_in_range.saturating_sub(whitespace_skipped);
return (seg_idx, col);
}
}
let last_idx = segments.len() - 1;
let last_len = segments[last_idx].text.chars().count();
(last_idx, last_len)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wrap_empty_line() {
let config = WrapConfig::new(60, 8, true);
let segments = wrap_line("", &config);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].text, "");
assert_eq!(segments[0].is_continuation, false);
}
#[test]
fn test_wrap_short_line() {
let config = WrapConfig::new(60, 8, true);
let text = "Hello world";
let segments = wrap_line(text, &config);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].text, text);
assert_eq!(segments[0].is_continuation, false);
}
#[test]
fn test_wrap_long_line() {
let config = WrapConfig::new(60, 8, true);
let text = "A fast, lightweight terminal text editor written in Rust. Handles files of any size with instant startup, low memory usage, and modern IDE features.";
let segments = wrap_line(text, &config);
const SEG0: &str = "A fast, lightweight terminal text editor written in";
const SEG1: &str = " Rust. Handles files of any size with instant start";
const SEG2: &str = "up, low memory usage, and modern IDE features.";
assert_eq!(segments.len(), 3);
assert_eq!(segments[0].text, SEG0);
assert_eq!(segments[0].is_continuation, false);
assert_eq!(segments[1].text, SEG1);
assert_eq!(segments[1].is_continuation, true);
assert_eq!(segments[2].text, SEG2);
assert_eq!(segments[2].is_continuation, true);
assert_eq!(char_position_to_segment(0, &segments), (0, 0));
assert_eq!(char_position_to_segment(25, &segments), (0, 25));
assert_eq!(
char_position_to_segment(SEG0.chars().count() - 1, &segments),
(0, SEG0.chars().count() - 1)
);
assert_eq!(
char_position_to_segment(SEG0.chars().count(), &segments),
(1, 0)
);
let pos_in_seg1 = SEG0.chars().count() + 30;
assert_eq!(char_position_to_segment(pos_in_seg1, &segments), (1, 30));
let seg2_start = SEG0.chars().count() + SEG1.chars().count();
assert_eq!(char_position_to_segment(seg2_start, &segments), (2, 0));
let text_len = text.chars().count();
assert_eq!(
char_position_to_segment(text_len, &segments),
(2, SEG2.chars().count())
);
assert_eq!(
char_position_to_segment(text_len + 10, &segments),
(2, SEG2.chars().count())
);
}
#[test]
fn test_wrap_with_leading_space() {
let config = WrapConfig::new(60, 8, true);
let text = format!("{} {}", "A".repeat(51), "B".repeat(50));
let segments = wrap_line(&text, &config);
println!("segments: {:?}", segments);
assert_eq!(segments.len(), 2);
assert_eq!(
segments[0].text.chars().count(),
51,
"First segment should be 51 chars"
);
assert_eq!(segments[1].is_continuation, true);
assert_eq!(
segments[1].text.chars().count(),
51,
"Continuation should also be 51 chars"
);
}
#[test]
fn test_wrap_exact_width() {
let config = WrapConfig::new(60, 8, true);
println!(
"Config: first={}, cont={}",
config.first_line_width, config.continuation_line_width
);
let text = "A".repeat(config.first_line_width * 2);
let segments = wrap_line(&text, &config);
println!("Number of segments: {}", segments.len());
for (i, seg) in segments.iter().enumerate() {
println!(
"Segment {}: len={}, start={}, end={}",
i,
seg.text.len(),
seg.start_char_offset,
seg.end_char_offset
);
}
assert_eq!(
segments[0].text.len(),
config.first_line_width,
"First segment should have first_line_width characters"
);
if segments.len() > 1 {
assert_eq!(
segments[1].text.len(),
config.continuation_line_width,
"Second segment should have continuation_line_width characters (same as first!)"
);
}
}
#[test]
fn test_wrap_with_real_text() {
let config = WrapConfig::new(60, 8, true);
println!(
"Config: first={}, cont={}",
config.first_line_width, config.continuation_line_width
);
let text = "The quick brown fox jumps over the lazy dog and runs through the forest, exploring ancient trees and mysterious pathways that wind between towering oaks.";
println!("Text len: {}", text.len());
println!("Text[48..55]: {:?}", &text[48..55]);
let segments = wrap_line(&text, &config);
for (i, seg) in segments.iter().enumerate() {
println!(
"Segment {}: len={}, start={}, end={}, text[..10]={:?}",
i,
seg.text.len(),
seg.start_char_offset,
seg.end_char_offset,
&seg.text[..seg.text.len().min(10)]
);
}
assert_eq!(
segments[0].text.len(),
config.first_line_width,
"First segment should have {} chars but has {}",
config.first_line_width,
segments[0].text.len()
);
}
#[test]
fn test_wrap_config_widths() {
let config = WrapConfig::new(60, 8, true);
println!(
"Config: first_line_width={}, continuation_line_width={}, gutter_width={}",
config.first_line_width, config.continuation_line_width, config.gutter_width
);
assert_eq!(config.first_line_width, 51);
assert_eq!(
config.continuation_line_width, 51,
"Continuation lines should have same text width as first line!"
);
let text = "The quick brown fox jumps over the lazy dog and runs through the forest, exploring ancient trees and mysterious pathways that wind between towering oaks.";
let segments = wrap_line(text, &config);
println!("Text length: {}", text.len());
println!("Number of segments: {}", segments.len());
for (i, seg) in segments.iter().enumerate() {
println!(
"Segment {}: start={}, end={}, len={}, is_continuation={}",
i,
seg.start_char_offset,
seg.end_char_offset,
seg.text.len(),
seg.is_continuation
);
println!(" Text: {:?}", &seg.text[..seg.text.len().min(40)]);
}
let (seg_idx, col_in_seg) = char_position_to_segment(51, &segments);
println!(
"Position 51: segment_idx={}, col_in_segment={}",
seg_idx, col_in_seg
);
assert_eq!(seg_idx, 1, "Position 51 should be in segment 1");
assert_eq!(col_in_seg, 0, "Position 51 should be at start of segment 1");
}
}