use super::*;
pub(crate) fn ratatui_wrapped_visual_rows(text: &str, wrap_width: u16) -> usize {
let lines = text
.split('\n')
.map(|line| Line::from(Span::raw(line.to_string())))
.collect::<Vec<_>>();
ratatui_wrapped_line_rows(lines, wrap_width)
}
pub(crate) fn ratatui_wrapped_line_rows(lines: Vec<Line<'static>>, wrap_width: u16) -> usize {
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.line_count(wrap_width.max(1))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct VisualRow {
pub(super) start: usize,
pub(super) end: usize,
pub(super) columns: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct VisualUnit {
pub(super) start: usize,
pub(super) end: usize,
pub(super) columns: usize,
}
pub(crate) fn text_position_for_visual_cell(
text: &str,
row: usize,
column: usize,
wrap_width: u16,
) -> Option<crate::tui::selection::TextPosition> {
let rows = visual_row_ranges(text, wrap_width);
let selected = rows.get(row).or_else(|| rows.last())?;
let byte = byte_index_for_row_column(text, *selected, column);
Some(crate::tui::selection::TextPosition::new(byte, row, column))
}
pub(crate) fn text_position_for_visual_cell_with_leading_columns(
text: &str,
leading_columns: usize,
row: usize,
column: usize,
wrap_width: u16,
) -> Option<crate::tui::selection::TextPosition> {
if leading_columns == 0 {
return text_position_for_visual_cell(text, row, column, wrap_width);
}
let rows = visual_row_ranges_with_leading_columns(text, leading_columns, wrap_width);
let selected = rows.get(row).or_else(|| rows.last())?;
let byte =
byte_index_for_row_column_with_leading_columns(text, leading_columns, *selected, column);
Some(crate::tui::selection::TextPosition::new(byte, row, column))
}
pub(super) fn visual_row_ranges(text: &str, wrap_width: u16) -> Vec<VisualRow> {
let wrap_width = usize::from(wrap_width.max(1));
let mut rows = Vec::new();
let mut line_start = 0usize;
let mut saw = false;
for segment in text.split_inclusive('\n') {
saw = true;
let line = segment.strip_suffix('\n').unwrap_or(segment);
push_visual_line_rows(
text,
line_start,
line_start + line.len(),
wrap_width,
&mut rows,
);
line_start += segment.len();
if segment.ends_with('\n') && line_start == text.len() {
rows.push(VisualRow {
start: line_start,
end: line_start,
columns: 0,
});
}
}
if !saw {
rows.push(VisualRow {
start: 0,
end: 0,
columns: 0,
});
} else if line_start < text.len() {
push_visual_line_rows(text, line_start, text.len(), wrap_width, &mut rows);
}
rows
}
fn visual_row_ranges_with_leading_columns(
text: &str,
leading_columns: usize,
wrap_width: u16,
) -> Vec<VisualRow> {
let wrap_width = usize::from(wrap_width.max(1));
let mut rows = Vec::new();
let mut line_start = 0usize;
let mut saw = false;
for segment in text.split_inclusive('\n') {
saw = true;
let line = segment.strip_suffix('\n').unwrap_or(segment);
let leading = if line_start == 0 { leading_columns } else { 0 };
push_visual_line_rows_with_leading_columns(
text,
line_start,
line_start + line.len(),
leading_columns,
leading,
wrap_width,
&mut rows,
);
line_start += segment.len();
if segment.ends_with('\n') && line_start == text.len() {
rows.push(VisualRow {
start: leading_columns + line_start,
end: leading_columns + line_start,
columns: 0,
});
}
}
if !saw {
push_visual_line_rows_with_leading_columns(
text,
0,
0,
leading_columns,
leading_columns,
wrap_width,
&mut rows,
);
} else if line_start < text.len() {
push_visual_line_rows_with_leading_columns(
text,
line_start,
text.len(),
leading_columns,
0,
wrap_width,
&mut rows,
);
}
rows
}
fn push_visual_line_rows(
text: &str,
line_start: usize,
line_end: usize,
wrap_width: usize,
rows: &mut Vec<VisualRow>,
) {
if line_start == line_end {
rows.push(VisualRow {
start: line_start,
end: line_end,
columns: 0,
});
return;
}
let mut line_width = 0usize;
let mut word_width = 0usize;
let mut whitespace_width = 0usize;
let mut pending_line: Vec<VisualUnit> = Vec::new();
let mut pending_word: Vec<VisualUnit> = Vec::new();
let mut pending_whitespace: VecDeque<VisualUnit> = VecDeque::new();
let mut non_whitespace_previous = false;
for (offset, grapheme) in text[line_start..line_end].grapheme_indices(true) {
if grapheme.chars().any(char::is_control) {
continue;
}
let start = line_start + offset;
let unit = VisualUnit {
start,
end: start + grapheme.len(),
columns: UnicodeWidthStr::width(grapheme),
};
let is_whitespace = grapheme.chars().all(char::is_whitespace);
let word_found = non_whitespace_previous && is_whitespace;
let untrimmed_overflow = pending_line.is_empty()
&& word_width
.saturating_add(whitespace_width)
.saturating_add(unit.columns)
> wrap_width;
if word_found || untrimmed_overflow {
line_width = line_width.saturating_add(whitespace_width);
pending_line.extend(pending_whitespace.drain(..));
whitespace_width = 0;
line_width = line_width.saturating_add(word_width);
pending_line.append(&mut pending_word);
word_width = 0;
}
let line_full = line_width >= wrap_width;
let pending_word_overflow = unit.columns > 0
&& line_width
.saturating_add(whitespace_width)
.saturating_add(word_width)
>= wrap_width;
if line_full || pending_word_overflow {
push_units_as_row(&pending_line, rows);
let mut remaining_width = wrap_width.saturating_sub(line_width);
pending_line.clear();
line_width = 0;
while let Some(front) = pending_whitespace.front().copied() {
if front.columns > remaining_width {
break;
}
whitespace_width = whitespace_width.saturating_sub(front.columns);
remaining_width = remaining_width.saturating_sub(front.columns);
pending_whitespace.pop_front();
}
if is_whitespace && pending_whitespace.is_empty() {
non_whitespace_previous = false;
continue;
}
}
if is_whitespace {
whitespace_width = whitespace_width.saturating_add(unit.columns);
pending_whitespace.push_back(unit);
} else {
word_width = word_width.saturating_add(unit.columns);
pending_word.push(unit);
}
non_whitespace_previous = !is_whitespace;
}
pending_line.extend(pending_whitespace);
pending_line.extend(pending_word);
if pending_line.is_empty() {
rows.push(VisualRow {
start: line_start,
end: line_end,
columns: 0,
});
} else {
push_units_as_row(&pending_line, rows);
}
}
fn push_visual_line_rows_with_leading_columns(
text: &str,
line_start: usize,
line_end: usize,
virtual_offset: usize,
leading_columns: usize,
wrap_width: usize,
rows: &mut Vec<VisualRow>,
) {
if line_start == line_end && leading_columns == 0 {
rows.push(VisualRow {
start: virtual_offset + line_start,
end: virtual_offset + line_end,
columns: 0,
});
return;
}
let mut line_width = 0usize;
let mut word_width = 0usize;
let mut whitespace_width = 0usize;
let mut pending_line: Vec<VisualUnit> = Vec::new();
let mut pending_word: Vec<VisualUnit> = Vec::new();
let mut pending_whitespace: VecDeque<VisualUnit> = VecDeque::new();
let mut non_whitespace_previous = false;
for index in 0..leading_columns {
push_visual_unit(
VisualUnit {
start: index,
end: index + 1,
columns: 1,
},
true,
wrap_width,
&mut line_width,
&mut word_width,
&mut whitespace_width,
&mut pending_line,
&mut pending_word,
&mut pending_whitespace,
&mut non_whitespace_previous,
rows,
);
}
for (offset, grapheme) in text[line_start..line_end].grapheme_indices(true) {
if grapheme.chars().any(char::is_control) {
continue;
}
let start = virtual_offset + line_start + offset;
push_visual_unit(
VisualUnit {
start,
end: start + grapheme.len(),
columns: UnicodeWidthStr::width(grapheme),
},
grapheme.chars().all(char::is_whitespace),
wrap_width,
&mut line_width,
&mut word_width,
&mut whitespace_width,
&mut pending_line,
&mut pending_word,
&mut pending_whitespace,
&mut non_whitespace_previous,
rows,
);
}
pending_line.extend(pending_whitespace);
pending_line.extend(pending_word);
if pending_line.is_empty() {
rows.push(VisualRow {
start: virtual_offset + line_start,
end: virtual_offset + line_end,
columns: 0,
});
} else {
push_units_as_row(&pending_line, rows);
}
}
#[expect(clippy::too_many_arguments)]
fn push_visual_unit(
unit: VisualUnit,
is_whitespace: bool,
wrap_width: usize,
line_width: &mut usize,
word_width: &mut usize,
whitespace_width: &mut usize,
pending_line: &mut Vec<VisualUnit>,
pending_word: &mut Vec<VisualUnit>,
pending_whitespace: &mut VecDeque<VisualUnit>,
non_whitespace_previous: &mut bool,
rows: &mut Vec<VisualRow>,
) {
let word_found = *non_whitespace_previous && is_whitespace;
let untrimmed_overflow = pending_line.is_empty()
&& word_width
.saturating_add(*whitespace_width)
.saturating_add(unit.columns)
> wrap_width;
if word_found || untrimmed_overflow {
*line_width = line_width.saturating_add(*whitespace_width);
pending_line.extend(pending_whitespace.drain(..));
*whitespace_width = 0;
*line_width = line_width.saturating_add(*word_width);
pending_line.append(pending_word);
*word_width = 0;
}
let line_full = *line_width >= wrap_width;
let pending_word_overflow = unit.columns > 0
&& line_width
.saturating_add(*whitespace_width)
.saturating_add(*word_width)
>= wrap_width;
if line_full || pending_word_overflow {
push_units_as_row(pending_line, rows);
let mut remaining_width = wrap_width.saturating_sub(*line_width);
pending_line.clear();
*line_width = 0;
while let Some(front) = pending_whitespace.front().copied() {
if front.columns > remaining_width {
break;
}
*whitespace_width = whitespace_width.saturating_sub(front.columns);
remaining_width = remaining_width.saturating_sub(front.columns);
pending_whitespace.pop_front();
}
if is_whitespace && pending_whitespace.is_empty() {
*non_whitespace_previous = false;
return;
}
}
if is_whitespace {
*whitespace_width = whitespace_width.saturating_add(unit.columns);
pending_whitespace.push_back(unit);
} else {
*word_width = word_width.saturating_add(unit.columns);
pending_word.push(unit);
}
*non_whitespace_previous = !is_whitespace;
}
fn push_units_as_row(units: &[VisualUnit], rows: &mut Vec<VisualRow>) {
if let (Some(first), Some(last)) = (units.first(), units.last()) {
rows.push(VisualRow {
start: first.start,
end: last.end,
columns: units.iter().map(|unit| unit.columns).sum(),
});
}
}
fn byte_index_for_row_column_with_leading_columns(
text: &str,
leading_columns: usize,
row: VisualRow,
column: usize,
) -> usize {
let target = column.min(row.columns);
if target == 0 {
return row.start.saturating_sub(leading_columns).min(text.len());
}
let mut width = 0usize;
for index in 0..leading_columns {
if index < row.start || index >= row.end {
continue;
}
let next_width = width.saturating_add(1);
if next_width > target {
return 0;
}
width = next_width;
if width == target {
return (index + 1).saturating_sub(leading_columns).min(text.len());
}
}
for (offset, grapheme) in text.grapheme_indices(true) {
let virtual_start = leading_columns + offset;
if virtual_start < row.start || virtual_start >= row.end {
continue;
}
if grapheme.chars().any(char::is_control) {
continue;
}
let next_width = width.saturating_add(UnicodeWidthStr::width(grapheme));
if next_width > target {
return offset;
}
width = next_width;
if width == target {
return offset + grapheme.len();
}
}
row.end.saturating_sub(leading_columns).min(text.len())
}
fn byte_index_for_row_column(text: &str, row: VisualRow, column: usize) -> usize {
let target = column.min(row.columns);
if target == 0 {
return row.start;
}
let mut width = 0usize;
for (offset, grapheme) in text[row.start..row.end].grapheme_indices(true) {
if grapheme.chars().any(char::is_control) {
continue;
}
let next_width = width.saturating_add(UnicodeWidthStr::width(grapheme));
if next_width > target {
return row.start + offset;
}
width = next_width;
if width == target {
return row.start + offset + grapheme.len();
}
}
row.end
}
pub(crate) fn usize_to_u16_saturating(value: usize) -> u16 {
u16::try_from(value).unwrap_or(u16::MAX)
}