#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Row {
pub line: usize,
pub start: usize,
pub end: usize,
}
impl Row {
#[must_use]
pub const fn is_line_start(&self) -> bool {
self.start == 0
}
#[must_use]
pub const fn width(&self) -> usize {
self.end - self.start
}
}
#[must_use]
pub fn rows<'a>(lines: impl Iterator<Item = &'a str>, width: usize, wrap: bool) -> Vec<Row> {
let mut rows = Vec::new();
for (line, text) in lines.enumerate() {
let length = text.chars().count();
if !wrap {
rows.push(Row {
line,
start: 0,
end: length,
});
continue;
}
let characters: Vec<char> = text.chars().collect();
let mut start = 0;
while length - start > width.max(1) {
let limit = start + width.max(1);
let end = characters
.iter()
.take(limit + 1)
.skip(start + 1)
.rposition(|character| *character == ' ')
.map_or(limit, |index| start + index + 2);
rows.push(Row { line, start, end });
start = end;
}
rows.push(Row {
line,
start,
end: length,
});
}
rows
}
#[must_use]
pub fn visible_end(line: &str, row: Row) -> usize {
let length = line.chars().count();
if row.end == length {
return row.end;
}
line.chars()
.take(row.end)
.collect::<Vec<char>>()
.iter()
.rposition(|character| *character != ' ')
.map_or(row.start, |index| (index + 1).max(row.start))
}
#[must_use]
pub fn locate(rows: &[Row], line: usize, column: usize) -> (usize, usize) {
let mut last = 0;
for (index, row) in rows.iter().enumerate() {
if row.line != line {
continue;
}
last = index;
if column < row.end {
return (index, column.saturating_sub(row.start));
}
}
(
last,
column.saturating_sub(rows.get(last).map_or(0, |row| row.start)),
)
}
#[cfg(test)]
mod tests {
use super::{Row, locate, rows};
fn layout(text: &str, width: usize) -> Vec<Row> {
rows(text.split('\n'), width, true)
}
#[test]
fn a_line_that_fits_stays_one_row() {
assert_eq!(
layout("hello", 10),
vec![Row {
line: 0,
start: 0,
end: 5
}]
);
}
#[test]
fn a_long_line_breaks_after_a_space() {
let laid_out = layout("one two three", 8);
assert_eq!(laid_out.len(), 2);
assert_eq!(laid_out[0].end, 8);
assert_eq!(laid_out[1].start, 8);
assert_eq!(laid_out[1].end, 13);
}
#[test]
fn a_word_too_long_to_fit_breaks_mid_word() {
let laid_out = layout("abcdefghij", 4);
assert_eq!(laid_out.len(), 3);
assert_eq!(
laid_out[0],
Row {
line: 0,
start: 0,
end: 4
}
);
assert_eq!(
laid_out[2],
Row {
line: 0,
start: 8,
end: 10
}
);
}
#[test]
fn rows_of_a_line_leave_no_column_uncovered() {
for row in layout("a bb ccc dddd eeeee", 6).windows(2) {
assert_eq!(row[0].end, row[1].start);
}
}
#[test]
fn every_line_keeps_its_own_rows() {
let laid_out = layout("short\nlonger than that", 8);
assert_eq!(laid_out[0].line, 0);
assert!(laid_out[1..].iter().all(|row| row.line == 1));
}
#[test]
fn without_wrapping_a_line_is_one_row_however_long() {
assert_eq!(
rows("a very long line indeed".split('\n'), 4, false).len(),
1
);
}
#[test]
fn a_caret_at_a_wrap_point_belongs_to_the_row_below() {
let laid_out = layout("one two three", 8);
assert_eq!(locate(&laid_out, 0, 8), (1, 0));
assert_eq!(locate(&laid_out, 0, 7), (0, 7));
}
#[test]
fn the_visible_end_of_a_broken_row_sits_before_its_trailing_space() {
let laid_out = layout("one two three", 8);
assert_eq!(super::visible_end("one two three", laid_out[0]), 7);
assert_eq!(locate(&laid_out, 0, 7), (0, 7));
}
#[test]
fn the_visible_end_of_a_last_row_is_the_line_end() {
let laid_out = layout("one two three", 8);
assert_eq!(super::visible_end("one two three", laid_out[1]), 13);
}
#[test]
fn a_caret_at_the_very_end_stays_on_the_last_row() {
let laid_out = layout("one two three", 8);
assert_eq!(locate(&laid_out, 0, 13), (1, 5));
}
}