Skip to main content

idet_core/
wrap.rs

1//! Breaking long lines into the screen rows that show them, for a frontend
2//! that lays out text itself.
3
4/// One screen row: the document line it belongs to, and the range of that
5/// line's characters it shows.
6///
7/// Rows of one line are contiguous and cover it completely, so every column
8/// belongs to exactly one row and a caret always has a row to sit on.
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub struct Row {
11    /// The index of the document line this row is part of.
12    pub line: usize,
13    /// The first column of the line shown here.
14    pub start: usize,
15    /// One past the last column shown here.
16    pub end: usize,
17}
18
19impl Row {
20    /// Whether this is the row a line starts on, which is where a frontend
21    /// puts the line number.
22    #[must_use]
23    pub const fn is_line_start(&self) -> bool {
24        self.start == 0
25    }
26
27    /// How many columns this row shows.
28    #[must_use]
29    pub const fn width(&self) -> usize {
30        self.end - self.start
31    }
32}
33
34/// Lays out `lines` into the rows that show them, breaking anything wider
35/// than `width` at the last space that fits, or mid-word for a word too long
36/// to ever fit.
37///
38/// With `wrap` off every line becomes a single row of its full length, which
39/// a frontend then scrolls horizontally.
40#[must_use]
41pub fn rows<'a>(lines: impl Iterator<Item = &'a str>, width: usize, wrap: bool) -> Vec<Row> {
42    let mut rows = Vec::new();
43    for (line, text) in lines.enumerate() {
44        let length = text.chars().count();
45        if !wrap {
46            rows.push(Row {
47                line,
48                start: 0,
49                end: length,
50            });
51            continue;
52        }
53        let characters: Vec<char> = text.chars().collect();
54        let mut start = 0;
55        while length - start > width.max(1) {
56            let limit = start + width.max(1);
57            let end = characters
58                .iter()
59                .take(limit + 1)
60                .skip(start + 1)
61                .rposition(|character| *character == ' ')
62                .map_or(limit, |index| start + index + 2);
63            rows.push(Row { line, start, end });
64            start = end;
65        }
66        rows.push(Row {
67            line,
68            start,
69            end: length,
70        });
71    }
72    rows
73}
74
75/// The column just past the last visible character of `row` within `line`.
76///
77/// A row broken at a space ends *after* that space, and a caret there shows up
78/// at the start of the row below rather than at the end of this one. Skipping
79/// the trailing spaces gives the end of the row the eye sees, which is where
80/// pressing End belongs.
81#[must_use]
82pub fn visible_end(line: &str, row: Row) -> usize {
83    let length = line.chars().count();
84    if row.end == length {
85        return row.end;
86    }
87    line.chars()
88        .take(row.end)
89        .collect::<Vec<char>>()
90        .iter()
91        .rposition(|character| *character != ' ')
92        .map_or(row.start, |index| (index + 1).max(row.start))
93}
94
95/// Finds the row holding a caret at `column` of `line`, and how far into that
96/// row it sits.
97///
98/// A caret at a wrap point belongs to the row that follows it, so typing
99/// continues where the eye is rather than off the right edge of the row above.
100#[must_use]
101pub fn locate(rows: &[Row], line: usize, column: usize) -> (usize, usize) {
102    let mut last = 0;
103    for (index, row) in rows.iter().enumerate() {
104        if row.line != line {
105            continue;
106        }
107        last = index;
108        if column < row.end {
109            return (index, column.saturating_sub(row.start));
110        }
111    }
112    (
113        last,
114        column.saturating_sub(rows.get(last).map_or(0, |row| row.start)),
115    )
116}
117
118#[cfg(test)]
119mod tests {
120    use super::{Row, locate, rows};
121
122    fn layout(text: &str, width: usize) -> Vec<Row> {
123        rows(text.split('\n'), width, true)
124    }
125
126    #[test]
127    fn a_line_that_fits_stays_one_row() {
128        assert_eq!(
129            layout("hello", 10),
130            vec![Row {
131                line: 0,
132                start: 0,
133                end: 5
134            }]
135        );
136    }
137
138    #[test]
139    fn a_long_line_breaks_after_a_space() {
140        let laid_out = layout("one two three", 8);
141        assert_eq!(laid_out.len(), 2);
142        assert_eq!(laid_out[0].end, 8);
143        assert_eq!(laid_out[1].start, 8);
144        assert_eq!(laid_out[1].end, 13);
145    }
146
147    #[test]
148    fn a_word_too_long_to_fit_breaks_mid_word() {
149        let laid_out = layout("abcdefghij", 4);
150        assert_eq!(laid_out.len(), 3);
151        assert_eq!(
152            laid_out[0],
153            Row {
154                line: 0,
155                start: 0,
156                end: 4
157            }
158        );
159        assert_eq!(
160            laid_out[2],
161            Row {
162                line: 0,
163                start: 8,
164                end: 10
165            }
166        );
167    }
168
169    #[test]
170    fn rows_of_a_line_leave_no_column_uncovered() {
171        for row in layout("a bb ccc dddd eeeee", 6).windows(2) {
172            assert_eq!(row[0].end, row[1].start);
173        }
174    }
175
176    #[test]
177    fn every_line_keeps_its_own_rows() {
178        let laid_out = layout("short\nlonger than that", 8);
179        assert_eq!(laid_out[0].line, 0);
180        assert!(laid_out[1..].iter().all(|row| row.line == 1));
181    }
182
183    #[test]
184    fn without_wrapping_a_line_is_one_row_however_long() {
185        assert_eq!(
186            rows("a very long line indeed".split('\n'), 4, false).len(),
187            1
188        );
189    }
190
191    #[test]
192    fn a_caret_at_a_wrap_point_belongs_to_the_row_below() {
193        let laid_out = layout("one two three", 8);
194        assert_eq!(locate(&laid_out, 0, 8), (1, 0));
195        assert_eq!(locate(&laid_out, 0, 7), (0, 7));
196    }
197
198    #[test]
199    fn the_visible_end_of_a_broken_row_sits_before_its_trailing_space() {
200        let laid_out = layout("one two three", 8);
201        assert_eq!(super::visible_end("one two three", laid_out[0]), 7);
202        assert_eq!(locate(&laid_out, 0, 7), (0, 7));
203    }
204
205    #[test]
206    fn the_visible_end_of_a_last_row_is_the_line_end() {
207        let laid_out = layout("one two three", 8);
208        assert_eq!(super::visible_end("one two three", laid_out[1]), 13);
209    }
210
211    #[test]
212    fn a_caret_at_the_very_end_stays_on_the_last_row() {
213        let laid_out = layout("one two three", 8);
214        assert_eq!(locate(&laid_out, 0, 13), (1, 5));
215    }
216}