use unicode_width::UnicodeWidthChar;
pub(super) fn wrap_ranges(chars: &[char], width: usize) -> Vec<(usize, usize)> {
let width = width.max(1);
let len = chars.len();
let mut rows = Vec::new();
let mut line_start = 0;
loop {
let line_end = chars[line_start..]
.iter()
.position(|&c| c == '\n')
.map_or(len, |offset| line_start + offset);
wrap_logical(chars, line_start, line_end, width, &mut rows);
if line_end >= len {
break;
}
line_start = line_end + 1;
if line_start == len {
rows.push((line_start, line_start));
break;
}
}
if rows.is_empty() {
rows.push((0, 0));
}
rows
}
pub(super) fn wrap_logical(
chars: &[char],
from: usize,
to: usize,
width: usize,
rows: &mut Vec<(usize, usize)>,
) {
if from == to {
rows.push((from, from));
return;
}
let mut start = from;
while start < to {
let mut end = start;
let mut used = 0usize;
while end < to {
let char_width = chars[end].width().unwrap_or(0);
if end > start && used + char_width > width {
break;
}
used += char_width;
end += 1;
}
if end >= to {
rows.push((start, to));
return;
}
match (start + 1..=end).rev().find(|&p| chars[p - 1] == ' ') {
Some(p) if p - 1 > start => {
rows.push((start, p - 1));
start = p;
}
_ => {
rows.push((start, end));
start = end;
}
}
}
}
#[must_use]
pub fn wrap_offsets(text: &str, width: usize) -> Vec<(String, usize)> {
let chars: Vec<char> = text.chars().collect();
wrap_ranges(&chars, width)
.into_iter()
.map(|(start, end)| (chars[start..end].iter().collect(), start))
.collect()
}
#[must_use]
pub fn cursor_to_display(
lines: &[(String, usize)],
total: usize,
cursor: usize,
) -> (usize, usize) {
let cursor = cursor.min(total);
let mut line = 0;
for (index, (_, start)) in lines.iter().enumerate() {
if *start <= cursor {
line = index;
} else {
break;
}
}
let (text, start) = &lines[line];
(line, (cursor - start).min(text.chars().count()))
}
#[must_use]
pub fn display_to_cursor(
lines: &[(String, usize)],
line: usize,
col: usize,
) -> usize {
let (text, start) = &lines[line];
start + col.min(text.chars().count())
}
pub(super) fn locate(rows: &[(usize, usize)], pos: usize) -> (usize, usize) {
for (index, &(start, end)) in rows.iter().enumerate() {
if pos < end {
return (index, pos - start);
}
if pos == end {
match rows.get(index + 1) {
Some(&(next_start, _)) if next_start == pos => {}
_ => return (index, pos - start),
}
}
}
let last = rows.len() - 1;
(last, pos - rows[last].0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_breaks_on_width_and_newlines() {
let chars: Vec<char> = "abcdef\nxy".chars().collect();
assert_eq!(wrap_ranges(&chars, 4), vec![(0, 4), (4, 6), (7, 9)]);
}
#[test]
fn wrap_breaks_on_the_last_space_and_consumes_it() {
assert_eq!(
wrap_offsets("hello world", 8),
vec![("hello".to_string(), 0), ("world".to_string(), 6)],
);
}
#[test]
fn wrap_hard_splits_a_word_longer_than_the_width() {
assert_eq!(
wrap_offsets("abcdefgh", 3),
vec![
("abc".to_string(), 0),
("def".to_string(), 3),
("gh".to_string(), 6),
],
);
}
#[test]
fn display_round_trips_a_caret_through_a_soft_break() {
let text = "hello world";
let rows = wrap_offsets(text, 8);
let total = text.chars().count();
assert_eq!(cursor_to_display(&rows, total, 6), (1, 0));
assert_eq!(display_to_cursor(&rows, 1, 0), 6);
assert_eq!(cursor_to_display(&rows, total, 5), (0, 5));
assert_eq!(display_to_cursor(&rows, 0, 5), 5);
}
#[test]
fn empty_buffer_has_one_row() {
assert_eq!(wrap_ranges(&[], 4), vec![(0, 0)]);
}
#[test]
fn wrap_measures_display_width_of_wide_chars() {
let chars: Vec<char> = "世界a".chars().collect();
assert_eq!(wrap_ranges(&chars, 3), vec![(0, 1), (1, 3)]);
}
#[test]
fn trailing_newline_adds_empty_row() {
let chars: Vec<char> = "ab\n".chars().collect();
assert_eq!(wrap_ranges(&chars, 4), vec![(0, 2), (3, 3)]);
}
}