use ratatui::text::Span;
use unicode_width::UnicodeWidthStr;
pub(in crate::pager) struct WrappedLine {
pub spans: Vec<Span<'static>>,
pub source_range: std::ops::Range<usize>,
pub indent: usize,
}
impl WrappedLine {
pub fn unwrapped(spans: Vec<Span<'static>>) -> Self {
let len = spans.iter().map(|span| span.content.len()).sum();
Self {
spans,
source_range: 0..len,
indent: 0,
}
}
}
pub(in crate::pager) fn wrap_spans_with_ranges(
spans: &[Span<'static>],
max_width: usize,
continuation_indent: usize,
) -> Vec<WrappedLine> {
use ratatui::style::Style;
use unicode_width::UnicodeWidthChar;
if max_width == 0 {
return vec![WrappedLine::unwrapped(spans.to_vec())];
}
let total_width: usize = spans
.iter()
.map(|s| UnicodeWidthStr::width(s.content.as_ref()))
.sum();
if total_width <= max_width {
return vec![WrappedLine::unwrapped(spans.to_vec())];
}
struct CI {
ch: char,
width: usize,
style: Style,
is_space: bool,
is_cjk: bool,
byte_offset: usize,
}
fn build_line(chars: &[CI], is_continuation: bool, indent: usize) -> WrappedLine {
let source_start = chars.first().map_or(0, |ch| ch.byte_offset);
let source_end = chars
.last()
.map_or(source_start, |ch| ch.byte_offset + ch.ch.len_utf8());
let indent = if is_continuation { indent } else { 0 };
let mut spans: Vec<Span<'static>> = Vec::new();
if indent > 0 {
spans.push(Span::raw(" ".repeat(indent)));
}
if chars.is_empty() {
return WrappedLine {
spans,
source_range: source_start..source_end,
indent,
};
}
let mut cur_style = chars[0].style;
let mut cur_text = String::new();
for ci in chars {
if ci.style == cur_style {
cur_text.push(ci.ch);
} else {
if !cur_text.is_empty() {
spans.push(Span::styled(cur_text, cur_style));
cur_text = String::new();
}
cur_style = ci.style;
cur_text.push(ci.ch);
}
}
if !cur_text.is_empty() {
spans.push(Span::styled(cur_text, cur_style));
}
WrappedLine {
spans,
source_range: source_start..source_end,
indent,
}
}
let mut chars: Vec<CI> = Vec::new();
let mut byte_offset = 0;
for span in spans {
let style = span.style;
for ch in span.content.chars() {
let w = UnicodeWidthChar::width(ch).unwrap_or(0);
chars.push(CI {
ch,
width: w,
style,
is_space: ch == ' ',
is_cjk: w == 2,
byte_offset,
});
byte_offset += ch.len_utf8();
}
}
let mut result: Vec<WrappedLine> = Vec::new();
let mut pos = 0;
while pos < chars.len() {
let is_continuation = !result.is_empty();
let line_width = if is_continuation {
max_width.saturating_sub(continuation_indent)
} else {
max_width
};
if line_width == 0 {
result.push(build_line(&chars[pos..pos + 1], false, 0));
pos += 1;
continue;
}
let mut col = 0;
let mut end = pos;
let mut last_break = None;
while end < chars.len() {
let ci = &chars[end];
if col + ci.width > line_width {
break;
}
col += ci.width;
end += 1;
if ci.is_space || (ci.is_cjk && end < chars.len()) {
last_break = Some(end);
}
}
if end == chars.len() {
result.push(build_line(
&chars[pos..end],
is_continuation,
continuation_indent,
));
break;
}
let split = if let Some(bp) = last_break {
if bp > pos { bp } else { end.max(pos + 1) }
} else {
end.max(pos + 1)
};
let mut trim_end = split;
while trim_end > pos && chars[trim_end - 1].is_space {
trim_end -= 1;
}
result.push(build_line(
&chars[pos..trim_end],
is_continuation,
continuation_indent,
));
pos = split;
while pos < chars.len() && chars[pos].is_space {
pos += 1;
}
}
if result.is_empty() {
vec![WrappedLine::unwrapped(spans.to_vec())]
} else {
result
}
}