Skip to main content

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}