use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use crate::text;
pub(crate) fn wrap(text: &str, width: u16) -> Vec<Range<usize>> {
let width = width.max(1);
let mut rows = Vec::new();
let mut line_start = 0;
for line in text.split('\n') {
let mut row_start = line_start;
let mut row_width = 0u16;
for (offset, word) in line.split_word_bound_indices() {
let start = line_start + offset;
let word_width = text::width(word);
if word.chars().all(char::is_whitespace) {
row_width = row_width.saturating_add(word_width);
continue;
}
if row_width.saturating_add(word_width) <= width {
row_width += word_width;
continue;
}
if row_width > 0 && word_width <= width {
rows.push(row_start..start);
row_start = start;
row_width = word_width;
continue;
}
for (g_offset, grapheme) in word.grapheme_indices(true) {
let g_width = text::grapheme_width(grapheme);
if row_width > 0 && row_width.saturating_add(g_width) > width {
rows.push(row_start..start + g_offset);
row_start = start + g_offset;
row_width = 0;
}
row_width = row_width.saturating_add(g_width);
}
}
rows.push(row_start..line_start + line.len());
line_start += line.len() + 1;
}
rows
}
pub(crate) fn ends_line(text: &str, row: &Range<usize>) -> bool {
row.end == text.len() || text.as_bytes()[row.end] == b'\n'
}
pub(crate) fn starts_line(text: &str, row: &Range<usize>) -> bool {
row.start == 0 || text.as_bytes()[row.start - 1] == b'\n'
}
pub(crate) fn locate(text: &str, rows: &[Range<usize>], offset: usize) -> (usize, u16) {
let row = rows.iter().rposition(|row| row.start <= offset).unwrap_or(0);
let start = rows.get(row).map_or(0, |row| row.start);
(row, text::width(&text[start..offset.clamp(start, text.len())]))
}
pub(crate) fn offset_at(text: &str, rows: &[Range<usize>], row: usize, column: u16) -> usize {
let Some(range) = rows.get(row) else {
return text.len();
};
let mut offset = range.start;
let mut cells = 0u16;
for (index, grapheme) in text[range.clone()].grapheme_indices(true) {
let g_width = text::grapheme_width(grapheme).max(1);
let next = range.start + index + grapheme.len();
if cells.saturating_add(g_width) > column || (next == range.end && !ends_line(text, range)) {
break;
}
cells += g_width;
offset = next;
}
offset
}
#[cfg(test)]
mod tests {
use super::*;
fn shown<'t>(text: &'t str, rows: &[Range<usize>]) -> Vec<&'t str> {
rows.iter().map(|row| &text[row.clone()]).collect()
}
#[test]
fn rows_cover_every_byte_and_keep_indentation() {
let text = "deploy the canary\n then wait\n\nsupercalifragilistic";
let rows = wrap(text, 8);
assert_eq!(
shown(text, &rows),
["deploy ", "the ", "canary", " then ", "wait", "", "supercal", "ifragili", "stic"]
);
assert!(ends_line(text, &rows[2]) && !ends_line(text, &rows[1]));
assert!(starts_line(text, &rows[3]) && !starts_line(text, &rows[4]));
assert_eq!(wrap("", 5).len(), 1);
assert!(wrap("", 5)[0].is_empty());
}
#[test]
fn offsets_and_cells_round_trip() {
let text = "çay demlendi\nbitti";
let rows = wrap(text, 9);
assert_eq!(shown(text, &rows), ["çay ", "demlendi", "bitti"]);
assert_eq!(locate(text, &rows, rows[1].start), (1, 0));
assert_eq!(locate(text, &rows, rows[1].end), (1, 8));
assert_eq!(offset_at(text, &rows, 0, 10), rows[0].end - 1, "stays before the wrap");
assert_eq!(offset_at(text, &rows, 2, 3), rows[2].start + 3);
assert_eq!(offset_at(text, &rows, 1, 99), rows[1].end, "a line end is reachable");
assert_eq!(offset_at(text, &rows, 9, 0), text.len());
}
}