1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct LineRec {
26 pub source: u16,
28 pub lineno: u32,
30 pub start: u32,
32 pub end: u32,
33 indent: u32,
37}
38
39impl LineRec {
40 pub(crate) fn new(source: u16, lineno: u32, start: u32, end: u32, line_text: &str) -> LineRec {
43 debug_assert_eq!(line_text.len(), (end - start) as usize);
44 let indent = (line_text.len() - line_text.trim_start_matches(' ').len()) as u32;
45 LineRec {
46 source,
47 lineno,
48 start,
49 end,
50 indent,
51 }
52 }
53
54 pub(crate) fn is_blank(&self) -> bool {
55 self.start == self.end
56 }
57
58 pub(crate) fn indent(&self) -> usize {
59 self.indent as usize
60 }
61
62 pub(crate) fn dedented(&self, n: usize) -> LineRec {
65 let n = n.min(self.indent()) as u32;
66 LineRec {
67 start: self.start + n,
68 indent: self.indent - n,
69 ..*self
70 }
71 }
72
73 pub(crate) fn slice<'t>(&self, text: &'t str) -> &'t str {
75 &text[self.start as usize..self.end as usize]
76 }
77}
78
79#[derive(Debug, Clone, Default)]
82pub struct Lines {
83 text: String,
84 recs: Vec<LineRec>,
85}
86
87fn is_line_boundary(c: char) -> bool {
88 matches!(
89 c,
90 '\n' | '\r' | '\x1c' | '\x1d' | '\x1e' | '\u{85}' | '\u{2028}' | '\u{2029}'
91 )
92}
93
94fn process_line(raw: &str, out: &mut String) {
95 let base = out.len();
97 let mut col = 0usize;
98 for c in raw.chars() {
99 match c {
100 '\t' => {
101 let next_stop = (col / 8 + 1) * 8;
102 for _ in col..next_stop {
103 out.push(' ');
104 }
105 col = next_stop;
106 }
107 '\x0b' | '\x0c' => {
108 out.push(' ');
109 col += 1;
110 }
111 _ => {
112 out.push(c);
113 col += 1;
114 }
115 }
116 }
117 out.truncate(base + out[base..].trim_end_matches(crate::utils::py_isspace).len());
120}
121
122impl Lines {
123 pub fn new(source: &str) -> Lines {
126 Lines::for_source(source, 0, 1)
127 }
128
129 pub(crate) fn for_source(source: &str, source_id: u16, first_lineno: u32) -> Lines {
133 let mut text = String::with_capacity(source.len());
134 let mut recs = Vec::new();
135 let push_line = |raw: &str, text: &mut String, recs: &mut Vec<LineRec>| {
136 if !recs.is_empty() {
137 text.push('\n');
138 }
139 let start = text.len() as u32;
140 process_line(raw, text);
141 let end = text.len() as u32;
142 let lineno = first_lineno + recs.len() as u32;
143 recs.push(LineRec::new(
144 source_id,
145 lineno,
146 start,
147 end,
148 &text[start as usize..end as usize],
149 ));
150 };
151 let bytes_len = source.len();
152 let mut line_start = 0usize;
153 let mut chars = source.char_indices().peekable();
154 while let Some((i, c)) = chars.next() {
155 if is_line_boundary(c) {
156 push_line(&source[line_start..i], &mut text, &mut recs);
157 if c == '\r' {
159 if let Some(&(_, '\n')) = chars.peek() {
160 chars.next();
161 }
162 }
163 line_start = match chars.peek() {
164 Some(&(j, _)) => j,
165 None => bytes_len,
166 };
167 }
168 }
169 if line_start < bytes_len {
170 push_line(&source[line_start..], &mut text, &mut recs);
171 }
172 Lines { text, recs }
173 }
174
175 #[cfg(test)]
176 pub(crate) fn recs(&self) -> &[LineRec] {
177 &self.recs
178 }
179
180 pub(crate) fn into_parts(self) -> (String, Vec<LineRec>) {
183 (self.text, self.recs)
184 }
185
186 #[cfg(test)]
187 fn line_str(&self, i: usize) -> &str {
188 self.recs[i].slice(&self.text)
189 }
190
191 #[cfg(test)]
192 fn len(&self) -> usize {
193 self.recs.len()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn tabs_expand_to_8_col_stops() {
203 assert_eq!(Lines::new("a\tb").line_str(0), "a b");
204 assert_eq!(Lines::new("\ta").line_str(0), " a");
205 assert_eq!(Lines::new("ab\tc").line_str(0), "ab c");
206 assert_eq!(Lines::new("abcdefgh\tz").line_str(0), "abcdefgh z");
207 assert_eq!(Lines::new("x\ty\tz").line_str(0), "x y z");
208 }
209
210 #[test]
211 fn trailing_whitespace_stripped_and_recs_map_into_processed_text() {
212 let l = Lines::new("one \ntwo");
213 assert_eq!(l.line_str(0), "one");
214 assert_eq!(l.line_str(1), "two");
215 assert_eq!(l.text, "one\ntwo");
217 assert_eq!(
218 (l.recs[1].start as usize, l.recs[1].end as usize),
219 (4, 7),
220 "second line's range starts past the first line and its separator"
221 );
222 }
223
224 #[test]
225 fn a_three_line_document_yields_source_0_linenos_1_to_3() {
226 let l = Lines::new("one\ntwo\nthree");
227 let stream: Vec<(u16, u32)> = l.recs().iter().map(|r| (r.source, r.lineno)).collect();
228 assert_eq!(stream, vec![(0, 1), (0, 2), (0, 3)]);
229 }
230
231 #[test]
232 fn for_source_stamps_the_given_source_id_and_base_lineno() {
233 let l = Lines::for_source("a\nb", 3, 10);
234 let stream: Vec<(u16, u32)> = l.recs().iter().map(|r| (r.source, r.lineno)).collect();
235 assert_eq!(stream, vec![(3, 10), (3, 11)]);
236 }
237
238 #[test]
239 fn vertical_tab_and_formfeed_become_spaces() {
240 assert_eq!(Lines::new("a\x0bb\x0cc").line_str(0), "a b c");
241 }
242
243 #[test]
244 fn crlf_and_cr_split_without_stray_cr() {
245 let l = Lines::new("one\r\ntwo\rthree");
246 assert_eq!(l.len(), 3);
247 assert_eq!(l.line_str(0), "one");
248 assert_eq!(l.line_str(1), "two");
249 assert_eq!(l.line_str(2), "three");
250 }
251
252 #[test]
253 fn trailing_newline_produces_no_empty_last_line() {
254 assert_eq!(Lines::new("a\n").len(), 1);
255 let l = Lines::new("a\n\n");
256 assert_eq!(l.len(), 2);
257 assert!(l.recs[1].is_blank());
258 }
259
260 #[test]
261 fn indent_counts_leading_spaces() {
262 let l = Lines::new(" four\n\tone-tab");
263 assert_eq!(l.recs[0].indent(), 4);
264 assert_eq!(l.recs[1].indent(), 8);
265 }
266
267 #[test]
268 fn dedent_moves_the_view_start_and_shrinks_the_cached_indent() {
269 let l = Lines::new(" body");
270 let d = l.recs[0].dedented(4);
271 assert_eq!(d.slice(&l.text), "body");
272 assert_eq!(d.indent(), 0);
273 assert_eq!(d.lineno, 1);
274 }
275}