1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub struct Row {
11 pub line: usize,
13 pub start: usize,
15 pub end: usize,
17}
18
19impl Row {
20 #[must_use]
23 pub const fn is_line_start(&self) -> bool {
24 self.start == 0
25 }
26
27 #[must_use]
29 pub const fn width(&self) -> usize {
30 self.end - self.start
31 }
32}
33
34#[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#[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#[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}