rustpython_ruff_source_file/line_ranges.rs
1use crate::find_newline;
2use memchr::{memchr2, memrchr2};
3use ruff_text_size::{TextLen, TextRange, TextSize};
4use std::ops::Add;
5
6/// Extension trait for [`str`] that provides methods for working with ranges of lines.
7pub trait LineRanges {
8 /// Computes the start position of the line of `offset`.
9 ///
10 /// ## Examples
11 ///
12 /// ```
13 /// # use ruff_text_size::TextSize;
14 /// # use ruff_source_file::LineRanges;
15 ///
16 /// let text = "First line\nsecond line\rthird line";
17 ///
18 /// assert_eq!(text.line_start(TextSize::from(0)), TextSize::from(0));
19 /// assert_eq!(text.line_start(TextSize::from(4)), TextSize::from(0));
20 ///
21 /// assert_eq!(text.line_start(TextSize::from(14)), TextSize::from(11));
22 /// assert_eq!(text.line_start(TextSize::from(28)), TextSize::from(23));
23 /// ```
24 ///
25 /// ## Panics
26 /// If `offset` is out of bounds.
27 fn line_start(&self, offset: TextSize) -> TextSize;
28
29 /// Computes the start position of the file contents: either the first byte, or the byte after
30 /// the BOM.
31 fn bom_start_offset(&self) -> TextSize;
32
33 /// Returns `true` if `offset` is at the start of a line.
34 fn is_at_start_of_line(&self, offset: TextSize) -> bool {
35 self.line_start(offset) == offset
36 }
37
38 /// Computes the offset that is right after the newline character that ends `offset`'s line.
39 ///
40 /// ## Examples
41 ///
42 /// ```
43 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
44 /// # use ruff_source_file::LineRanges;
45 ///
46 /// let text = "First line\nsecond line\r\nthird line";
47 ///
48 /// assert_eq!(text.full_line_end(TextSize::from(3)), TextSize::from(11));
49 /// assert_eq!(text.full_line_end(TextSize::from(14)), TextSize::from(24));
50 /// assert_eq!(text.full_line_end(TextSize::from(28)), TextSize::from(34));
51 /// ```
52 ///
53 /// ## Panics
54 ///
55 /// If `offset` is passed the end of the content.
56 fn full_line_end(&self, offset: TextSize) -> TextSize;
57
58 /// Computes the offset that is right before the newline character that ends `offset`'s line.
59 ///
60 /// ## Examples
61 ///
62 /// ```
63 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
64 /// # use ruff_source_file::LineRanges;
65 ///
66 /// let text = "First line\nsecond line\r\nthird line";
67 ///
68 /// assert_eq!(text.line_end(TextSize::from(3)), TextSize::from(10));
69 /// assert_eq!(text.line_end(TextSize::from(14)), TextSize::from(22));
70 /// assert_eq!(text.line_end(TextSize::from(28)), TextSize::from(34));
71 /// ```
72 ///
73 /// ## Panics
74 ///
75 /// If `offset` is passed the end of the content.
76 fn line_end(&self, offset: TextSize) -> TextSize;
77
78 /// Computes the range of this `offset`s line.
79 ///
80 /// The range starts at the beginning of the line and goes up to, and including, the new line character
81 /// at the end of the line.
82 ///
83 /// ## Examples
84 ///
85 /// ```
86 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
87 /// # use ruff_source_file::LineRanges;
88 ///
89 /// let text = "First line\nsecond line\r\nthird line";
90 ///
91 /// assert_eq!(text.full_line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(11)));
92 /// assert_eq!(text.full_line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(24)));
93 /// assert_eq!(text.full_line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34)));
94 /// ```
95 ///
96 /// ## Panics
97 /// If `offset` is out of bounds.
98 fn full_line_range(&self, offset: TextSize) -> TextRange {
99 TextRange::new(self.line_start(offset), self.full_line_end(offset))
100 }
101
102 /// Computes the range of this `offset`s line ending before the newline character.
103 ///
104 /// The range starts at the beginning of the line and goes up to, but excluding, the new line character
105 /// at the end of the line.
106 ///
107 /// ## Examples
108 ///
109 /// ```
110 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
111 /// # use ruff_source_file::LineRanges;
112 ///
113 /// let text = "First line\nsecond line\r\nthird line";
114 ///
115 /// assert_eq!(text.line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(10)));
116 /// assert_eq!(text.line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(22)));
117 /// assert_eq!(text.line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34)));
118 /// ```
119 ///
120 /// ## Panics
121 /// If `offset` is out of bounds.
122 fn line_range(&self, offset: TextSize) -> TextRange {
123 TextRange::new(self.line_start(offset), self.line_end(offset))
124 }
125
126 /// Returns the text of the `offset`'s line.
127 ///
128 /// The line includes the newline characters at the end of the line.
129 ///
130 /// ## Examples
131 ///
132 /// ```
133 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
134 /// # use ruff_source_file::LineRanges;
135 ///
136 /// let text = "First line\nsecond line\r\nthird line";
137 ///
138 /// assert_eq!(text.full_line_str(TextSize::from(3)), "First line\n");
139 /// assert_eq!(text.full_line_str(TextSize::from(14)), "second line\r\n");
140 /// assert_eq!(text.full_line_str(TextSize::from(28)), "third line");
141 /// ```
142 ///
143 /// ## Panics
144 /// If `offset` is out of bounds.
145 fn full_line_str(&self, offset: TextSize) -> &str;
146
147 /// Returns the text of the `offset`'s line.
148 ///
149 /// Excludes the newline characters at the end of the line.
150 ///
151 /// ## Examples
152 ///
153 /// ```
154 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
155 /// # use ruff_source_file::LineRanges;
156 ///
157 /// let text = "First line\nsecond line\r\nthird line";
158 ///
159 /// assert_eq!(text.line_str(TextSize::from(3)), "First line");
160 /// assert_eq!(text.line_str(TextSize::from(14)), "second line");
161 /// assert_eq!(text.line_str(TextSize::from(28)), "third line");
162 /// ```
163 ///
164 /// ## Panics
165 /// If `offset` is out of bounds.
166 fn line_str(&self, offset: TextSize) -> &str;
167
168 /// Computes the range of all lines that this `range` covers.
169 ///
170 /// The range starts at the beginning of the line at `range.start()` and goes up to, and including, the new line character
171 /// at the end of `range.ends()`'s line.
172 ///
173 /// ## Examples
174 ///
175 /// ```
176 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
177 /// # use ruff_source_file::LineRanges;
178 ///
179 /// let text = "First line\nsecond line\r\nthird line";
180 ///
181 /// assert_eq!(
182 /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))),
183 /// TextRange::new(TextSize::from(0), TextSize::from(11))
184 /// );
185 /// assert_eq!(
186 /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))),
187 /// TextRange::new(TextSize::from(0), TextSize::from(24))
188 /// );
189 /// ```
190 ///
191 /// ## Panics
192 /// If the start or end of `range` is out of bounds.
193 fn full_lines_range(&self, range: TextRange) -> TextRange {
194 TextRange::new(
195 self.line_start(range.start()),
196 self.full_line_end(range.end()),
197 )
198 }
199
200 /// Computes the range of all lines that this `range` covers.
201 ///
202 /// The range starts at the beginning of the line at `range.start()` and goes up to, but excluding, the new line character
203 /// at the end of `range.end()`'s line.
204 ///
205 /// ## Examples
206 ///
207 /// ```
208 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
209 /// # use ruff_source_file::LineRanges;
210 ///
211 /// let text = "First line\nsecond line\r\nthird line";
212 ///
213 /// assert_eq!(
214 /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))),
215 /// TextRange::new(TextSize::from(0), TextSize::from(10))
216 /// );
217 /// assert_eq!(
218 /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))),
219 /// TextRange::new(TextSize::from(0), TextSize::from(22))
220 /// );
221 /// ```
222 ///
223 /// ## Panics
224 /// If the start or end of `range` is out of bounds.
225 fn lines_range(&self, range: TextRange) -> TextRange {
226 TextRange::new(self.line_start(range.start()), self.line_end(range.end()))
227 }
228
229 /// Returns true if the text of `range` contains any line break.
230 ///
231 /// ```
232 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
233 /// # use ruff_source_file::LineRanges;
234 ///
235 /// let text = "First line\nsecond line\r\nthird line";
236 ///
237 /// assert!(
238 /// !text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(5))),
239 /// );
240 /// assert!(
241 /// text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(14))),
242 /// );
243 /// ```
244 ///
245 /// ## Panics
246 /// If the `range` is out of bounds.
247 fn contains_line_break(&self, range: TextRange) -> bool;
248
249 /// Returns the text of all lines that include `range`.
250 ///
251 /// ## Examples
252 ///
253 /// ```
254 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
255 /// # use ruff_source_file::LineRanges;
256 ///
257 /// let text = "First line\nsecond line\r\nthird line";
258 ///
259 /// assert_eq!(
260 /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))),
261 /// "First line"
262 /// );
263 /// assert_eq!(
264 /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))),
265 /// "First line\nsecond line"
266 /// );
267 /// ```
268 ///
269 /// ## Panics
270 /// If the start or end of `range` is out of bounds.
271 fn lines_str(&self, range: TextRange) -> &str;
272
273 /// Returns the text of all lines that include `range`.
274 ///
275 /// Includes the newline characters of the last line.
276 ///
277 /// ## Examples
278 ///
279 /// ```
280 /// # use ruff_text_size::{Ranged, TextRange, TextSize};
281 /// # use ruff_source_file::LineRanges;
282 ///
283 /// let text = "First line\nsecond line\r\nthird line";
284 ///
285 /// assert_eq!(
286 /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))),
287 /// "First line\n"
288 /// );
289 /// assert_eq!(
290 /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))),
291 /// "First line\nsecond line\r\n"
292 /// );
293 /// ```
294 ///
295 /// ## Panics
296 /// If the start or end of `range` is out of bounds.
297 fn full_lines_str(&self, range: TextRange) -> &str;
298
299 /// The number of lines `range` spans.
300 ///
301 /// ## Examples
302 ///
303 /// ```
304 /// # use ruff_text_size::{Ranged, TextRange};
305 /// # use ruff_source_file::LineRanges;
306 ///
307 /// assert_eq!("a\nb".count_lines(TextRange::up_to(1.into())), 0);
308 /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(3.into())), 1, "Up to the end of the second line");
309 /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(4.into())), 2, "In between the line break characters");
310 /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(5.into())), 2);
311 /// assert_eq!("Single line".count_lines(TextRange::up_to(13.into())), 0);
312 /// assert_eq!("out\nof\nbounds end".count_lines(TextRange::up_to(55.into())), 2);
313 /// ```
314 fn count_lines(&self, range: TextRange) -> u32 {
315 let mut count = 0;
316 let mut line_end = self.line_end(range.start());
317
318 loop {
319 let next_line_start = self.full_line_end(line_end);
320
321 // Reached the end of the string
322 if next_line_start == line_end {
323 break count;
324 }
325
326 // Range ends at the line boundary
327 if line_end >= range.end() {
328 break count;
329 }
330
331 count += 1;
332
333 line_end = self.line_end(next_line_start);
334 }
335 }
336}
337
338impl LineRanges for str {
339 fn line_start(&self, offset: TextSize) -> TextSize {
340 let bytes = self[TextRange::up_to(offset)].as_bytes();
341 if let Some(index) = memrchr2(b'\n', b'\r', bytes) {
342 // SAFETY: Safe because `index < offset`
343 TextSize::try_from(index).unwrap().add(TextSize::from(1))
344 } else {
345 self.bom_start_offset()
346 }
347 }
348
349 fn bom_start_offset(&self) -> TextSize {
350 if self.starts_with('\u{feff}') {
351 // Skip the BOM.
352 '\u{feff}'.text_len()
353 } else {
354 // Start of file.
355 TextSize::default()
356 }
357 }
358
359 fn full_line_end(&self, offset: TextSize) -> TextSize {
360 let slice = &self[usize::from(offset)..];
361 if let Some((index, line_ending)) = find_newline(slice) {
362 offset + TextSize::try_from(index).unwrap() + line_ending.text_len()
363 } else {
364 self.text_len()
365 }
366 }
367
368 fn line_end(&self, offset: TextSize) -> TextSize {
369 let slice = &self[offset.to_usize()..];
370 if let Some(index) = memchr2(b'\n', b'\r', slice.as_bytes()) {
371 offset + TextSize::try_from(index).unwrap()
372 } else {
373 self.text_len()
374 }
375 }
376
377 fn full_line_str(&self, offset: TextSize) -> &str {
378 &self[self.full_line_range(offset)]
379 }
380
381 fn line_str(&self, offset: TextSize) -> &str {
382 &self[self.line_range(offset)]
383 }
384
385 fn contains_line_break(&self, range: TextRange) -> bool {
386 memchr2(b'\n', b'\r', self[range].as_bytes()).is_some()
387 }
388
389 fn lines_str(&self, range: TextRange) -> &str {
390 &self[self.lines_range(range)]
391 }
392
393 fn full_lines_str(&self, range: TextRange) -> &str {
394 &self[self.full_lines_range(range)]
395 }
396}